// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v2"
)
type DockerCompose struct {
Version string `yaml:"version"`
Services map[string]*Container `yaml:"services"`
}
type Container struct {
Command string `yaml:"command,omitempty"`
Image string `yaml:"image,omitempty"`
Network []string `yaml:"networks,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
}
func main() {
validServices := map[string]int{
"mysql": 3306,
"postgres": 5432,
"minio": 9000,
"inbucket": 9001,
"openldap": 389,
"elasticsearch": 9200,
"dejavu": 1358,
"keycloak": 8080,
"prometheus": 9090,
"grafana": 3000,
"mysql-read-replica": 3306, // FIXME: not recognizing the successfully running service on port 3307.
}
command := []string{}
for _, arg := range os.Args[1:] {
port, ok := validServices[arg]
if !ok {
panic(fmt.Sprintf("Unknown service %s", arg))
}
command = append(command, fmt.Sprintf("%s:%d", arg, port))
}
var dockerCompose DockerCompose
dockerCompose.Version = "2.4"
dockerCompose.Services = map[string]*Container{}
dockerCompose.Services["start_dependencies"] = &Container{
Image: "mattermost/mattermost-wait-for-dep:latest",
Network: []string{"mm-test"},
DependsOn: os.Args[1:],
Command: strings.Join(command, " "),
}
resultData, err := yaml.Marshal(dockerCompose)
if err != nil {
panic(fmt.Sprintf("Unable to serialize the docker-compose file: %s.", err.Error()))
}
fmt.Println(string(resultData))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
const (
AccessTokenGrantType = "authorization_code"
AccessTokenType = "bearer"
RefreshTokenGrantType = "refresh_token"
)
type AccessData struct {
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"`
Scope string `json:"scope"`
}
type AccessResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresInSeconds int32 `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
IdToken string `json:"id_token"`
}
// IsValid validates the AccessData and returns an error if it isn't configured
// correctly.
func (ad *AccessData) IsValid() *AppError {
if ad.ClientId == "" || len(ad.ClientId) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if ad.UserId == "" || len(ad.UserId) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.Token) != 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.access_token.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.RefreshToken) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "", http.StatusBadRequest)
}
if ad.RedirectUri == "" || len(ad.RedirectUri) > 256 || !IsValidHTTPURL(ad.RedirectUri) {
return NewAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (ad *AccessData) IsExpired() bool {
if ad.ExpiresAt <= 0 {
return false
}
if GetMillis() > ad.ExpiresAt {
return true
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"strings"
"github.com/francoispqt/gojay"
)
// AuditModelTypeConv converts key model types to something better suited for audit output.
func AuditModelTypeConv(val any) (newVal any, converted bool) {
if val == nil {
return nil, false
}
switch v := val.(type) {
case *Channel:
return newAuditChannel(v), true
case Channel:
return newAuditChannel(&v), true
case *Team:
return newAuditTeam(v), true
case Team:
return newAuditTeam(&v), true
case *User:
return newAuditUser(v), true
case User:
return newAuditUser(&v), true
case *UserPatch:
return newAuditUserPatch(v), true
case UserPatch:
return newAuditUserPatch(&v), true
case *Command:
return newAuditCommand(v), true
case Command:
return newAuditCommand(&v), true
case *CommandArgs:
return newAuditCommandArgs(v), true
case CommandArgs:
return newAuditCommandArgs(&v), true
case *Bot:
return newAuditBot(v), true
case Bot:
return newAuditBot(&v), true
case *ChannelModerationPatch:
return newAuditChannelModerationPatch(v), true
case ChannelModerationPatch:
return newAuditChannelModerationPatch(&v), true
case *Emoji:
return newAuditEmoji(v), true
case Emoji:
return newAuditEmoji(&v), true
case *FileInfo:
return newAuditFileInfo(v), true
case FileInfo:
return newAuditFileInfo(&v), true
case *Group:
return newAuditGroup(v), true
case Group:
return newAuditGroup(&v), true
case *Job:
return newAuditJob(v), true
case Job:
return newAuditJob(&v), true
case *OAuthApp:
return newAuditOAuthApp(v), true
case OAuthApp:
return newAuditOAuthApp(&v), true
case *Post:
return newAuditPost(v), true
case Post:
return newAuditPost(&v), true
case *Role:
return newAuditRole(v), true
case Role:
return newAuditRole(&v), true
case *Scheme:
return newAuditScheme(v), true
case Scheme:
return newAuditScheme(&v), true
case *SchemeRoles:
return newAuditSchemeRoles(v), true
case SchemeRoles:
return newAuditSchemeRoles(&v), true
case *Session:
return newAuditSession(v), true
case Session:
return newAuditSession(&v), true
case *IncomingWebhook:
return newAuditIncomingWebhook(v), true
case IncomingWebhook:
return newAuditIncomingWebhook(&v), true
case *OutgoingWebhook:
return newAuditOutgoingWebhook(v), true
case OutgoingWebhook:
return newAuditOutgoingWebhook(&v), true
case *RemoteCluster:
return newRemoteCluster(v), true
case RemoteCluster:
return newRemoteCluster(&v), true
}
return val, false
}
type auditChannel struct {
ID string
Name string
Type ChannelType
}
// newAuditChannel creates a simplified representation of Channel for output to audit log.
func newAuditChannel(c *Channel) auditChannel {
var channel auditChannel
if c != nil {
channel.ID = c.Id
channel.Name = c.Name
channel.Type = c.Type
}
return channel
}
func (c auditChannel) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", c.ID)
enc.StringKey("name", c.Name)
enc.StringKey("type", string(c.Type))
}
func (c auditChannel) IsNil() bool {
return false
}
type auditTeam struct {
ID string
Name string
Type string
}
// newAuditTeam creates a simplified representation of Team for output to audit log.
func newAuditTeam(t *Team) auditTeam {
var team auditTeam
if t != nil {
team.ID = t.Id
team.Name = t.Name
team.Type = t.Type
}
return team
}
func (t auditTeam) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", t.ID)
enc.StringKey("name", t.Name)
enc.StringKey("type", t.Type)
}
func (t auditTeam) IsNil() bool {
return false
}
type auditUser struct {
ID string
Name string
Roles string
}
// newAuditUser creates a simplified representation of User for output to audit log.
func newAuditUser(u *User) auditUser {
var user auditUser
if u != nil {
user.ID = u.Id
user.Name = u.Username
user.Roles = u.Roles
}
return user
}
type auditUserPatch struct {
Name string
}
// newAuditUserPatch creates a simplified representation of UserPatch for output to audit log.
func newAuditUserPatch(up *UserPatch) auditUserPatch {
var userPatch auditUserPatch
if up != nil {
if up.Username != nil {
userPatch.Name = *up.Username
}
}
return userPatch
}
func (u auditUser) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", u.ID)
enc.StringKey("name", u.Name)
enc.StringKey("roles", u.Roles)
}
func (u auditUser) IsNil() bool {
return false
}
type auditCommand struct {
ID string
CreatorID string
TeamID string
Trigger string
Method string
Username string
IconURL string
AutoComplete bool
AutoCompleteDesc string
AutoCompleteHint string
DisplayName string
Description string
URL string
}
// newAuditCommand creates a simplified representation of Command for output to audit log.
func newAuditCommand(c *Command) auditCommand {
var cmd auditCommand
if c != nil {
cmd.ID = c.Id
cmd.CreatorID = c.CreatorId
cmd.TeamID = c.TeamId
cmd.Trigger = c.Trigger
cmd.Method = c.Method
cmd.Username = c.Username
cmd.IconURL = c.IconURL
cmd.AutoComplete = c.AutoComplete
cmd.AutoCompleteDesc = c.AutoCompleteDesc
cmd.AutoCompleteHint = c.AutoCompleteHint
cmd.DisplayName = c.DisplayName
cmd.Description = c.Description
cmd.URL = c.URL
}
return cmd
}
func (cmd auditCommand) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", cmd.ID)
enc.StringKey("creator_id", cmd.CreatorID)
enc.StringKey("team_id", cmd.TeamID)
enc.StringKey("trigger", cmd.Trigger)
enc.StringKey("method", cmd.Method)
enc.StringKey("username", cmd.Username)
enc.StringKey("icon_url", cmd.IconURL)
enc.BoolKey("auto_complete", cmd.AutoComplete)
enc.StringKey("auto_complete_desc", cmd.AutoCompleteDesc)
enc.StringKey("auto_complete_hint", cmd.AutoCompleteHint)
enc.StringKey("display", cmd.DisplayName)
enc.StringKey("desc", cmd.Description)
enc.StringKey("url", cmd.URL)
}
func (cmd auditCommand) IsNil() bool {
return false
}
type auditCommandArgs struct {
ChannelID string
TeamID string
TriggerID string
Command string
}
// newAuditCommandArgs creates a simplified representation of CommandArgs for output to audit log.
func newAuditCommandArgs(ca *CommandArgs) auditCommandArgs {
var cmdargs auditCommandArgs
if ca != nil {
cmdargs.ChannelID = ca.ChannelId
cmdargs.TeamID = ca.TeamId
cmdargs.TriggerID = ca.TriggerId
cmdFields := strings.Fields(ca.Command)
if len(cmdFields) > 0 {
cmdargs.Command = cmdFields[0]
}
}
return cmdargs
}
func (ca auditCommandArgs) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("channel_id", ca.ChannelID)
enc.StringKey("team_id", ca.TriggerID)
enc.StringKey("trigger_id", ca.TeamID)
enc.StringKey("command", ca.Command)
}
func (ca auditCommandArgs) IsNil() bool {
return false
}
type auditBot struct {
UserID string
Username string
Displayname string
}
// newAuditBot creates a simplified representation of Bot for output to audit log.
func newAuditBot(b *Bot) auditBot {
var bot auditBot
if b != nil {
bot.UserID = b.UserId
bot.Username = b.Username
bot.Displayname = b.DisplayName
}
return bot
}
func (b auditBot) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("user_id", b.UserID)
enc.StringKey("username", b.Username)
enc.StringKey("display", b.Displayname)
}
func (b auditBot) IsNil() bool {
return false
}
type auditChannelModerationPatch struct {
Name string
RoleGuests bool
RoleMembers bool
}
// newAuditChannelModerationPatch creates a simplified representation of ChannelModerationPatch for output to audit log.
func newAuditChannelModerationPatch(p *ChannelModerationPatch) auditChannelModerationPatch {
var patch auditChannelModerationPatch
if p != nil {
if p.Name != nil {
patch.Name = *p.Name
}
if p.Roles.Guests != nil {
patch.RoleGuests = *p.Roles.Guests
}
if p.Roles.Members != nil {
patch.RoleMembers = *p.Roles.Members
}
}
return patch
}
func (p auditChannelModerationPatch) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("name", p.Name)
enc.BoolKey("role_guests", p.RoleGuests)
enc.BoolKey("role_members", p.RoleMembers)
}
func (p auditChannelModerationPatch) IsNil() bool {
return false
}
type auditEmoji struct {
ID string
Name string
}
// newAuditEmoji creates a simplified representation of Emoji for output to audit log.
func newAuditEmoji(e *Emoji) auditEmoji {
var emoji auditEmoji
if e != nil {
emoji.ID = e.Id
emoji.Name = e.Name
}
return emoji
}
func (e auditEmoji) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", e.ID)
enc.StringKey("name", e.Name)
}
func (e auditEmoji) IsNil() bool {
return false
}
type auditFileInfo struct {
ID string
PostID string
Path string
Name string
Extension string
Size int64
}
// newAuditFileInfo creates a simplified representation of FileInfo for output to audit log.
func newAuditFileInfo(f *FileInfo) auditFileInfo {
var fi auditFileInfo
if f != nil {
fi.ID = f.Id
fi.PostID = f.PostId
fi.Path = f.Path
fi.Name = f.Name
fi.Extension = f.Extension
fi.Size = f.Size
}
return fi
}
func (fi auditFileInfo) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", fi.ID)
enc.StringKey("post_id", fi.PostID)
enc.StringKey("path", fi.Path)
enc.StringKey("name", fi.Name)
enc.StringKey("ext", fi.Extension)
enc.Int64Key("size", fi.Size)
}
func (fi auditFileInfo) IsNil() bool {
return false
}
type auditGroup struct {
ID string
Name string
DisplayName string
Description string
}
// newAuditGroup creates a simplified representation of Group for output to audit log.
func newAuditGroup(g *Group) auditGroup {
var group auditGroup
if g != nil {
group.ID = g.Id
if g.Name == nil {
group.Name = ""
} else {
group.Name = *g.Name
}
group.DisplayName = g.DisplayName
group.Description = g.Description
}
return group
}
func (g auditGroup) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", g.ID)
enc.StringKey("name", g.Name)
enc.StringKey("display", g.DisplayName)
enc.StringKey("desc", g.Description)
}
func (g auditGroup) IsNil() bool {
return false
}
type auditJob struct {
ID string
Type string
Priority int64
StartAt int64
}
// newAuditJob creates a simplified representation of Job for output to audit log.
func newAuditJob(j *Job) auditJob {
var job auditJob
if j != nil {
job.ID = j.Id
job.Type = j.Type
job.Priority = j.Priority
job.StartAt = j.StartAt
}
return job
}
func (j auditJob) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", j.ID)
enc.StringKey("type", j.Type)
enc.Int64Key("priority", j.Priority)
enc.Int64Key("start_at", j.StartAt)
}
func (j auditJob) IsNil() bool {
return false
}
type auditOAuthApp struct {
ID string
CreatorID string
Name string
Description string
IsTrusted bool
}
// newAuditOAuthApp creates a simplified representation of OAuthApp for output to audit log.
func newAuditOAuthApp(o *OAuthApp) auditOAuthApp {
var oauth auditOAuthApp
if o != nil {
oauth.ID = o.Id
oauth.CreatorID = o.CreatorId
oauth.Name = o.Name
oauth.Description = o.Description
oauth.IsTrusted = o.IsTrusted
}
return oauth
}
func (o auditOAuthApp) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", o.ID)
enc.StringKey("creator_id", o.CreatorID)
enc.StringKey("name", o.Name)
enc.StringKey("desc", o.Description)
enc.BoolKey("trusted", o.IsTrusted)
}
func (o auditOAuthApp) IsNil() bool {
return false
}
type auditPost struct {
ID string
ChannelID string
Type string
IsPinned bool
}
// newAuditPost creates a simplified representation of Post for output to audit log.
func newAuditPost(p *Post) auditPost {
var post auditPost
if p != nil {
post.ID = p.Id
post.ChannelID = p.ChannelId
post.Type = p.Type
post.IsPinned = p.IsPinned
}
return post
}
func (p auditPost) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", p.ID)
enc.StringKey("channel_id", p.ChannelID)
enc.StringKey("type", p.Type)
enc.BoolKey("pinned", p.IsPinned)
}
func (p auditPost) IsNil() bool {
return false
}
type auditRole struct {
ID string
Name string
DisplayName string
Permissions []string
SchemeManaged bool
BuiltIn bool
}
// newAuditRole creates a simplified representation of Role for output to audit log.
func newAuditRole(r *Role) auditRole {
var role auditRole
if r != nil {
role.ID = r.Id
role.Name = r.Name
role.DisplayName = r.DisplayName
role.Permissions = append(role.Permissions, r.Permissions...)
role.SchemeManaged = r.SchemeManaged
role.BuiltIn = r.BuiltIn
}
return role
}
func (r auditRole) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", r.ID)
enc.StringKey("name", r.Name)
enc.StringKey("display", r.DisplayName)
enc.SliceStringKey("perms", r.Permissions)
enc.BoolKey("schemeManaged", r.SchemeManaged)
enc.BoolKey("builtin", r.BuiltIn)
}
func (r auditRole) IsNil() bool {
return false
}
type auditScheme struct {
ID string
Name string
DisplayName string
Scope string
}
// newAuditScheme creates a simplified representation of Scheme for output to audit log.
func newAuditScheme(s *Scheme) auditScheme {
var scheme auditScheme
if s != nil {
scheme.ID = s.Id
scheme.Name = s.Name
scheme.DisplayName = s.DisplayName
scheme.Scope = s.Scope
}
return scheme
}
func (s auditScheme) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", s.ID)
enc.StringKey("name", s.Name)
enc.StringKey("display", s.DisplayName)
enc.StringKey("scope", s.Scope)
}
func (s auditScheme) IsNil() bool {
return false
}
type auditSchemeRoles struct {
SchemeAdmin bool
SchemeUser bool
SchemeGuest bool
}
// newAuditSchemeRoles creates a simplified representation of SchemeRoles for output to audit log.
func newAuditSchemeRoles(s *SchemeRoles) auditSchemeRoles {
var roles auditSchemeRoles
if s != nil {
roles.SchemeAdmin = s.SchemeAdmin
roles.SchemeUser = s.SchemeUser
roles.SchemeGuest = s.SchemeGuest
}
return roles
}
func (s auditSchemeRoles) MarshalJSONObject(enc *gojay.Encoder) {
enc.BoolKey("admin", s.SchemeAdmin)
enc.BoolKey("user", s.SchemeUser)
enc.BoolKey("guest", s.SchemeGuest)
}
func (s auditSchemeRoles) IsNil() bool {
return false
}
type auditSession struct {
ID string
UserId string
DeviceId string
}
// newAuditSession creates a simplified representation of Session for output to audit log.
func newAuditSession(s *Session) auditSession {
var session auditSession
if s != nil {
session.ID = s.Id
session.UserId = s.UserId
session.DeviceId = s.DeviceId
}
return session
}
func (s auditSession) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", s.ID)
enc.StringKey("user_id", s.UserId)
enc.StringKey("device_id", s.DeviceId)
}
func (s auditSession) IsNil() bool {
return false
}
type auditIncomingWebhook struct {
ID string
ChannelID string
TeamId string
DisplayName string
Description string
}
// newAuditIncomingWebhook creates a simplified representation of IncomingWebhook for output to audit log.
func newAuditIncomingWebhook(h *IncomingWebhook) auditIncomingWebhook {
var hook auditIncomingWebhook
if h != nil {
hook.ID = h.Id
hook.ChannelID = h.ChannelId
hook.TeamId = h.TeamId
hook.DisplayName = h.DisplayName
hook.Description = h.Description
}
return hook
}
func (h auditIncomingWebhook) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", h.ID)
enc.StringKey("channel_id", h.ChannelID)
enc.StringKey("team_id", h.TeamId)
enc.StringKey("display", h.DisplayName)
enc.StringKey("desc", h.Description)
}
func (h auditIncomingWebhook) IsNil() bool {
return false
}
type auditOutgoingWebhook struct {
ID string
ChannelID string
TeamID string
TriggerWords StringArray
TriggerWhen int
DisplayName string
Description string
ContentType string
Username string
}
// newAuditOutgoingWebhook creates a simplified representation of OutgoingWebhook for output to audit log.
func newAuditOutgoingWebhook(h *OutgoingWebhook) auditOutgoingWebhook {
var hook auditOutgoingWebhook
if h != nil {
hook.ID = h.Id
hook.ChannelID = h.ChannelId
hook.TeamID = h.TeamId
hook.TriggerWords = h.TriggerWords
hook.TriggerWhen = h.TriggerWhen
hook.DisplayName = h.DisplayName
hook.Description = h.Description
hook.ContentType = h.ContentType
hook.Username = h.Username
}
return hook
}
func (h auditOutgoingWebhook) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("id", h.ID)
enc.StringKey("channel_id", h.ChannelID)
enc.StringKey("team_id", h.TeamID)
enc.SliceStringKey("trigger_words", h.TriggerWords)
enc.IntKey("trigger_when", h.TriggerWhen)
enc.StringKey("display", h.DisplayName)
enc.StringKey("desc", h.Description)
enc.StringKey("content_type", h.ContentType)
enc.StringKey("username", h.Username)
}
func (h auditOutgoingWebhook) IsNil() bool {
return false
}
type auditRemoteCluster struct {
RemoteId string
RemoteTeamId string
Name string
DisplayName string
SiteURL string
CreateAt int64
LastPingAt int64
CreatorId string
}
// newRemoteCluster creates a simplified representation of RemoteCluster for output to audit log.
func newRemoteCluster(r *RemoteCluster) auditRemoteCluster {
var rc auditRemoteCluster
if r != nil {
rc.RemoteId = r.RemoteId
rc.RemoteTeamId = r.RemoteTeamId
rc.Name = r.Name
rc.DisplayName = r.DisplayName
rc.SiteURL = r.SiteURL
rc.CreateAt = r.CreateAt
rc.LastPingAt = r.LastPingAt
rc.CreatorId = r.CreatorId
}
return rc
}
func (r auditRemoteCluster) MarshalJSONObject(enc *gojay.Encoder) {
enc.StringKey("remote_id", r.RemoteId)
enc.StringKey("remote_team_id", r.RemoteTeamId)
enc.StringKey("name", r.Name)
enc.StringKey("display_name", r.DisplayName)
enc.StringKey("site_url", r.SiteURL)
enc.Int64Key("create_at", r.CreateAt)
enc.Int64Key("last_ping_at", r.LastPingAt)
enc.StringKey("creator_id", r.CreatorId)
}
func (r auditRemoteCluster) IsNil() bool {
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type Audits []Audit
func (o Audits) Etag() string {
if len(o) > 0 {
// the first in the list is always the most current
return Etag(o[0].CreateAt)
}
return ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
const (
AuthCodeExpireTime = 60 * 10 // 10 minutes
AuthCodeResponseType = "code"
ImplicitResponseType = "token"
DefaultScope = "user"
)
type AuthData struct {
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Code string `json:"code"`
ExpiresIn int32 `json:"expires_in"`
CreateAt int64 `json:"create_at"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Scope string `json:"scope"`
}
type AuthorizeRequest struct {
ResponseType string `json:"response_type"`
ClientId string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
State string `json:"state"`
}
// IsValid validates the AuthData and returns an error if it isn't configured
// correctly.
func (ad *AuthData) IsValid() *AppError {
if !IsValidId(ad.ClientId) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(ad.UserId) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if ad.Code == "" || len(ad.Code) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.auth_code.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if ad.ExpiresIn == 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.expires.app_error", nil, "", http.StatusBadRequest)
}
if ad.CreateAt <= 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.RedirectUri) > 256 || !IsValidHTTPURL(ad.RedirectUri) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.State) > 1024 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.Scope) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
return nil
}
// IsValid validates the AuthorizeRequest and returns an error if it isn't configured
// correctly.
func (ar *AuthorizeRequest) IsValid() *AppError {
if !IsValidId(ar.ClientId) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if ar.ResponseType == "" {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.response_type.app_error", nil, "", http.StatusBadRequest)
}
if ar.RedirectURI == "" || len(ar.RedirectURI) > 256 || !IsValidHTTPURL(ar.RedirectURI) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.State) > 1024 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.Scope) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
return nil
}
func (ad *AuthData) PreSave() {
if ad.ExpiresIn == 0 {
ad.ExpiresIn = AuthCodeExpireTime
}
if ad.CreateAt == 0 {
ad.CreateAt = GetMillis()
}
if ad.Scope == "" {
ad.Scope = DefaultScope
}
}
func (ad *AuthData) IsExpired() bool {
return GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"strings"
"unicode/utf8"
)
const (
BotDisplayNameMaxRunes = UserFirstNameMaxRunes
BotDescriptionMaxRunes = 1024
BotCreatorIdMaxRunes = KeyValuePluginIdMaxRunes // UserId or PluginId
BotWarnMetricBotUsername = "mattermost-advisor"
BotSystemBotUsername = "system-bot"
)
// Bot is a special type of User meant for programmatic interactions.
// Note that the primary key of a bot is the UserId, and matches the primary key of the
// corresponding user.
type Bot struct {
UserId string `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name,omitempty"`
Description string `json:"description,omitempty"`
OwnerId string `json:"owner_id"`
LastIconUpdate int64 `json:"last_icon_update,omitempty"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
func (b *Bot) Auditable() map[string]interface{} {
return map[string]interface{}{
"user_id": b.UserId,
"username": b.Username,
"display_name": b.DisplayName,
"description": b.Description,
"owner_id": b.OwnerId,
"last_icon_update": b.LastIconUpdate,
"create_at": b.CreateAt,
"update_at": b.UpdateAt,
"delete_at": b.DeleteAt,
}
}
// BotPatch is a description of what fields to update on an existing bot.
type BotPatch struct {
Username *string `json:"username"`
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
}
func (b *BotPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"username": b.Username,
"display_name": b.DisplayName,
"description": b.Description,
}
}
// BotGetOptions acts as a filter on bulk bot fetching queries.
type BotGetOptions struct {
OwnerId string
IncludeDeleted bool
OnlyOrphaned bool
Page int
PerPage int
}
// BotList is a list of bots.
type BotList []*Bot
// Trace describes the minimum information required to identify a bot for the purpose of logging.
func (b *Bot) Trace() map[string]any {
return map[string]any{"user_id": b.UserId}
}
// Clone returns a shallow copy of the bot.
func (b *Bot) Clone() *Bot {
copy := *b
return ©
}
// IsValidCreate validates bot for Create call. This skips validations of fields that are auto-filled on Create
func (b *Bot) IsValidCreate() *AppError {
if !IsValidUsername(b.Username) {
return NewAppError("Bot.IsValid", "model.bot.is_valid.username.app_error", b.Trace(), "", http.StatusBadRequest)
}
if utf8.RuneCountInString(b.DisplayName) > BotDisplayNameMaxRunes {
return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest)
}
if utf8.RuneCountInString(b.Description) > BotDescriptionMaxRunes {
return NewAppError("Bot.IsValid", "model.bot.is_valid.description.app_error", b.Trace(), "", http.StatusBadRequest)
}
if b.OwnerId == "" || utf8.RuneCountInString(b.OwnerId) > BotCreatorIdMaxRunes {
return NewAppError("Bot.IsValid", "model.bot.is_valid.creator_id.app_error", b.Trace(), "", http.StatusBadRequest)
}
return nil
}
// IsValid validates the bot and returns an error if it isn't configured correctly.
func (b *Bot) IsValid() *AppError {
if !IsValidId(b.UserId) {
return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest)
}
if b.CreateAt == 0 {
return NewAppError("Bot.IsValid", "model.bot.is_valid.create_at.app_error", b.Trace(), "", http.StatusBadRequest)
}
if b.UpdateAt == 0 {
return NewAppError("Bot.IsValid", "model.bot.is_valid.update_at.app_error", b.Trace(), "", http.StatusBadRequest)
}
return b.IsValidCreate()
}
// PreSave should be run before saving a new bot to the database.
func (b *Bot) PreSave() {
b.CreateAt = GetMillis()
b.UpdateAt = b.CreateAt
b.DeleteAt = 0
}
// PreUpdate should be run before saving an updated bot to the database.
func (b *Bot) PreUpdate() {
b.UpdateAt = GetMillis()
}
// Etag generates an etag for caching.
func (b *Bot) Etag() string {
return Etag(b.UserId, b.UpdateAt)
}
// Patch modifies an existing bot with optional fields from the given patch.
// TODO 6.0: consider returning a boolean to indicate whether or not the patch
// applied any changes.
func (b *Bot) Patch(patch *BotPatch) {
if patch.Username != nil {
b.Username = *patch.Username
}
if patch.DisplayName != nil {
b.DisplayName = *patch.DisplayName
}
if patch.Description != nil {
b.Description = *patch.Description
}
}
// WouldPatch returns whether or not the given patch would be applied or not.
func (b *Bot) WouldPatch(patch *BotPatch) bool {
if patch == nil {
return false
}
if patch.Username != nil && *patch.Username != b.Username {
return true
}
if patch.DisplayName != nil && *patch.DisplayName != b.DisplayName {
return true
}
if patch.Description != nil && *patch.Description != b.Description {
return true
}
return false
}
// UserFromBot returns a user model describing the bot fields stored in the User store.
func UserFromBot(b *Bot) *User {
return &User{
Id: b.UserId,
Username: b.Username,
Email: NormalizeEmail(fmt.Sprintf("%s@localhost", b.Username)),
FirstName: b.DisplayName,
Roles: SystemUserRoleId,
}
}
// BotFromUser returns a bot model given a user model
func BotFromUser(u *User) *Bot {
return &Bot{
OwnerId: u.Id,
UserId: u.Id,
Username: u.Username,
DisplayName: u.GetDisplayName(ShowUsername),
}
}
// Etag computes the etag for a list of bots.
func (l *BotList) Etag() string {
id := "0"
var t int64 = 0
var delta int64 = 0
for _, v := range *l {
if v.UpdateAt > t {
t = v.UpdateAt
id = v.UserId
}
}
return Etag(id, t, delta, len(*l))
}
// MakeBotNotFoundError creates the error returned when a bot does not exist, or when the user isn't allowed to query the bot.
// The errors must the same in both cases to avoid leaking that a user is a bot.
func MakeBotNotFoundError(userId string) *AppError {
return NewAppError("SqlBotStore.Get", "store.sql_bot.get.missing.app_error", map[string]any{"user_id": userId}, "", http.StatusNotFound)
}
func IsBotDMChannel(channel *Channel, botUserID string) bool {
if channel.Type != ChannelTypeDirect {
return false
}
if !strings.HasPrefix(channel.Name, botUserID+"__") && !strings.HasSuffix(channel.Name, "__"+botUserID) {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
func NewBool(b bool) *bool { return &b }
func NewInt(n int) *int { return &n }
func NewInt64(n int64) *int64 { return &n }
func NewString(s string) *string { return &s }
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type BundleInfo struct {
Path string
Manifest *Manifest
ManifestPath string
ManifestError error
}
func (b *BundleInfo) WrapLogger(logger *mlog.Logger) *mlog.Logger {
if b.Manifest != nil {
return logger.With(mlog.String("plugin_id", b.Manifest.Id))
}
return logger.With(mlog.String("plugin_path", b.Path))
}
// Returns bundle info for the given path. The return value is never nil.
func BundleInfoForPath(path string) *BundleInfo {
m, mpath, err := FindManifest(path)
return &BundleInfo{
Path: path,
Manifest: m,
ManifestPath: mpath,
ManifestError: err,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"regexp"
"sort"
"strings"
"unicode/utf8"
)
type ChannelType string
const (
ChannelTypeOpen ChannelType = "O"
ChannelTypePrivate ChannelType = "P"
ChannelTypeDirect ChannelType = "D"
ChannelTypeGroup ChannelType = "G"
ChannelGroupMaxUsers = 8
ChannelGroupMinUsers = 3
DefaultChannelName = "town-square"
ChannelDisplayNameMaxRunes = 64
ChannelNameMinLength = 1
ChannelNameMaxLength = 64
ChannelHeaderMaxRunes = 1024
ChannelPurposeMaxRunes = 250
ChannelCacheSize = 25000
ChannelSortByUsername = "username"
ChannelSortByStatus = "status"
)
type Channel struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type ChannelType `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
SchemeId *string `json:"scheme_id"`
Props map[string]any `json:"props"`
GroupConstrained *bool `json:"group_constrained"`
Shared *bool `json:"shared"`
TotalMsgCountRoot int64 `json:"total_msg_count_root"`
PolicyID *string `json:"policy_id"`
LastRootPostAt int64 `json:"last_root_post_at"`
}
func (o *Channel) Auditable() map[string]interface{} {
return map[string]interface{}{
"create_at": o.CreateAt,
"creator_id": o.CreatorId,
"delete_at": o.DeleteAt,
"extra_group_at": o.ExtraUpdateAt,
"group_constrained": o.GroupConstrained,
"id": o.Id,
"last_post_at": o.LastPostAt,
"last_root_post_at": o.LastRootPostAt,
"policy_id": o.PolicyID,
"props": o.Props,
"scheme_id": o.SchemeId,
"shared": o.Shared,
"team_id": o.TeamId,
"total_msg_count_root": o.TotalMsgCountRoot,
"type": o.Type,
"update_at": o.UpdateAt,
}
}
type ChannelWithTeamData struct {
Channel
TeamDisplayName string `json:"team_display_name"`
TeamName string `json:"team_name"`
TeamUpdateAt int64 `json:"team_update_at"`
}
type ChannelsWithCount struct {
Channels ChannelListWithTeamData `json:"channels"`
TotalCount int64 `json:"total_count"`
}
type ChannelPatch struct {
DisplayName *string `json:"display_name"`
Name *string `json:"name"`
Header *string `json:"header"`
Purpose *string `json:"purpose"`
GroupConstrained *bool `json:"group_constrained"`
}
func (c *ChannelPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"header": c.Header,
"group_constrained": c.GroupConstrained,
"purpose": c.Purpose,
}
}
type ChannelForExport struct {
Channel
TeamName string
SchemeName *string
}
type DirectChannelForExport struct {
Channel
Members *[]string
}
type ChannelModeration struct {
Name string `json:"name"`
Roles *ChannelModeratedRoles `json:"roles"`
}
type ChannelModeratedRoles struct {
Guests *ChannelModeratedRole `json:"guests"`
Members *ChannelModeratedRole `json:"members"`
}
type ChannelModeratedRole struct {
Value bool `json:"value"`
Enabled bool `json:"enabled"`
}
type ChannelModerationPatch struct {
Name *string `json:"name"`
Roles *ChannelModeratedRolesPatch `json:"roles"`
}
func (c *ChannelModerationPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"name": c.Name,
"roles": c.Roles,
}
}
type ChannelModeratedRolesPatch struct {
Guests *bool `json:"guests"`
Members *bool `json:"members"`
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.
// ExcludeDefaultChannels will exclude the configured default channels (ex 'town-square' and 'off-topic').
// IncludeDeleted will include channel records where DeleteAt != 0.
// ExcludeChannelNames will exclude channels from the results by name.
// IncludeSearchById will include searching matches against channel IDs in the results
// Paginate whether to paginate the results.
// Page page requested, if results are paginated.
// PerPage number of results per page, if paginated.
type ChannelSearchOpts struct {
NotAssociatedToGroup string
ExcludeDefaultChannels bool
IncludeDeleted bool // If true, deleted channels will be included in the results.
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeSearchById bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int // When combined with IncludeDeleted, only channels deleted after this time will be returned.
LastUpdateAt int
}
type ChannelMemberCountByGroup struct {
GroupId string `json:"group_id"`
ChannelMemberCount int64 `json:"channel_member_count"`
ChannelMemberTimezonesCount int64 `json:"channel_member_timezones_count"`
}
type ChannelOption func(channel *Channel)
var gmNameRegex = regexp.MustCompile("^[a-f0-9]{40}$")
func WithID(ID string) ChannelOption {
return func(channel *Channel) {
channel.Id = ID
}
}
// The following are some GraphQL methods necessary to return the
// data in float64 type. The spec doesn't support 64 bit integers,
// so we have to pass the data in float64. The _ at the end is
// a hack to keep the attribute name same in GraphQL schema.
func (o *Channel) CreateAt_() float64 {
return float64(o.CreateAt)
}
func (o *Channel) UpdateAt_() float64 {
return float64(o.UpdateAt)
}
func (o *Channel) DeleteAt_() float64 {
return float64(o.DeleteAt)
}
func (o *Channel) LastPostAt_() float64 {
return float64(o.LastPostAt)
}
func (o *Channel) TotalMsgCount_() float64 {
return float64(o.TotalMsgCount)
}
func (o *Channel) TotalMsgCountRoot_() float64 {
return float64(o.TotalMsgCountRoot)
}
func (o *Channel) LastRootPostAt_() float64 {
return float64(o.LastRootPostAt)
}
func (o *Channel) ExtraUpdateAt_() float64 {
return float64(o.ExtraUpdateAt)
}
func (o *Channel) Props_() StringInterface {
return StringInterface(o.Props)
}
func (o *Channel) DeepCopy() *Channel {
copy := *o
if copy.SchemeId != nil {
copy.SchemeId = NewString(*o.SchemeId)
}
return ©
}
func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Channel) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.DisplayName) > ChannelDisplayNameMaxRunes {
return NewAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidChannelIdentifier(o.Name) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.1_or_more.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !(o.Type == ChannelTypeOpen || o.Type == ChannelTypePrivate || o.Type == ChannelTypeDirect || o.Type == ChannelTypeGroup) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Header) > ChannelHeaderMaxRunes {
return NewAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Purpose) > ChannelPurposeMaxRunes {
return NewAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.CreatorId) > 26 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "", http.StatusBadRequest)
}
if o.Type != ChannelTypeDirect && o.Type != ChannelTypeGroup {
userIds := strings.Split(o.Name, "__")
if ok := gmNameRegex.MatchString(o.Name); ok || (o.Type != ChannelTypeDirect && len(userIds) == 2 && IsValidId(userIds[0]) && IsValidId(userIds[1])) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.name.app_error", nil, "", http.StatusBadRequest)
}
}
return nil
}
func (o *Channel) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.Name = SanitizeUnicode(o.Name)
o.DisplayName = SanitizeUnicode(o.DisplayName)
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt
o.ExtraUpdateAt = 0
}
func (o *Channel) PreUpdate() {
o.UpdateAt = GetMillis()
o.Name = SanitizeUnicode(o.Name)
o.DisplayName = SanitizeUnicode(o.DisplayName)
}
func (o *Channel) IsGroupOrDirect() bool {
return o.Type == ChannelTypeDirect || o.Type == ChannelTypeGroup
}
func (o *Channel) IsOpen() bool {
return o.Type == ChannelTypeOpen
}
func (o *Channel) Patch(patch *ChannelPatch) {
if patch.DisplayName != nil {
o.DisplayName = *patch.DisplayName
}
if patch.Name != nil {
o.Name = *patch.Name
}
if patch.Header != nil {
o.Header = *patch.Header
}
if patch.Purpose != nil {
o.Purpose = *patch.Purpose
}
if patch.GroupConstrained != nil {
o.GroupConstrained = patch.GroupConstrained
}
}
func (o *Channel) MakeNonNil() {
if o.Props == nil {
o.Props = make(map[string]any)
}
}
func (o *Channel) AddProp(key string, value any) {
o.MakeNonNil()
o.Props[key] = value
}
func (o *Channel) IsGroupConstrained() bool {
return o.GroupConstrained != nil && *o.GroupConstrained
}
func (o *Channel) IsShared() bool {
return o.Shared != nil && *o.Shared
}
func (o *Channel) GetOtherUserIdForDM(userId string) string {
if o.Type != ChannelTypeDirect {
return ""
}
userIds := strings.Split(o.Name, "__")
var otherUserId string
if userIds[0] != userIds[1] {
if userIds[0] == userId {
otherUserId = userIds[1]
} else {
otherUserId = userIds[0]
}
}
return otherUserId
}
func (ChannelType) ImplementsGraphQLType(name string) bool {
return name == "ChannelType"
}
func (t ChannelType) MarshalJSON() ([]byte, error) {
return json.Marshal(string(t))
}
func (t *ChannelType) UnmarshalGraphQL(input any) error {
chType, ok := input.(string)
if !ok {
return errors.New("wrong type")
}
*t = ChannelType(chType)
return nil
}
func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 {
return userId2 + "__" + userId1
}
return userId1 + "__" + userId2
}
func GetGroupDisplayNameFromUsers(users []*User, truncate bool) string {
usernames := make([]string, len(users))
for index, user := range users {
usernames[index] = user.Username
}
sort.Strings(usernames)
name := strings.Join(usernames, ", ")
if truncate && len(name) > ChannelNameMaxLength {
name = name[:ChannelNameMaxLength]
}
return name
}
func GetGroupNameFromUserIds(userIds []string) string {
sort.Strings(userIds)
h := sha1.New()
for _, id := range userIds {
io.WriteString(h, id)
}
return hex.EncodeToString(h.Sum(nil))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/md5"
"fmt"
"sort"
"strconv"
)
type ChannelCounts struct {
Counts map[string]int64 `json:"counts"`
CountsRoot map[string]int64 `json:"counts_root"`
UpdateTimes map[string]int64 `json:"update_times"`
}
func (o *ChannelCounts) Etag() string {
// we don't include CountsRoot in ETag calculation, since it's a derivative
ids := []string{}
for id := range o.Counts {
ids = append(ids, id)
}
sort.Strings(ids)
str := ""
for _, id := range ids {
str += id + strconv.FormatInt(o.Counts[id], 10)
}
md5Counts := fmt.Sprintf("%x", md5.Sum([]byte(str)))
var update int64 = 0
for _, u := range o.UpdateTimes {
if u > update {
update = u
}
}
return Etag(md5Counts, update)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type ChannelData struct {
Channel *Channel `json:"channel"`
Member *ChannelMember `json:"member"`
}
func (o *ChannelData) Etag() string {
var mt int64 = 0
if o.Member != nil {
mt = o.Member.LastUpdateAt
}
return Etag(o.Channel.Id, o.Channel.UpdateAt, o.Channel.LastPostAt, mt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type ChannelList []*Channel
func (o *ChannelList) Etag() string {
id := "0"
var t int64 = 0
var delta int64 = 0
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
}
if v.UpdateAt > t {
t = v.UpdateAt
id = v.Id
}
}
return Etag(id, t, delta, len(*o))
}
type ChannelListWithTeamData []*ChannelWithTeamData
func (o *ChannelListWithTeamData) Etag() string {
id := "0"
var t int64 = 0
var delta int64 = 0
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
}
if v.UpdateAt > t {
t = v.UpdateAt
id = v.Id
}
if v.TeamUpdateAt > t {
t = v.TeamUpdateAt
id = v.Id
}
}
return Etag(id, t, delta, len(*o))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"strings"
)
const (
ChannelNotifyDefault = "default"
ChannelNotifyAll = "all"
ChannelNotifyMention = "mention"
ChannelNotifyNone = "none"
ChannelMarkUnreadAll = "all"
ChannelMarkUnreadMention = "mention"
IgnoreChannelMentionsDefault = "default"
IgnoreChannelMentionsOff = "off"
IgnoreChannelMentionsOn = "on"
IgnoreChannelMentionsNotifyProp = "ignore_channel_mentions"
)
type ChannelUnread struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
MentionCountRoot int64 `json:"mention_count_root"`
UrgentMentionCount int64 `json:"urgent_mention_count"`
MsgCountRoot int64 `json:"msg_count_root"`
NotifyProps StringMap `json:"-"`
}
type ChannelUnreadAt struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
MentionCountRoot int64 `json:"mention_count_root"`
UrgentMentionCount int64 `json:"urgent_mention_count"`
MsgCountRoot int64 `json:"msg_count_root"`
LastViewedAt int64 `json:"last_viewed_at"`
NotifyProps StringMap `json:"-"`
}
type ChannelMember struct {
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
LastViewedAt int64 `json:"last_viewed_at"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
MentionCountRoot int64 `json:"mention_count_root"`
UrgentMentionCount int64 `json:"urgent_mention_count"`
MsgCountRoot int64 `json:"msg_count_root"`
NotifyProps StringMap `json:"notify_props"`
LastUpdateAt int64 `json:"last_update_at"`
SchemeGuest bool `json:"scheme_guest"`
SchemeUser bool `json:"scheme_user"`
SchemeAdmin bool `json:"scheme_admin"`
ExplicitRoles string `json:"explicit_roles"`
}
func (o *ChannelMember) Auditable() map[string]interface{} {
return map[string]interface{}{
"channel_id": o.ChannelId,
"user_id": o.UserId,
"roles": o.Roles,
"last_viewed_at": o.LastViewedAt,
"msg_count": o.MsgCount,
"mention_count": o.MentionCount,
"mention_count_root": o.MentionCountRoot,
"urgent_mention_count": o.UrgentMentionCount,
"msg_count_root": o.MsgCountRoot,
"notify_props": o.NotifyProps,
"last_update_at": o.LastUpdateAt,
"scheme_guest": o.SchemeGuest,
"scheme_user": o.SchemeUser,
"scheme_admin": o.SchemeAdmin,
"explicit_roles": o.ExplicitRoles,
}
}
// The following are some GraphQL methods necessary to return the
// data in float64 type. The spec doesn't support 64 bit integers,
// so we have to pass the data in float64. The _ at the end is
// a hack to keep the attribute name same in GraphQL schema.
func (o *ChannelMember) LastViewedAt_() float64 {
return float64(o.LastViewedAt)
}
func (o *ChannelMember) MsgCount_() float64 {
return float64(o.MsgCount)
}
func (o *ChannelMember) MentionCount_() float64 {
return float64(o.MentionCount)
}
func (o *ChannelMember) MentionCountRoot_() float64 {
return float64(o.MentionCountRoot)
}
func (o *ChannelMember) UrgentMentionCount_() float64 {
return float64(o.UrgentMentionCount)
}
func (o *ChannelMember) MsgCountRoot_() float64 {
return float64(o.MsgCountRoot)
}
func (o *ChannelMember) LastUpdateAt_() float64 {
return float64(o.LastUpdateAt)
}
// ChannelMemberWithTeamData contains ChannelMember appended with extra team information
// as well.
type ChannelMemberWithTeamData struct {
ChannelMember
TeamDisplayName string `json:"team_display_name"`
TeamName string `json:"team_name"`
TeamUpdateAt int64 `json:"team_update_at"`
}
type ChannelMembers []ChannelMember
type ChannelMembersWithTeamData []ChannelMemberWithTeamData
type ChannelMemberForExport struct {
ChannelMember
ChannelName string
Username string
}
func (o *ChannelMember) IsValid() *AppError {
if !IsValidId(o.ChannelId) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
notifyLevel := o.NotifyProps[DesktopNotifyProp]
if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error", nil, "notify_level="+notifyLevel, http.StatusBadRequest)
}
markUnreadLevel := o.NotifyProps[MarkUnreadNotifyProp]
if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.unread_level.app_error", nil, "mark_unread_level="+markUnreadLevel, http.StatusBadRequest)
}
if pushLevel, ok := o.NotifyProps[PushNotifyProp]; ok {
if len(pushLevel) > 20 || !IsChannelNotifyLevelValid(pushLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.push_level.app_error", nil, "push_notification_level="+pushLevel, http.StatusBadRequest)
}
}
if sendEmail, ok := o.NotifyProps[EmailNotifyProp]; ok {
if len(sendEmail) > 20 || !IsSendEmailValid(sendEmail) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.email_value.app_error", nil, "push_notification_level="+sendEmail, http.StatusBadRequest)
}
}
if ignoreChannelMentions, ok := o.NotifyProps[IgnoreChannelMentionsNotifyProp]; ok {
if len(ignoreChannelMentions) > 40 || !IsIgnoreChannelMentionsValid(ignoreChannelMentions) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", nil, "ignore_channel_mentions="+ignoreChannelMentions, http.StatusBadRequest)
}
}
if len(o.Roles) > UserRolesMaxLength {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.roles_limit.app_error",
map[string]any{"Limit": UserRolesMaxLength}, "", http.StatusBadRequest)
}
return nil
}
func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) PreUpdate() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
func (o *ChannelMember) SetChannelMuted(muted bool) {
if o.IsChannelMuted() {
o.NotifyProps[MarkUnreadNotifyProp] = ChannelMarkUnreadAll
} else {
o.NotifyProps[MarkUnreadNotifyProp] = ChannelMarkUnreadMention
}
}
func (o *ChannelMember) IsChannelMuted() bool {
return o.NotifyProps[MarkUnreadNotifyProp] == ChannelMarkUnreadMention
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == ChannelNotifyDefault ||
notifyLevel == ChannelNotifyAll ||
notifyLevel == ChannelNotifyMention ||
notifyLevel == ChannelNotifyNone
}
func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool {
return markUnreadLevel == ChannelMarkUnreadAll || markUnreadLevel == ChannelMarkUnreadMention
}
func IsSendEmailValid(sendEmail string) bool {
return sendEmail == ChannelNotifyDefault || sendEmail == "true" || sendEmail == "false"
}
func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool {
return ignoreChannelMentions == IgnoreChannelMentionsOn || ignoreChannelMentions == IgnoreChannelMentionsOff || ignoreChannelMentions == IgnoreChannelMentionsDefault
}
func GetDefaultChannelNotifyProps() StringMap {
return StringMap{
DesktopNotifyProp: ChannelNotifyDefault,
MarkUnreadNotifyProp: ChannelMarkUnreadAll,
PushNotifyProp: ChannelNotifyDefault,
EmailNotifyProp: ChannelNotifyDefault,
IgnoreChannelMentionsNotifyProp: IgnoreChannelMentionsDefault,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"regexp"
"strings"
)
var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`)
func ChannelMentions(message string) []string {
var names []string
if strings.Contains(message, "~") {
alreadyMentioned := make(map[string]bool)
for _, match := range channelMentionRegexp.FindAllString(message, -1) {
name := match[1:]
if !alreadyMentioned[name] {
names = append(names, name)
alreadyMentioned[name] = true
}
}
}
return names
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"regexp"
)
type SidebarCategoryType string
type SidebarCategorySorting string
const (
// Each sidebar category has a 'type'. System categories are Channels, Favorites and DMs
// All user-created categories will have type Custom
SidebarCategoryChannels SidebarCategoryType = "channels"
SidebarCategoryDirectMessages SidebarCategoryType = "direct_messages"
SidebarCategoryFavorites SidebarCategoryType = "favorites"
SidebarCategoryCustom SidebarCategoryType = "custom"
// Increment to use when adding/reordering things in the sidebar
MinimalSidebarSortDistance = 10
// Default Sort Orders for categories
DefaultSidebarSortOrderFavorites = 0
DefaultSidebarSortOrderChannels = DefaultSidebarSortOrderFavorites + MinimalSidebarSortDistance
DefaultSidebarSortOrderDMs = DefaultSidebarSortOrderChannels + MinimalSidebarSortDistance
// Sorting modes
// default for all categories except DMs (behaves like manual)
SidebarCategorySortDefault SidebarCategorySorting = ""
// sort manually
SidebarCategorySortManual SidebarCategorySorting = "manual"
// sort by recency (default for DMs)
SidebarCategorySortRecent SidebarCategorySorting = "recent"
// sort by display name alphabetically
SidebarCategorySortAlphabetical SidebarCategorySorting = "alpha"
)
// SidebarCategory represents the corresponding DB table
type SidebarCategory struct {
Id string `json:"id"`
UserId string `json:"user_id"`
TeamId string `json:"team_id"`
SortOrder int64 `json:"sort_order"`
Sorting SidebarCategorySorting `json:"sorting"`
Type SidebarCategoryType `json:"type"`
DisplayName string `json:"display_name"`
Muted bool `json:"muted"`
Collapsed bool `json:"collapsed"`
}
// SidebarCategoryWithChannels combines data from SidebarCategory table with the Channel IDs that belong to that category
type SidebarCategoryWithChannels struct {
SidebarCategory
Channels []string `json:"channel_ids"`
}
func (sc SidebarCategoryWithChannels) ChannelIds() []string {
return sc.Channels
}
type SidebarCategoryOrder []string
// OrderedSidebarCategories combines categories, their channel IDs and an array of Category IDs, sorted
type OrderedSidebarCategories struct {
Categories SidebarCategoriesWithChannels `json:"categories"`
Order SidebarCategoryOrder `json:"order"`
}
type SidebarChannel struct {
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
CategoryId string `json:"category_id"`
SortOrder int64 `json:"-"`
}
type SidebarChannels []*SidebarChannel
type SidebarCategoriesWithChannels []*SidebarCategoryWithChannels
var categoryIdPattern = regexp.MustCompile("(favorites|channels|direct_messages)_[a-z0-9]{26}_[a-z0-9]{26}")
func IsValidCategoryId(s string) bool {
// Category IDs can either be regular IDs
if IsValidId(s) {
return true
}
// Or default categories can follow the pattern {type}_{userID}_{teamID}
return categoryIdPattern.MatchString(s)
}
func (SidebarCategoryType) ImplementsGraphQLType(name string) bool {
return name == "SidebarCategoryType"
}
func (t SidebarCategoryType) MarshalJSON() ([]byte, error) {
return json.Marshal(string(t))
}
func (t *SidebarCategoryType) UnmarshalGraphQL(input any) error {
chType, ok := input.(string)
if !ok {
return errors.New("wrong type")
}
*t = SidebarCategoryType(chType)
return nil
}
func (SidebarCategorySorting) ImplementsGraphQLType(name string) bool {
return name == "SidebarCategorySorting"
}
func (t SidebarCategorySorting) MarshalJSON() ([]byte, error) {
return json.Marshal(string(t))
}
func (t *SidebarCategorySorting) UnmarshalGraphQL(input any) error {
chType, ok := input.(string)
if !ok {
return errors.New("wrong type")
}
*t = SidebarCategorySorting(chType)
return nil
}
func (t *SidebarCategory) SortOrder_() float64 {
return float64(t.SortOrder)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type ChannelStats struct {
ChannelId string `json:"channel_id"`
MemberCount int64 `json:"member_count"`
GuestCount int64 `json:"guest_count"`
PinnedPostCount int64 `json:"pinnedpost_count"`
FilesCount int64 `json:"files_count"`
}
func (o *ChannelStats) MemberCount_() float64 {
return float64(o.MemberCount)
}
func (o *ChannelStats) GuestCount_() float64 {
return float64(o.GuestCount)
}
func (o *ChannelStats) PinnedPostCount_() float64 {
return float64(o.PinnedPostCount)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"strconv"
"strings"
)
const (
HeaderRequestId = "X-Request-ID"
HeaderVersionId = "X-Version-ID"
HeaderClusterId = "X-Cluster-ID"
HeaderEtagServer = "ETag"
HeaderEtagClient = "If-None-Match"
HeaderForwarded = "X-Forwarded-For"
HeaderRealIP = "X-Real-IP"
HeaderForwardedProto = "X-Forwarded-Proto"
HeaderToken = "token"
HeaderCsrfToken = "X-CSRF-Token"
HeaderBearer = "BEARER"
HeaderAuth = "Authorization"
HeaderCloudToken = "X-Cloud-Token"
HeaderRemoteclusterToken = "X-RemoteCluster-Token"
HeaderRemoteclusterId = "X-RemoteCluster-Id"
HeaderRequestedWith = "X-Requested-With"
HeaderRequestedWithXML = "XMLHttpRequest"
HeaderFirstInaccessiblePostTime = "First-Inaccessible-Post-Time"
HeaderFirstInaccessibleFileTime = "First-Inaccessible-File-Time"
HeaderRange = "Range"
STATUS = "status"
StatusOk = "OK"
StatusFail = "FAIL"
StatusUnhealthy = "UNHEALTHY"
StatusRemove = "REMOVE"
ConnectionId = "Connection-Id"
ClientDir = "client"
APIURLSuffixV1 = "/api/v1"
APIURLSuffixV4 = "/api/v4"
APIURLSuffixV5 = "/api/v5"
APIURLSuffix = APIURLSuffixV4
)
type Response struct {
StatusCode int
RequestId string
Etag string
ServerVersion string
Header http.Header
}
type Client4 struct {
URL string // The location of the server, for example "http://localhost:8065"
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
HTTPClient *http.Client // The http client
AuthToken string
AuthType string
HTTPHeader map[string]string // Headers to be copied over for each request
// TrueString is the string value sent to the server for true boolean query parameters.
trueString string
// FalseString is the string value sent to the server for false boolean query parameters.
falseString string
}
// SetBoolString is a helper method for overriding how true and false query string parameters are
// sent to the server.
//
// This method is only exposed for testing. It is never necessary to configure these values
// in production.
func (c *Client4) SetBoolString(value bool, valueStr string) {
if value {
c.trueString = valueStr
} else {
c.falseString = valueStr
}
}
// boolString builds the query string parameter for boolean values.
func (c *Client4) boolString(value bool) string {
if value && c.trueString != "" {
return c.trueString
} else if !value && c.falseString != "" {
return c.falseString
}
if value {
return "true"
}
return "false"
}
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
func NewAPIv4Client(url string) *Client4 {
url = strings.TrimRight(url, "/")
return &Client4{url, url + APIURLSuffix, &http.Client{}, "", "", map[string]string{}, "", ""}
}
func NewAPIv4SocketClient(socketPath string) *Client4 {
tr := &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
}
client := NewAPIv4Client("http://_")
client.HTTPClient = &http.Client{Transport: tr}
return client
}
func BuildResponse(r *http.Response) *Response {
if r == nil {
return nil
}
return &Response{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HeaderRequestId),
Etag: r.Header.Get(HeaderEtagServer),
ServerVersion: r.Header.Get(HeaderVersionId),
Header: r.Header,
}
}
func (c *Client4) SetToken(token string) {
c.AuthToken = token
c.AuthType = HeaderBearer
}
// MockSession is deprecated in favour of SetToken
func (c *Client4) MockSession(token string) {
c.SetToken(token)
}
func (c *Client4) SetOAuthToken(token string) {
c.AuthToken = token
c.AuthType = HeaderToken
}
func (c *Client4) ClearOAuthToken() {
c.AuthToken = ""
c.AuthType = HeaderBearer
}
func (c *Client4) usersRoute() string {
return "/users"
}
func (c *Client4) userRoute(userId string) string {
return fmt.Sprintf(c.usersRoute()+"/%v", userId)
}
func (c *Client4) userThreadsRoute(userID, teamID string) string {
return c.userRoute(userID) + c.teamRoute(teamID) + "/threads"
}
func (c *Client4) userThreadRoute(userId, teamId, threadId string) string {
return c.userThreadsRoute(userId, teamId) + "/" + threadId
}
func (c *Client4) userCategoryRoute(userID, teamID string) string {
return c.userRoute(userID) + c.teamRoute(teamID) + "/channels/categories"
}
func (c *Client4) userAccessTokensRoute() string {
return fmt.Sprintf(c.usersRoute() + "/tokens")
}
func (c *Client4) userAccessTokenRoute(tokenId string) string {
return fmt.Sprintf(c.usersRoute()+"/tokens/%v", tokenId)
}
func (c *Client4) userByUsernameRoute(userName string) string {
return fmt.Sprintf(c.usersRoute()+"/username/%v", userName)
}
func (c *Client4) userByEmailRoute(email string) string {
return fmt.Sprintf(c.usersRoute()+"/email/%v", email)
}
func (c *Client4) botsRoute() string {
return "/bots"
}
func (c *Client4) botRoute(botUserId string) string {
return fmt.Sprintf("%s/%s", c.botsRoute(), botUserId)
}
func (c *Client4) teamsRoute() string {
return "/teams"
}
func (c *Client4) teamRoute(teamId string) string {
return fmt.Sprintf(c.teamsRoute()+"/%v", teamId)
}
func (c *Client4) teamAutoCompleteCommandsRoute(teamId string) string {
return fmt.Sprintf(c.teamsRoute()+"/%v/commands/autocomplete", teamId)
}
func (c *Client4) teamByNameRoute(teamName string) string {
return fmt.Sprintf(c.teamsRoute()+"/name/%v", teamName)
}
func (c *Client4) teamMemberRoute(teamId, userId string) string {
return fmt.Sprintf(c.teamRoute(teamId)+"/members/%v", userId)
}
func (c *Client4) teamMembersRoute(teamId string) string {
return fmt.Sprintf(c.teamRoute(teamId) + "/members")
}
func (c *Client4) teamStatsRoute(teamId string) string {
return fmt.Sprintf(c.teamRoute(teamId) + "/stats")
}
func (c *Client4) teamImportRoute(teamId string) string {
return fmt.Sprintf(c.teamRoute(teamId) + "/import")
}
func (c *Client4) channelsRoute() string {
return "/channels"
}
func (c *Client4) channelsForTeamRoute(teamId string) string {
return fmt.Sprintf(c.teamRoute(teamId) + "/channels")
}
func (c *Client4) channelRoute(channelId string) string {
return fmt.Sprintf(c.channelsRoute()+"/%v", channelId)
}
func (c *Client4) channelByNameRoute(channelName, teamId string) string {
return fmt.Sprintf(c.teamRoute(teamId)+"/channels/name/%v", channelName)
}
func (c *Client4) channelsForTeamForUserRoute(teamId, userId string, includeDeleted bool) string {
route := fmt.Sprintf(c.userRoute(userId) + c.teamRoute(teamId) + "/channels")
if includeDeleted {
query := fmt.Sprintf("?include_deleted=%v", includeDeleted)
return route + query
}
return route
}
func (c *Client4) channelByNameForTeamNameRoute(channelName, teamName string) string {
return fmt.Sprintf(c.teamByNameRoute(teamName)+"/channels/name/%v", channelName)
}
func (c *Client4) channelMembersRoute(channelId string) string {
return fmt.Sprintf(c.channelRoute(channelId) + "/members")
}
func (c *Client4) channelMemberRoute(channelId, userId string) string {
return fmt.Sprintf(c.channelMembersRoute(channelId)+"/%v", userId)
}
func (c *Client4) postsRoute() string {
return "/posts"
}
func (c *Client4) postsEphemeralRoute() string {
return "/posts/ephemeral"
}
func (c *Client4) configRoute() string {
return "/config"
}
func (c *Client4) licenseRoute() string {
return "/license"
}
func (c *Client4) postRoute(postId string) string {
return fmt.Sprintf(c.postsRoute()+"/%v", postId)
}
func (c *Client4) filesRoute() string {
return "/files"
}
func (c *Client4) fileRoute(fileId string) string {
return fmt.Sprintf(c.filesRoute()+"/%v", fileId)
}
func (c *Client4) uploadsRoute() string {
return "/uploads"
}
func (c *Client4) uploadRoute(uploadId string) string {
return fmt.Sprintf("%s/%s", c.uploadsRoute(), uploadId)
}
func (c *Client4) pluginsRoute() string {
return "/plugins"
}
func (c *Client4) pluginRoute(pluginId string) string {
return fmt.Sprintf(c.pluginsRoute()+"/%v", pluginId)
}
func (c *Client4) systemRoute() string {
return "/system"
}
func (c *Client4) cloudRoute() string {
return "/cloud"
}
func (c *Client4) hostedCustomerRoute() string {
return "/hosted_customer"
}
func (c *Client4) testEmailRoute() string {
return "/email/test"
}
func (c *Client4) usageRoute() string {
return "/usage"
}
func (c *Client4) testSiteURLRoute() string {
return "/site_url/test"
}
func (c *Client4) testS3Route() string {
return "/file/s3_test"
}
func (c *Client4) databaseRoute() string {
return "/database"
}
func (c *Client4) cacheRoute() string {
return "/caches"
}
func (c *Client4) clusterRoute() string {
return "/cluster"
}
func (c *Client4) incomingWebhooksRoute() string {
return "/hooks/incoming"
}
func (c *Client4) incomingWebhookRoute(hookID string) string {
return fmt.Sprintf(c.incomingWebhooksRoute()+"/%v", hookID)
}
func (c *Client4) complianceReportsRoute() string {
return "/compliance/reports"
}
func (c *Client4) complianceReportRoute(reportId string) string {
return fmt.Sprintf("%s/%s", c.complianceReportsRoute(), reportId)
}
func (c *Client4) complianceReportDownloadRoute(reportId string) string {
return fmt.Sprintf("%s/%s/download", c.complianceReportsRoute(), reportId)
}
func (c *Client4) outgoingWebhooksRoute() string {
return "/hooks/outgoing"
}
func (c *Client4) outgoingWebhookRoute(hookID string) string {
return fmt.Sprintf(c.outgoingWebhooksRoute()+"/%v", hookID)
}
func (c *Client4) preferencesRoute(userId string) string {
return fmt.Sprintf(c.userRoute(userId) + "/preferences")
}
func (c *Client4) userStatusRoute(userId string) string {
return fmt.Sprintf(c.userRoute(userId) + "/status")
}
func (c *Client4) userStatusesRoute() string {
return fmt.Sprintf(c.usersRoute() + "/status")
}
func (c *Client4) samlRoute() string {
return "/saml"
}
func (c *Client4) ldapRoute() string {
return "/ldap"
}
func (c *Client4) brandRoute() string {
return "/brand"
}
func (c *Client4) dataRetentionRoute() string {
return "/data_retention"
}
func (c *Client4) dataRetentionPolicyRoute(policyID string) string {
return fmt.Sprintf(c.dataRetentionRoute()+"/policies/%v", policyID)
}
func (c *Client4) elasticsearchRoute() string {
return "/elasticsearch"
}
func (c *Client4) bleveRoute() string {
return "/bleve"
}
func (c *Client4) commandsRoute() string {
return "/commands"
}
func (c *Client4) commandRoute(commandId string) string {
return fmt.Sprintf(c.commandsRoute()+"/%v", commandId)
}
func (c *Client4) commandMoveRoute(commandId string) string {
return fmt.Sprintf(c.commandsRoute()+"/%v/move", commandId)
}
func (c *Client4) draftsRoute() string {
return "/drafts"
}
func (c *Client4) emojisRoute() string {
return "/emoji"
}
func (c *Client4) emojiRoute(emojiId string) string {
return fmt.Sprintf(c.emojisRoute()+"/%v", emojiId)
}
func (c *Client4) emojiByNameRoute(name string) string {
return fmt.Sprintf(c.emojisRoute()+"/name/%v", name)
}
func (c *Client4) reactionsRoute() string {
return "/reactions"
}
func (c *Client4) oAuthAppsRoute() string {
return "/oauth/apps"
}
func (c *Client4) oAuthAppRoute(appId string) string {
return fmt.Sprintf("/oauth/apps/%v", appId)
}
func (c *Client4) openGraphRoute() string {
return "/opengraph"
}
func (c *Client4) jobsRoute() string {
return "/jobs"
}
func (c *Client4) rolesRoute() string {
return "/roles"
}
func (c *Client4) schemesRoute() string {
return "/schemes"
}
func (c *Client4) schemeRoute(id string) string {
return c.schemesRoute() + fmt.Sprintf("/%v", id)
}
func (c *Client4) analyticsRoute() string {
return "/analytics"
}
func (c *Client4) timezonesRoute() string {
return fmt.Sprintf(c.systemRoute() + "/timezones")
}
func (c *Client4) channelSchemeRoute(channelId string) string {
return fmt.Sprintf(c.channelsRoute()+"/%v/scheme", channelId)
}
func (c *Client4) teamSchemeRoute(teamId string) string {
return fmt.Sprintf(c.teamsRoute()+"/%v/scheme", teamId)
}
func (c *Client4) totalUsersStatsRoute() string {
return fmt.Sprintf(c.usersRoute() + "/stats")
}
func (c *Client4) redirectLocationRoute() string {
return "/redirect_location"
}
func (c *Client4) serverBusyRoute() string {
return "/server_busy"
}
func (c *Client4) userTermsOfServiceRoute(userId string) string {
return c.userRoute(userId) + "/terms_of_service"
}
func (c *Client4) termsOfServiceRoute() string {
return "/terms_of_service"
}
func (c *Client4) groupsRoute() string {
return "/groups"
}
func (c *Client4) publishUserTypingRoute(userId string) string {
return c.userRoute(userId) + "/typing"
}
func (c *Client4) groupRoute(groupID string) string {
return fmt.Sprintf("%s/%s", c.groupsRoute(), groupID)
}
func (c *Client4) groupSyncableRoute(groupID, syncableID string, syncableType GroupSyncableType) string {
return fmt.Sprintf("%s/%ss/%s", c.groupRoute(groupID), strings.ToLower(syncableType.String()), syncableID)
}
func (c *Client4) groupSyncablesRoute(groupID string, syncableType GroupSyncableType) string {
return fmt.Sprintf("%s/%ss", c.groupRoute(groupID), strings.ToLower(syncableType.String()))
}
func (c *Client4) importsRoute() string {
return "/imports"
}
func (c *Client4) exportsRoute() string {
return "/exports"
}
func (c *Client4) exportRoute(name string) string {
return fmt.Sprintf(c.exportsRoute()+"/%v", name)
}
func (c *Client4) sharedChannelsRoute() string {
return "/sharedchannels"
}
func (c *Client4) permissionsRoute() string {
return "/permissions"
}
func (c *Client4) DoAPIGet(url string, etag string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodGet, c.APIURL+url, "", etag)
}
func (c *Client4) DoAPIPost(url string, data string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodPost, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIDeleteBytes(url string, data []byte) (*http.Response, error) {
return c.DoAPIRequestBytes(http.MethodDelete, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIPatchBytes(url string, data []byte) (*http.Response, error) {
return c.DoAPIRequestBytes(http.MethodPatch, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIPostBytes(url string, data []byte) (*http.Response, error) {
return c.DoAPIRequestBytes(http.MethodPost, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIPut(url string, data string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIPutBytes(url string, data []byte) (*http.Response, error) {
return c.DoAPIRequestBytes(http.MethodPut, c.APIURL+url, data, "")
}
func (c *Client4) DoAPIDelete(url string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "")
}
func (c *Client4) DoAPIRequest(method, url, data, etag string) (*http.Response, error) {
return c.DoAPIRequestReader(method, url, strings.NewReader(data), map[string]string{HeaderEtagClient: etag})
}
func (c *Client4) DoAPIRequestWithHeaders(method, url, data string, headers map[string]string) (*http.Response, error) {
return c.DoAPIRequestReader(method, url, strings.NewReader(data), headers)
}
func (c *Client4) DoAPIRequestBytes(method, url string, data []byte, etag string) (*http.Response, error) {
return c.DoAPIRequestReader(method, url, bytes.NewReader(data), map[string]string{HeaderEtagClient: etag})
}
func (c *Client4) DoAPIRequestReader(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) {
rq, err := http.NewRequest(method, url, data)
if err != nil {
return nil, err
}
for k, v := range headers {
rq.Header.Set(k, v)
}
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
if c.HTTPHeader != nil && len(c.HTTPHeader) > 0 {
for k, v := range c.HTTPHeader {
rq.Header.Set(k, v)
}
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return rp, err
}
if rp.StatusCode == 304 {
return rp, nil
}
if rp.StatusCode >= 300 {
defer closeBody(rp)
return rp, AppErrorFromJSON(rp.Body)
}
return rp, nil
}
func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*FileUploadResponse, *Response, error) {
return c.doUploadFile(url, bytes.NewReader(data), contentType, 0)
}
func (c *Client4) doUploadFile(url string, body io.Reader, contentType string, contentLength int64) (*FileUploadResponse, *Response, error) {
rq, err := http.NewRequest("POST", c.APIURL+url, body)
if err != nil {
return nil, nil, err
}
if contentLength != 0 {
rq.ContentLength = contentLength
}
rq.Header.Set("Content-Type", contentType)
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
var res FileUploadResponse
if err := json.NewDecoder(rp.Body).Decode(&res); err != nil {
return nil, nil, NewAppError("doUploadFile", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &res, BuildResponse(rp), nil
}
func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response, error) {
rq, err := http.NewRequest("POST", c.APIURL+url, bytes.NewReader(data))
if err != nil {
return nil, nil, err
}
rq.Header.Set("Content-Type", contentType)
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
var e Emoji
if err := json.NewDecoder(rp.Body).Decode(&e); err != nil {
return nil, nil, NewAppError("DoEmojiUploadFile", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &e, BuildResponse(rp), nil
}
func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) (map[string]string, *Response, error) {
rq, err := http.NewRequest("POST", c.APIURL+url, bytes.NewReader(data))
if err != nil {
return nil, nil, err
}
rq.Header.Set("Content-Type", contentType)
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
return MapFromJSON(rp.Body), BuildResponse(rp), nil
}
// Authentication Section
// LoginById authenticates a user by user id and password.
func (c *Client4) LoginById(id string, password string) (*User, *Response, error) {
m := make(map[string]string)
m["id"] = id
m["password"] = password
return c.login(m)
}
// Login authenticates a user by login id, which can be username, email or some sort
// of SSO identifier based on server configuration, and a password.
func (c *Client4) Login(loginId string, password string) (*User, *Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
return c.login(m)
}
// LoginByLdap authenticates a user by LDAP id and password.
func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["ldap_only"] = c.boolString(true)
return c.login(m)
}
// LoginWithDevice authenticates a user by login id (username, email or some sort
// of SSO identifier based on configuration), password and attaches a device id to
// the session.
func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["device_id"] = deviceId
return c.login(m)
}
// LoginWithMFA logs a user in with a MFA token
func (c *Client4) LoginWithMFA(loginId, password, mfaToken string) (*User, *Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["token"] = mfaToken
return c.login(m)
}
func (c *Client4) login(m map[string]string) (*User, *Response, error) {
r, err := c.DoAPIPost("/users/login", MapToJSON(m))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
c.AuthToken = r.Header.Get(HeaderToken)
c.AuthType = HeaderBearer
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
return nil, nil, NewAppError("login", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &user, BuildResponse(r), nil
}
// Logout terminates the current user's session.
func (c *Client4) Logout() (*Response, error) {
r, err := c.DoAPIPost("/users/logout", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
c.AuthToken = ""
c.AuthType = HeaderBearer
return BuildResponse(r), nil
}
// SwitchAccountType changes a user's login type from one type to another.
func (c *Client4) SwitchAccountType(switchRequest *SwitchRequest) (string, *Response, error) {
buf, err := json.Marshal(switchRequest)
if err != nil {
return "", BuildResponse(nil), NewAppError("SwitchAccountType", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.usersRoute()+"/login/switch", buf)
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["follow_link"], BuildResponse(r), nil
}
// User Section
// CreateUser creates a user in the system based on the provided user struct.
func (c *Client4) CreateUser(user *User) (*User, *Response, error) {
userJSON, err := json.Marshal(user)
if err != nil {
return nil, nil, NewAppError("CreateUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.usersRoute(), string(userJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("CreateUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// CreateUserWithToken creates a user in the system based on the provided tokenId.
func (c *Client4) CreateUserWithToken(user *User, tokenId string) (*User, *Response, error) {
if tokenId == "" {
return nil, nil, NewAppError("MissingHashOrData", "api.user.create_user.missing_token.app_error", nil, "", http.StatusBadRequest)
}
query := "?t=" + tokenId
buf, err := json.Marshal(user)
if err != nil {
return nil, nil, NewAppError("CreateUserWithToken", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.usersRoute()+query, buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("CreateUserWithToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// CreateUserWithInviteId creates a user in the system based on the provided invited id.
func (c *Client4) CreateUserWithInviteId(user *User, inviteId string) (*User, *Response, error) {
if inviteId == "" {
return nil, nil, NewAppError("MissingInviteId", "api.user.create_user.missing_invite_id.app_error", nil, "", http.StatusBadRequest)
}
query := "?iid=" + url.QueryEscape(inviteId)
buf, err := json.Marshal(user)
if err != nil {
return nil, nil, NewAppError("CreateUserWithInviteId", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.usersRoute()+query, buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("CreateUserWithInviteId", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// GetMe returns the logged in user.
func (c *Client4) GetMe(etag string) (*User, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(Me), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("GetMe", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// GetUser returns a user based on the provided user id string.
func (c *Client4) GetUser(userId, etag string) (*User, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("GetUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// GetUserByUsername returns a user based on the provided user name string.
func (c *Client4) GetUserByUsername(userName, etag string) (*User, *Response, error) {
r, err := c.DoAPIGet(c.userByUsernameRoute(userName), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("GetUserByUsername", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// GetUserByEmail returns a user based on the provided user email string.
func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response, error) {
r, err := c.DoAPIGet(c.userByEmailRoute(email), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("GetUserByEmail", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// AutocompleteUsersInTeam returns the users on a team based on search term.
func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, limit int, etag string) (*UserAutocomplete, *Response, error) {
query := fmt.Sprintf("?in_team=%v&name=%v&limit=%d", teamId, username, limit)
r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u UserAutocomplete
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("AutocompleteUsersInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// AutocompleteUsersInChannel returns the users in a channel based on search term.
func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, limit int, etag string) (*UserAutocomplete, *Response, error) {
query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v&limit=%d", teamId, channelId, username, limit)
r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u UserAutocomplete
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("AutocompleteUsersInChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// AutocompleteUsers returns the users in the system based on search term.
func (c *Client4) AutocompleteUsers(username string, limit int, etag string) (*UserAutocomplete, *Response, error) {
query := fmt.Sprintf("?name=%v&limit=%d", username, limit)
r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u UserAutocomplete
if r.StatusCode == http.StatusNotModified {
return &u, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("AutocompleteUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// GetDefaultProfileImage gets the default user's profile image. Must be logged in.
func (c *Client4) GetDefaultProfileImage(userId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/image/default", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetDefaultProfileImage", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetProfileImage gets user's profile image. Must be logged in.
func (c *Client4) GetProfileImage(userId, etag string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/image", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetProfileImage", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetUsers returns a page of users on the system. Page counting starts at 0.
func (c *Client4) GetUsers(page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersWithChannelRoles returns a page of users on the system. Page counting starts at 0.
func (c *Client4) GetUsersWithCustomQueryParameters(page int, perPage int, queryParameters, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&%v", page, perPage, queryParameters)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetNewUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetNewUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?sort=create_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetNewUsersInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetRecentlyActiveUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetRecentlyActiveUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?sort=last_activity_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetRecentlyActiveUsersInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetActiveUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetActiveUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?active=true&in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetActiveUsersInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersNotInTeam returns a page of users who are not in a team. Page counting starts at 0.
func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?not_in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersNotInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0.
func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersInChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersInChannelByStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status
func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersInChannelByStatus", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0.
func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?in_team=%v¬_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersNotInChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersWithoutTeam returns a page of users on the system that aren't on any teams. Page counting starts at 0.
func (c *Client4) GetUsersWithoutTeam(page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersWithoutTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersInGroup returns a page of users in a group. Page counting starts at 0.
func (c *Client4) GetUsersInGroup(groupID string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?in_group=%v&page=%v&per_page=%v", groupID, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersInGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersInGroup returns a page of users in a group. Page counting starts at 0.
func (c *Client4) GetUsersInGroupByDisplayName(groupID string, page int, perPage int, etag string) ([]*User, *Response, error) {
query := fmt.Sprintf("?sort=display_name&in_group=%v&page=%v&per_page=%v", groupID, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersInGroupByDisplayName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersByIds returns a list of users based on the provided user ids.
func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/ids", ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersByIds", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersByIds returns a list of users based on the provided user ids.
func (c *Client4) GetUsersByIdsWithOptions(userIds []string, options *UserGetByIdsOptions) ([]*User, *Response, error) {
v := url.Values{}
if options.Since != 0 {
v.Set("since", fmt.Sprintf("%d", options.Since))
}
url := c.usersRoute() + "/ids"
if len(v) > 0 {
url += "?" + v.Encode()
}
r, err := c.DoAPIPost(url, ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersByIdsWithOptions", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersByUsernames returns a list of users based on the provided usernames.
func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/usernames", ArrayToJSON(usernames))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersByUsernames", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUsersByGroupChannelIds returns a map with channel ids as keys
// and a list of users as values based on the provided user ids.
func (c *Client4) GetUsersByGroupChannelIds(groupChannelIds []string) (map[string][]*User, *Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/group_channels", ArrayToJSON(groupChannelIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
usersByChannelId := map[string][]*User{}
json.NewDecoder(r.Body).Decode(&usersByChannelId)
return usersByChannelId, BuildResponse(r), nil
}
// SearchUsers returns a list of users based on some search criteria.
func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response, error) {
buf, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.usersRoute()+"/search", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// UpdateUser updates a user in the system based on the provided user struct.
func (c *Client4) UpdateUser(user *User) (*User, *Response, error) {
buf, err := json.Marshal(user)
if err != nil {
return nil, nil, NewAppError("UpdateUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.userRoute(user.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("UpdateUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// PatchUser partially updates a user in the system. Any missing fields are not updated.
func (c *Client4) PatchUser(userId string, patch *UserPatch) (*User, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.userRoute(userId)+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("PatchUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// UpdateUserAuth updates a user AuthData (uthData, authService and password) in the system.
func (c *Client4) UpdateUserAuth(userId string, userAuth *UserAuth) (*UserAuth, *Response, error) {
buf, err := json.Marshal(userAuth)
if err != nil {
return nil, nil, NewAppError("UpdateUserAuth", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.userRoute(userId)+"/auth", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ua UserAuth
if err := json.NewDecoder(r.Body).Decode(&ua); err != nil {
return nil, nil, NewAppError("UpdateUserAuth", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ua, BuildResponse(r), nil
}
// UpdateUserMfa activates multi-factor authentication for a user if activate
// is true and a valid code is provided. If activate is false, then code is not
// required and multi-factor authentication is disabled for the user.
func (c *Client4) UpdateUserMfa(userId, code string, activate bool) (*Response, error) {
requestBody := make(map[string]any)
requestBody["activate"] = activate
requestBody["code"] = code
r, err := c.DoAPIPut(c.userRoute(userId)+"/mfa", StringInterfaceToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GenerateMfaSecret will generate a new MFA secret for a user and return it as a string and
// as a base64 encoded image QR code.
func (c *Client4) GenerateMfaSecret(userId string) (*MfaSecret, *Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+"/mfa/generate", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var secret MfaSecret
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
return nil, nil, NewAppError("GenerateMfaSecret", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &secret, BuildResponse(r), nil
}
// UpdateUserPassword updates a user's password. Must be logged in as the user or be a system administrator.
func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (*Response, error) {
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateUserHashedPassword updates a user's password with an already-hashed password. Must be a system administrator.
func (c *Client4) UpdateUserHashedPassword(userId, newHashedPassword string) (*Response, error) {
requestBody := map[string]string{"already_hashed": "true", "new_password": newHashedPassword}
r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PromoteGuestToUser convert a guest into a regular user
func (c *Client4) PromoteGuestToUser(guestId string) (*Response, error) {
r, err := c.DoAPIPost(c.userRoute(guestId)+"/promote", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DemoteUserToGuest convert a regular user into a guest
func (c *Client4) DemoteUserToGuest(guestId string) (*Response, error) {
r, err := c.DoAPIPost(c.userRoute(guestId)+"/demote", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateUserRoles updates a user's roles in the system. A user can have "system_user" and "system_admin" roles.
func (c *Client4) UpdateUserRoles(userId, roles string) (*Response, error) {
requestBody := map[string]string{"roles": roles}
r, err := c.DoAPIPut(c.userRoute(userId)+"/roles", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateUserActive updates status of a user whether active or not.
func (c *Client4) UpdateUserActive(userId string, active bool) (*Response, error) {
requestBody := make(map[string]any)
requestBody["active"] = active
r, err := c.DoAPIPut(c.userRoute(userId)+"/active", StringInterfaceToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeleteUser deactivates a user in the system based on the provided user id string.
func (c *Client4) DeleteUser(userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PermanentDeleteUser deletes a user in the system based on the provided user id string.
func (c *Client4) PermanentDeleteUser(userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId) + "?permanent=" + c.boolString(true))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// ConvertUserToBot converts a user to a bot user.
func (c *Client4) ConvertUserToBot(userId string) (*Bot, *Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+"/convert_to_bot", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("ConvertUserToBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// ConvertBotToUser converts a bot user to a user.
func (c *Client4) ConvertBotToUser(userId string, userPatch *UserPatch, setSystemAdmin bool) (*User, *Response, error) {
var query string
if setSystemAdmin {
query = "?set_system_admin=true"
}
buf, err := json.Marshal(userPatch)
if err != nil {
return nil, nil, NewAppError("ConvertBotToUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.botRoute(userId)+"/convert_to_user"+query, buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("ConvertBotToUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// PermanentDeleteAll permanently deletes all users in the system. This is a local only endpoint
func (c *Client4) PermanentDeleteAllUsers() (*Response, error) {
r, err := c.DoAPIDelete(c.usersRoute())
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// SendPasswordResetEmail will send a link for password resetting to a user with the
// provided email.
func (c *Client4) SendPasswordResetEmail(email string) (*Response, error) {
requestBody := map[string]string{"email": email}
r, err := c.DoAPIPost(c.usersRoute()+"/password/reset/send", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// ResetPassword uses a recovery code to update reset a user's password.
func (c *Client4) ResetPassword(token, newPassword string) (*Response, error) {
requestBody := map[string]string{"token": token, "new_password": newPassword}
r, err := c.DoAPIPost(c.usersRoute()+"/password/reset", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetSessions returns a list of sessions based on the provided user id string.
func (c *Client4) GetSessions(userId, etag string) ([]*Session, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/sessions", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Session
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetSessions", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// RevokeSession revokes a user session based on the provided user id and session id strings.
func (c *Client4) RevokeSession(userId, sessionId string) (*Response, error) {
requestBody := map[string]string{"session_id": sessionId}
r, err := c.DoAPIPost(c.userRoute(userId)+"/sessions/revoke", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RevokeAllSessions revokes all sessions for the provided user id string.
func (c *Client4) RevokeAllSessions(userId string) (*Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+"/sessions/revoke/all", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RevokeAllSessions revokes all sessions for all the users.
func (c *Client4) RevokeSessionsFromAllUsers() (*Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/sessions/revoke/all", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// AttachDeviceId attaches a mobile device ID to the current session.
func (c *Client4) AttachDeviceId(deviceId string) (*Response, error) {
requestBody := map[string]string{"device_id": deviceId}
r, err := c.DoAPIPut(c.usersRoute()+"/sessions/device", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTeamsUnreadForUser will return an array with TeamUnread objects that contain the amount
// of unread messages and mentions the current user has for the teams it belongs to.
// An optional team ID can be set to exclude that team from the results.
// An optional boolean can be set to include collapsed thread unreads. Must be authenticated.
func (c *Client4) GetTeamsUnreadForUser(userId, teamIdToExclude string, includeCollapsedThreads bool) ([]*TeamUnread, *Response, error) {
query := url.Values{}
if teamIdToExclude != "" {
query.Set("exclude_team", teamIdToExclude)
}
if includeCollapsedThreads {
query.Set("include_collapsed_threads", "true")
}
r, err := c.DoAPIGet(c.userRoute(userId)+"/teams/unread?"+query.Encode(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*TeamUnread
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetTeamsUnreadForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUserAudits returns a list of audit based on the provided user id string.
func (c *Client4) GetUserAudits(userId string, page int, perPage int, etag string) (Audits, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/audits"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var audits Audits
err = json.NewDecoder(r.Body).Decode(&audits)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetUserAudits", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return audits, BuildResponse(r), nil
}
// VerifyUserEmail will verify a user's email using the supplied token.
func (c *Client4) VerifyUserEmail(token string) (*Response, error) {
requestBody := map[string]string{"token": token}
r, err := c.DoAPIPost(c.usersRoute()+"/email/verify", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// VerifyUserEmailWithoutToken will verify a user's email by its Id. (Requires manage system role)
func (c *Client4) VerifyUserEmailWithoutToken(userId string) (*User, *Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+"/email/verify/member", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("VerifyUserEmailWithoutToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// SendVerificationEmail will send an email to the user with the provided email address, if
// that user exists. The email will contain a link that can be used to verify the user's
// email address.
func (c *Client4) SendVerificationEmail(email string) (*Response, error) {
requestBody := map[string]string{"email": email}
r, err := c.DoAPIPost(c.usersRoute()+"/email/verify/send", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// SetDefaultProfileImage resets the profile image to a default generated one.
func (c *Client4) SetDefaultProfileImage(userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId) + "/image")
if err != nil {
return BuildResponse(r), err
}
return BuildResponse(r), nil
}
// SetProfileImage sets profile image of the user.
func (c *Client4) SetProfileImage(userId string, data []byte) (*Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "profile.png")
if err != nil {
return nil, NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = writer.Close(); err != nil {
return nil, NewAppError("SetProfileImage", "model.client.set_profile_user.writer.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rq, err := http.NewRequest("POST", c.APIURL+c.userRoute(userId)+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
return BuildResponse(rp), nil
}
// CreateUserAccessToken will generate a user access token that can be used in place
// of a session token to access the REST API. Must have the 'create_user_access_token'
// permission and if generating for another user, must have the 'edit_other_users'
// permission. A non-blank description is required.
func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccessToken, *Response, error) {
requestBody := map[string]string{"description": description}
r, err := c.DoAPIPost(c.userRoute(userId)+"/tokens", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var uat UserAccessToken
if err := json.NewDecoder(r.Body).Decode(&uat); err != nil {
return nil, nil, NewAppError("CreateUserAccessToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &uat, BuildResponse(r), nil
}
// GetUserAccessTokens will get a page of access tokens' id, description, is_active
// and the user_id in the system. The actual token will not be returned. Must have
// the 'manage_system' permission.
func (c *Client4) GetUserAccessTokens(page int, perPage int) ([]*UserAccessToken, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userAccessTokensRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*UserAccessToken
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUserAccessTokens", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetUserAccessToken will get a user access tokens' id, description, is_active
// and the user_id of the user it is for. The actual token will not be returned.
// Must have the 'read_user_access_token' permission and if getting for another
// user, must have the 'edit_other_users' permission.
func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response, error) {
r, err := c.DoAPIGet(c.userAccessTokenRoute(tokenId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var uat UserAccessToken
if err := json.NewDecoder(r.Body).Decode(&uat); err != nil {
return nil, nil, NewAppError("GetUserAccessToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &uat, BuildResponse(r), nil
}
// GetUserAccessTokensForUser will get a paged list of user access tokens showing id,
// description and user_id for each. The actual tokens will not be returned. Must have
// the 'read_user_access_token' permission and if getting for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*UserAccessToken, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/tokens"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*UserAccessToken
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUserAccessTokensForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// RevokeUserAccessToken will revoke a user access token by id. Must have the
// 'revoke_user_access_token' permission and if revoking for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) RevokeUserAccessToken(tokenId string) (*Response, error) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoAPIPost(c.usersRoute()+"/tokens/revoke", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// SearchUserAccessTokens returns user access tokens matching the provided search term.
func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response, error) {
buf, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.usersRoute()+"/tokens/search", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*UserAccessToken
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchUserAccessTokens", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// DisableUserAccessToken will disable a user access token by id. Must have the
// 'revoke_user_access_token' permission and if disabling for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) DisableUserAccessToken(tokenId string) (*Response, error) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoAPIPost(c.usersRoute()+"/tokens/disable", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// EnableUserAccessToken will enable a user access token by id. Must have the
// 'create_user_access_token' permission and if enabling for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) EnableUserAccessToken(tokenId string) (*Response, error) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoAPIPost(c.usersRoute()+"/tokens/enable", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Bots section
// CreateBot creates a bot in the system based on the provided bot struct.
func (c *Client4) CreateBot(bot *Bot) (*Bot, *Response, error) {
buf, err := json.Marshal(bot)
if err != nil {
return nil, nil, NewAppError("CreateBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.botsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resp *Bot
err = json.NewDecoder(r.Body).Decode(&resp)
if err != nil {
return nil, BuildResponse(r), NewAppError("CreateBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return resp, BuildResponse(r), nil
}
// PatchBot partially updates a bot. Any missing fields are not updated.
func (c *Client4) PatchBot(userId string, patch *BotPatch) (*Bot, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.botRoute(userId), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("PatchBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// GetBot fetches the given, undeleted bot.
func (c *Client4) GetBot(userId string, etag string) (*Bot, *Response, error) {
r, err := c.DoAPIGet(c.botRoute(userId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// GetBotIncludeDeleted fetches the given bot, even if it is deleted.
func (c *Client4) GetBotIncludeDeleted(userId string, etag string) (*Bot, *Response, error) {
r, err := c.DoAPIGet(c.botRoute(userId)+"?include_deleted="+c.boolString(true), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBotIncludeDeleted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// GetBots fetches the given page of bots, excluding deleted.
func (c *Client4) GetBots(page, perPage int, etag string) ([]*Bot, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.botsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bots BotList
err = json.NewDecoder(r.Body).Decode(&bots)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBots", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bots, BuildResponse(r), nil
}
// GetBotsIncludeDeleted fetches the given page of bots, including deleted.
func (c *Client4) GetBotsIncludeDeleted(page, perPage int, etag string) ([]*Bot, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted="+c.boolString(true), page, perPage)
r, err := c.DoAPIGet(c.botsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bots BotList
err = json.NewDecoder(r.Body).Decode(&bots)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBotsIncludeDeleted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bots, BuildResponse(r), nil
}
// GetBotsOrphaned fetches the given page of bots, only including orphaned bots.
func (c *Client4) GetBotsOrphaned(page, perPage int, etag string) ([]*Bot, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&only_orphaned="+c.boolString(true), page, perPage)
r, err := c.DoAPIGet(c.botsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bots BotList
err = json.NewDecoder(r.Body).Decode(&bots)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBotsOrphaned", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bots, BuildResponse(r), nil
}
// DisableBot disables the given bot in the system.
func (c *Client4) DisableBot(botUserId string) (*Bot, *Response, error) {
r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/disable", nil)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("DisableBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// EnableBot disables the given bot in the system.
func (c *Client4) EnableBot(botUserId string) (*Bot, *Response, error) {
r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/enable", nil)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("EnableBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// AssignBot assigns the given bot to the given user
func (c *Client4) AssignBot(botUserId, newOwnerId string) (*Bot, *Response, error) {
r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/assign/"+newOwnerId, nil)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var bot *Bot
err = json.NewDecoder(r.Body).Decode(&bot)
if err != nil {
return nil, BuildResponse(r), NewAppError("AssignBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bot, BuildResponse(r), nil
}
// Team Section
// CreateTeam creates a team in the system based on the provided team struct.
func (c *Client4) CreateTeam(team *Team) (*Team, *Response, error) {
buf, err := json.Marshal(team)
if err != nil {
return nil, nil, NewAppError("CreateTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("CreateTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// GetTeam returns a team based on the provided team id string.
func (c *Client4) GetTeam(teamId, etag string) (*Team, *Response, error) {
r, err := c.DoAPIGet(c.teamRoute(teamId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("GetTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// GetAllTeams returns all teams based on permissions.
func (c *Client4) GetAllTeams(etag string, page int, perPage int) ([]*Team, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.teamsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Team
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetAllTeams", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetAllTeamsWithTotalCount returns all teams based on permissions.
func (c *Client4) GetAllTeamsWithTotalCount(etag string, page int, perPage int) ([]*Team, int64, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count="+c.boolString(true), page, perPage)
r, err := c.DoAPIGet(c.teamsRoute()+query, etag)
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
var listWithCount TeamsWithCount
if err := json.NewDecoder(r.Body).Decode(&listWithCount); err != nil {
return nil, 0, nil, NewAppError("GetAllTeamsWithTotalCount", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return listWithCount.Teams, listWithCount.TotalCount, BuildResponse(r), nil
}
// GetAllTeamsExcludePolicyConstrained returns all teams which are not part of a data retention policy.
// Must be a system administrator.
func (c *Client4) GetAllTeamsExcludePolicyConstrained(etag string, page int, perPage int) ([]*Team, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&exclude_policy_constrained=%v", page, perPage, true)
r, err := c.DoAPIGet(c.teamsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Team
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetAllTeamsExcludePolicyConstrained", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetTeamByName returns a team based on the provided team name string.
func (c *Client4) GetTeamByName(name, etag string) (*Team, *Response, error) {
r, err := c.DoAPIGet(c.teamByNameRoute(name), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("GetTeamByName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// SearchTeams returns teams matching the provided search term.
func (c *Client4) SearchTeams(search *TeamSearch) ([]*Team, *Response, error) {
buf, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchTeams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamsRoute()+"/search", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Team
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchTeams", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// SearchTeamsPaged returns a page of teams and the total count matching the provided search term.
func (c *Client4) SearchTeamsPaged(search *TeamSearch) ([]*Team, int64, *Response, error) {
if search.Page == nil {
search.Page = NewInt(0)
}
if search.PerPage == nil {
search.PerPage = NewInt(100)
}
buf, err := json.Marshal(search)
if err != nil {
return nil, 0, BuildResponse(nil), NewAppError("SearchTeamsPaged", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamsRoute()+"/search", buf)
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
var listWithCount TeamsWithCount
if err := json.NewDecoder(r.Body).Decode(&listWithCount); err != nil {
return nil, 0, nil, NewAppError("GetAllTeamsWithTotalCount", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return listWithCount.Teams, listWithCount.TotalCount, BuildResponse(r), nil
}
// TeamExists returns true or false if the team exist or not.
func (c *Client4) TeamExists(name, etag string) (bool, *Response, error) {
r, err := c.DoAPIGet(c.teamByNameRoute(name)+"/exists", etag)
if err != nil {
return false, BuildResponse(r), err
}
defer closeBody(r)
return MapBoolFromJSON(r.Body)["exists"], BuildResponse(r), nil
}
// GetTeamsForUser returns a list of teams a user is on. Must be logged in as the user
// or be a system administrator.
func (c *Client4) GetTeamsForUser(userId, etag string) ([]*Team, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/teams", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Team
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetTeamsForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetTeamMember returns a team member based on the provided team and user id strings.
func (c *Client4) GetTeamMember(teamId, userId, etag string) (*TeamMember, *Response, error) {
r, err := c.DoAPIGet(c.teamMemberRoute(teamId, userId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tm TeamMember
if r.StatusCode == http.StatusNotModified {
return &tm, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&tm); err != nil {
return nil, nil, NewAppError("GetTeamMember", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tm, BuildResponse(r), nil
}
// UpdateTeamMemberRoles will update the roles on a team for a user.
func (c *Client4) UpdateTeamMemberRoles(teamId, userId, newRoles string) (*Response, error) {
requestBody := map[string]string{"roles": newRoles}
r, err := c.DoAPIPut(c.teamMemberRoute(teamId, userId)+"/roles", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateTeamMemberSchemeRoles will update the scheme-derived roles on a team for a user.
func (c *Client4) UpdateTeamMemberSchemeRoles(teamId string, userId string, schemeRoles *SchemeRoles) (*Response, error) {
buf, err := json.Marshal(schemeRoles)
if err != nil {
return nil, NewAppError("UpdateTeamMemberSchemeRoles", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.teamMemberRoute(teamId, userId)+"/schemeRoles", buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateTeam will update a team.
func (c *Client4) UpdateTeam(team *Team) (*Team, *Response, error) {
buf, err := json.Marshal(team)
if err != nil {
return nil, nil, NewAppError("UpdateTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.teamRoute(team.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("UpdateTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// PatchTeam partially updates a team. Any missing fields are not updated.
func (c *Client4) PatchTeam(teamId string, patch *TeamPatch) (*Team, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.teamRoute(teamId)+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("PatchTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// RestoreTeam restores a previously deleted team.
func (c *Client4) RestoreTeam(teamId string) (*Team, *Response, error) {
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/restore", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("RestoreTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// RegenerateTeamInviteId requests a new invite ID to be generated.
func (c *Client4) RegenerateTeamInviteId(teamId string) (*Team, *Response, error) {
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/regenerate_invite_id", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("RegenerateTeamInviteId", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// SoftDeleteTeam deletes the team softly (archive only, not permanent delete).
func (c *Client4) SoftDeleteTeam(teamId string) (*Response, error) {
r, err := c.DoAPIDelete(c.teamRoute(teamId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PermanentDeleteTeam deletes the team, should only be used when needed for
// compliance and the like.
func (c *Client4) PermanentDeleteTeam(teamId string) (*Response, error) {
r, err := c.DoAPIDelete(c.teamRoute(teamId) + "?permanent=" + c.boolString(true))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateTeamPrivacy modifies the team type (model.TeamOpen <--> model.TeamInvite) and sets
// the corresponding AllowOpenInvite appropriately.
func (c *Client4) UpdateTeamPrivacy(teamId string, privacy string) (*Team, *Response, error) {
requestBody := map[string]string{"privacy": privacy}
r, err := c.DoAPIPut(c.teamRoute(teamId)+"/privacy", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("UpdateTeamPrivacy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// GetTeamMembers returns team members based on the provided team id string.
func (c *Client4) GetTeamMembers(teamId string, page int, perPage int, etag string) ([]*TeamMember, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.teamMembersRoute(teamId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMember
if r.StatusCode == http.StatusNotModified {
return tms, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("GetTeamMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// GetTeamMembersWithoutDeletedUsers returns team members based on the provided team id string. Additional parameters of sort and exclude_deleted_users accepted as well
// Could not add it to above function due to it be a breaking change.
func (c *Client4) GetTeamMembersSortAndWithoutDeletedUsers(teamId string, page int, perPage int, sort string, excludeDeletedUsers bool, etag string) ([]*TeamMember, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v&exclude_deleted_users=%v", page, perPage, sort, excludeDeletedUsers)
r, err := c.DoAPIGet(c.teamMembersRoute(teamId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMember
if r.StatusCode == http.StatusNotModified {
return tms, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("GetTeamMembersSortAndWithoutDeletedUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// GetTeamMembersForUser returns the team members for a user.
func (c *Client4) GetTeamMembersForUser(userId string, etag string) ([]*TeamMember, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/teams/members", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMember
if r.StatusCode == http.StatusNotModified {
return tms, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("GetTeamMembersForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// GetTeamMembersByIds will return an array of team members based on the
// team id and a list of user ids provided. Must be authenticated.
func (c *Client4) GetTeamMembersByIds(teamId string, userIds []string) ([]*TeamMember, *Response, error) {
r, err := c.DoAPIPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMember
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("GetTeamMembersByIds", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// AddTeamMember adds user to a team and return a team member.
func (c *Client4) AddTeamMember(teamId, userId string) (*TeamMember, *Response, error) {
member := &TeamMember{TeamId: teamId, UserId: userId}
buf, err := json.Marshal(member)
if err != nil {
return nil, nil, NewAppError("AddTeamMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamMembersRoute(teamId), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tm TeamMember
if err := json.NewDecoder(r.Body).Decode(&tm); err != nil {
return nil, nil, NewAppError("AddTeamMember", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tm, BuildResponse(r), nil
}
// AddTeamMemberFromInvite adds a user to a team and return a team member using an invite id
// or an invite token/data pair.
func (c *Client4) AddTeamMemberFromInvite(token, inviteId string) (*TeamMember, *Response, error) {
var query string
if inviteId != "" {
query += fmt.Sprintf("?invite_id=%v", inviteId)
}
if token != "" {
query += fmt.Sprintf("?token=%v", token)
}
r, err := c.DoAPIPost(c.teamsRoute()+"/members/invite"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tm TeamMember
if err := json.NewDecoder(r.Body).Decode(&tm); err != nil {
return nil, nil, NewAppError("AddTeamMemberFromInvite", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tm, BuildResponse(r), nil
}
// AddTeamMembers adds a number of users to a team and returns the team members.
func (c *Client4) AddTeamMembers(teamId string, userIds []string) ([]*TeamMember, *Response, error) {
var members []*TeamMember
for _, userId := range userIds {
member := &TeamMember{TeamId: teamId, UserId: userId}
members = append(members, member)
}
js, err := json.Marshal(members)
if err != nil {
return nil, nil, NewAppError("AddTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.teamMembersRoute(teamId)+"/batch", string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMember
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("AddTeamMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// AddTeamMembers adds a number of users to a team and returns the team members.
func (c *Client4) AddTeamMembersGracefully(teamId string, userIds []string) ([]*TeamMemberWithError, *Response, error) {
var members []*TeamMember
for _, userId := range userIds {
member := &TeamMember{TeamId: teamId, UserId: userId}
members = append(members, member)
}
js, err := json.Marshal(members)
if err != nil {
return nil, nil, NewAppError("AddTeamMembersGracefully", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.teamMembersRoute(teamId)+"/batch?graceful="+c.boolString(true), string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tms []*TeamMemberWithError
if err := json.NewDecoder(r.Body).Decode(&tms); err != nil {
return nil, nil, NewAppError("AddTeamMembersGracefully", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tms, BuildResponse(r), nil
}
// RemoveTeamMember will remove a user from a team.
func (c *Client4) RemoveTeamMember(teamId, userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.teamMemberRoute(teamId, userId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTeamStats returns a team stats based on the team id string.
// Must be authenticated.
func (c *Client4) GetTeamStats(teamId, etag string) (*TeamStats, *Response, error) {
r, err := c.DoAPIGet(c.teamStatsRoute(teamId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ts TeamStats
if err := json.NewDecoder(r.Body).Decode(&ts); err != nil {
return nil, nil, NewAppError("GetTeamStats", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ts, BuildResponse(r), nil
}
// GetTotalUsersStats returns a total system user stats.
// Must be authenticated.
func (c *Client4) GetTotalUsersStats(etag string) (*UsersStats, *Response, error) {
r, err := c.DoAPIGet(c.totalUsersStatsRoute(), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var stats UsersStats
if err := json.NewDecoder(r.Body).Decode(&stats); err != nil {
return nil, nil, NewAppError("GetTotalUsersStats", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &stats, BuildResponse(r), nil
}
// GetTeamUnread will return a TeamUnread object that contains the amount of
// unread messages and mentions the user has for the specified team.
// Must be authenticated.
func (c *Client4) GetTeamUnread(teamId, userId string) (*TeamUnread, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+c.teamRoute(teamId)+"/unread", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tu TeamUnread
if err := json.NewDecoder(r.Body).Decode(&tu); err != nil {
return nil, nil, NewAppError("GetTeamUnread", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tu, BuildResponse(r), nil
}
// ImportTeam will import an exported team from other app into a existing team.
func (c *Client4) ImportTeam(data []byte, filesize int, importFrom, filename, teamId string) (map[string]string, *Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return nil, nil, err
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, nil, err
}
part, err = writer.CreateFormField("filesize")
if err != nil {
return nil, nil, err
}
if _, err = io.Copy(part, strings.NewReader(strconv.Itoa(filesize))); err != nil {
return nil, nil, err
}
part, err = writer.CreateFormField("importFrom")
if err != nil {
return nil, nil, err
}
if _, err := io.Copy(part, strings.NewReader(importFrom)); err != nil {
return nil, nil, err
}
if err := writer.Close(); err != nil {
return nil, nil, err
}
return c.DoUploadImportTeam(c.teamImportRoute(teamId), body.Bytes(), writer.FormDataContentType())
}
// InviteUsersToTeam invite users by email to the team.
func (c *Client4) InviteUsersToTeam(teamId string, userEmails []string) (*Response, error) {
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email", ArrayToJSON(userEmails))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// InviteGuestsToTeam invite guest by email to some channels in a team.
func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channels []string, message string) (*Response, error) {
guestsInvite := GuestsInvite{
Emails: userEmails,
Channels: channels,
Message: message,
}
buf, err := json.Marshal(guestsInvite)
if err != nil {
return nil, NewAppError("InviteGuestsToTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite-guests/email", buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// InviteUsersToTeam invite users by email to the team.
func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response, error) {
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), ArrayToJSON(userEmails))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*EmailInviteWithError
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("InviteUsersToTeamGracefully", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// InviteUsersToTeam invite users by email to the team.
func (c *Client4) InviteUsersToTeamAndChannelsGracefully(teamId string, userEmails []string, channelIds []string, message string) ([]*EmailInviteWithError, *Response, error) {
memberInvite := MemberInvite{
Emails: userEmails,
ChannelIds: channelIds,
Message: message,
}
buf, err := json.Marshal(memberInvite)
if err != nil {
return nil, nil, NewAppError("InviteMembersToTeamAndChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*EmailInviteWithError
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("InviteUsersToTeamGracefully", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// InviteGuestsToTeam invite guest by email to some channels in a team.
func (c *Client4) InviteGuestsToTeamGracefully(teamId string, userEmails []string, channels []string, message string) ([]*EmailInviteWithError, *Response, error) {
guestsInvite := GuestsInvite{
Emails: userEmails,
Channels: channels,
Message: message,
}
buf, err := json.Marshal(guestsInvite)
if err != nil {
return nil, nil, NewAppError("InviteGuestsToTeamGracefully", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite-guests/email?graceful="+c.boolString(true), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*EmailInviteWithError
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("InviteGuestsToTeamGracefully", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user.
func (c *Client4) InvalidateEmailInvites() (*Response, error) {
r, err := c.DoAPIDelete(c.teamsRoute() + "/invites/email")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTeamInviteInfo returns a team object from an invite id containing sanitized information.
func (c *Client4) GetTeamInviteInfo(inviteId string) (*Team, *Response, error) {
r, err := c.DoAPIGet(c.teamsRoute()+"/invite/"+inviteId, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var t Team
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
return nil, nil, NewAppError("GetTeamInviteInfo", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &t, BuildResponse(r), nil
}
// SetTeamIcon sets team icon of the team.
func (c *Client4) SetTeamIcon(teamId string, data []byte) (*Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "teamIcon.png")
if err != nil {
return nil, NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = writer.Close(); err != nil {
return nil, NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rq, err := http.NewRequest("POST", c.APIURL+c.teamRoute(teamId)+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
return BuildResponse(rp), nil
}
// GetTeamIcon gets the team icon of the team.
func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.teamRoute(teamId)+"/image", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// RemoveTeamIcon updates LastTeamIconUpdate to 0 which indicates team icon is removed.
func (c *Client4) RemoveTeamIcon(teamId string) (*Response, error) {
r, err := c.DoAPIDelete(c.teamRoute(teamId) + "/image")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Channel Section
// GetAllChannels get all the channels. Must be a system administrator.
func (c *Client4) GetAllChannels(page int, perPage int, etag string) (ChannelListWithTeamData, *Response, error) {
return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{})
}
// GetAllChannelsIncludeDeleted get all the channels. Must be a system administrator.
func (c *Client4) GetAllChannelsIncludeDeleted(page int, perPage int, etag string) (ChannelListWithTeamData, *Response, error) {
return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{IncludeDeleted: true})
}
// GetAllChannelsExcludePolicyConstrained gets all channels which are not part of a data retention policy.
// Must be a system administrator.
func (c *Client4) GetAllChannelsExcludePolicyConstrained(page, perPage int, etag string) (ChannelListWithTeamData, *Response, error) {
return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{ExcludePolicyConstrained: true})
}
func (c *Client4) getAllChannels(page int, perPage int, etag string, opts ChannelSearchOpts) (ChannelListWithTeamData, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted=%v&exclude_policy_constrained=%v",
page, perPage, opts.IncludeDeleted, opts.ExcludePolicyConstrained)
r, err := c.DoAPIGet(c.channelsRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelListWithTeamData
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("getAllChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetAllChannelsWithCount get all the channels including the total count. Must be a system administrator.
func (c *Client4) GetAllChannelsWithCount(page int, perPage int, etag string) (ChannelListWithTeamData, int64, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count="+c.boolString(true), page, perPage)
r, err := c.DoAPIGet(c.channelsRoute()+query, etag)
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
var cwc *ChannelsWithCount
err = json.NewDecoder(r.Body).Decode(&cwc)
if err != nil {
return nil, 0, BuildResponse(r), NewAppError("GetAllChannelsWithCount", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return cwc.Channels, cwc.TotalCount, BuildResponse(r), nil
}
// CreateChannel creates a channel based on the provided channel struct.
func (c *Client4) CreateChannel(channel *Channel) (*Channel, *Response, error) {
channelJSON, err := json.Marshal(channel)
if err != nil {
return nil, nil, NewAppError("CreateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsRoute(), string(channelJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("CreateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// UpdateChannel updates a channel based on the provided channel struct.
func (c *Client4) UpdateChannel(channel *Channel) (*Channel, *Response, error) {
channelJSON, err := json.Marshal(channel)
if err != nil {
return nil, nil, NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(c.channelRoute(channel.Id), string(channelJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// PatchChannel partially updates a channel. Any missing fields are not updated.
func (c *Client4) PatchChannel(channelId string, patch *ChannelPatch) (*Channel, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.channelRoute(channelId)+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("PatchChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// UpdateChannelPrivacy updates channel privacy
func (c *Client4) UpdateChannelPrivacy(channelId string, privacy ChannelType) (*Channel, *Response, error) {
requestBody := map[string]string{"privacy": string(privacy)}
r, err := c.DoAPIPut(c.channelRoute(channelId)+"/privacy", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("UpdateChannelPrivacy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// RestoreChannel restores a previously deleted channel. Any missing fields are not updated.
func (c *Client4) RestoreChannel(channelId string) (*Channel, *Response, error) {
r, err := c.DoAPIPost(c.channelRoute(channelId)+"/restore", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("RestoreChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// CreateDirectChannel creates a direct message channel based on the two user
// ids provided.
func (c *Client4) CreateDirectChannel(userId1, userId2 string) (*Channel, *Response, error) {
requestBody := []string{userId1, userId2}
r, err := c.DoAPIPost(c.channelsRoute()+"/direct", ArrayToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("CreateDirectChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// CreateGroupChannel creates a group message channel based on userIds provided.
func (c *Client4) CreateGroupChannel(userIds []string) (*Channel, *Response, error) {
r, err := c.DoAPIPost(c.channelsRoute()+"/group", ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("CreateGroupChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannel returns a channel based on the provided channel id string.
func (c *Client4) GetChannel(channelId, etag string) (*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelRoute(channelId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelStats returns statistics for a channel.
func (c *Client4) GetChannelStats(channelId string, etag string, excludeFilesCount bool) (*ChannelStats, *Response, error) {
route := c.channelRoute(channelId) + fmt.Sprintf("/stats?exclude_files_count=%v", excludeFilesCount)
r, err := c.DoAPIGet(route, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var stats ChannelStats
if err := json.NewDecoder(r.Body).Decode(&stats); err != nil {
return nil, nil, NewAppError("GetChannelStats", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &stats, BuildResponse(r), nil
}
// GetChannelMembersTimezones gets a list of timezones for a channel.
func (c *Client4) GetChannelMembersTimezones(channelId string) ([]string, *Response, error) {
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/timezones", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
// GetPinnedPosts gets a list of pinned posts.
func (c *Client4) GetPinnedPosts(channelId string, etag string) (*PostList, *Response, error) {
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/pinned", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPinnedPosts", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPrivateChannelsForTeam returns a list of private channels based on the provided team id string.
func (c *Client4) GetPrivateChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response, error) {
query := fmt.Sprintf("/private?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetPrivateChannelsForTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetPublicChannelsForTeam returns a list of public channels based on the provided team id string.
func (c *Client4) GetPublicChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetPublicChannelsForTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetDeletedChannelsForTeam returns a list of public channels based on the provided team id string.
func (c *Client4) GetDeletedChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response, error) {
query := fmt.Sprintf("/deleted?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetDeletedChannelsForTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetPublicChannelsByIdsForTeam returns a list of public channels based on provided team id string.
func (c *Client4) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) ([]*Channel, *Response, error) {
r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/ids", ArrayToJSON(channelIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetPublicChannelsByIdsForTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelsForTeamForUser returns a list channels of on a team for a user.
func (c *Client4) GetChannelsForTeamForUser(teamId, userId string, includeDeleted bool, etag string) ([]*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelsForTeamForUserRoute(teamId, userId, includeDeleted), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelsForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelsForTeamAndUserWithLastDeleteAt returns a list channels of a team for a user, additionally filtered with lastDeleteAt. This does not have any effect if includeDeleted is set to false.
func (c *Client4) GetChannelsForTeamAndUserWithLastDeleteAt(teamId, userId string, includeDeleted bool, lastDeleteAt int, etag string) ([]*Channel, *Response, error) {
route := fmt.Sprintf(c.userRoute(userId) + c.teamRoute(teamId) + "/channels")
route += fmt.Sprintf("?include_deleted=%v&last_delete_at=%d", includeDeleted, lastDeleteAt)
r, err := c.DoAPIGet(route, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelsForTeamAndUserWithLastDeleteAt", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelsForUserWithLastDeleteAt returns a list channels for a user, additionally filtered with lastDeleteAt.
func (c *Client4) GetChannelsForUserWithLastDeleteAt(userID string, lastDeleteAt int) ([]*Channel, *Response, error) {
route := fmt.Sprintf(c.userRoute(userID) + "/channels")
route += fmt.Sprintf("?last_delete_at=%d", lastDeleteAt)
r, err := c.DoAPIGet(route, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelsForUserWithLastDeleteAt", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// SearchChannels returns the channels on a team matching the provided search term.
func (c *Client4) SearchChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response, error) {
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/search", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("SearchChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// SearchArchivedChannels returns the archived channels on a team matching the provided search term.
func (c *Client4) SearchArchivedChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response, error) {
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchArchivedChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/search_archived", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("SearchArchivedChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// SearchAllChannels search in all the channels. Must be a system administrator.
func (c *Client4) SearchAllChannels(search *ChannelSearch) (ChannelListWithTeamData, *Response, error) {
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchAllChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsRoute()+"/search", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelListWithTeamData
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("SearchAllChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// SearchAllChannelsForUser search in all the channels for a regular user.
func (c *Client4) SearchAllChannelsForUser(term string) (ChannelListWithTeamData, *Response, error) {
search := &ChannelSearch{
Term: term,
}
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchAllChannelsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsRoute()+"/search?system_console=false", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelListWithTeamData
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("SearchAllChannelsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// SearchAllChannelsPaged searches all the channels and returns the results paged with the total count.
func (c *Client4) SearchAllChannelsPaged(search *ChannelSearch) (*ChannelsWithCount, *Response, error) {
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchAllChannelsPaged", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsRoute()+"/search", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cwc *ChannelsWithCount
err = json.NewDecoder(r.Body).Decode(&cwc)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetAllChannelsWithCount", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return cwc, BuildResponse(r), nil
}
// SearchGroupChannels returns the group channels of the user whose members' usernames match the search term.
func (c *Client4) SearchGroupChannels(search *ChannelSearch) ([]*Channel, *Response, error) {
searchJSON, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchGroupChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.channelsRoute()+"/group/search", string(searchJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("SearchGroupChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// DeleteChannel deletes channel based on the provided channel id string.
func (c *Client4) DeleteChannel(channelId string) (*Response, error) {
r, err := c.DoAPIDelete(c.channelRoute(channelId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PermanentDeleteChannel deletes a channel based on the provided channel id string.
func (c *Client4) PermanentDeleteChannel(channelId string) (*Response, error) {
r, err := c.DoAPIDelete(c.channelRoute(channelId) + "?permanent=" + c.boolString(true))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// MoveChannel moves the channel to the destination team.
func (c *Client4) MoveChannel(channelId, teamId string, force bool) (*Channel, *Response, error) {
requestBody := map[string]any{
"team_id": teamId,
"force": force,
}
r, err := c.DoAPIPost(c.channelRoute(channelId)+"/move", StringInterfaceToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("MoveChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelByName returns a channel based on the provided channel name and team id strings.
func (c *Client4) GetChannelByName(channelName, teamId string, etag string) (*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelByNameRoute(channelName, teamId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelByName", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelByNameIncludeDeleted returns a channel based on the provided channel name and team id strings. Other then GetChannelByName it will also return deleted channels.
func (c *Client4) GetChannelByNameIncludeDeleted(channelName, teamId string, etag string) (*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelByNameRoute(channelName, teamId)+"?include_deleted="+c.boolString(true), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelByNameIncludeDeleted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelByNameForTeamName returns a channel based on the provided channel name and team name strings.
func (c *Client4) GetChannelByNameForTeamName(channelName, teamName string, etag string) (*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelByNameForTeamNameRoute(channelName, teamName), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelByNameForTeamName", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelByNameForTeamNameIncludeDeleted returns a channel based on the provided channel name and team name strings. Other then GetChannelByNameForTeamName it will also return deleted channels.
func (c *Client4) GetChannelByNameForTeamNameIncludeDeleted(channelName, teamName string, etag string) (*Channel, *Response, error) {
r, err := c.DoAPIGet(c.channelByNameForTeamNameRoute(channelName, teamName)+"?include_deleted="+c.boolString(true), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *Channel
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelByNameForTeamNameIncludeDeleted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelMembers gets a page of channel members specific to a channel.
func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag string) (ChannelMembers, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.channelMembersRoute(channelId)+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelMembers
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelMembersWithTeamData gets a page of all channel members for a user.
func (c *Client4) GetChannelMembersWithTeamData(userID string, page, perPage int) (ChannelMembersWithTeamData, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userRoute(userID)+"/channel_members"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelMembersWithTeamData
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMembersWithTeamData", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelMembersByIds gets the channel members in a channel for a list of user ids.
func (c *Client4) GetChannelMembersByIds(channelId string, userIds []string) (ChannelMembers, *Response, error) {
r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"/ids", ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelMembers
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMembersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelMember gets a channel member.
func (c *Client4) GetChannelMember(channelId, userId, etag string) (*ChannelMember, *Response, error) {
r, err := c.DoAPIGet(c.channelMemberRoute(channelId, userId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *ChannelMember
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelMembersForUser gets all the channel members for a user on a team.
func (c *Client4) GetChannelMembersForUser(userId, teamId, etag string) (ChannelMembers, *Response, error) {
r, err := c.DoAPIGet(fmt.Sprintf(c.userRoute(userId)+"/teams/%v/channels/members", teamId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelMembers
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMembersForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// ViewChannel performs a view action for a user. Synonymous with switching channels or marking channels as read by a user.
func (c *Client4) ViewChannel(userId string, view *ChannelView) (*ChannelViewResponse, *Response, error) {
url := fmt.Sprintf(c.channelsRoute()+"/members/%v/view", userId)
buf, err := json.Marshal(view)
if err != nil {
return nil, nil, NewAppError("ViewChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(url, buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *ChannelViewResponse
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("ViewChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetChannelUnread will return a ChannelUnread object that contains the number of
// unread messages and mentions for a user.
func (c *Client4) GetChannelUnread(channelId, userId string) (*ChannelUnread, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+c.channelRoute(channelId)+"/unread", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *ChannelUnread
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelUnread", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// UpdateChannelRoles will update the roles on a channel for a user.
func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (*Response, error) {
requestBody := map[string]string{"roles": roles}
r, err := c.DoAPIPut(c.channelMemberRoute(channelId, userId)+"/roles", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateChannelMemberSchemeRoles will update the scheme-derived roles on a channel for a user.
func (c *Client4) UpdateChannelMemberSchemeRoles(channelId string, userId string, schemeRoles *SchemeRoles) (*Response, error) {
buf, err := json.Marshal(schemeRoles)
if err != nil {
return nil, NewAppError("UpdateChannelMemberSchemeRoles", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.channelMemberRoute(channelId, userId)+"/schemeRoles", buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateChannelNotifyProps will update the notification properties on a channel for a user.
func (c *Client4) UpdateChannelNotifyProps(channelId, userId string, props map[string]string) (*Response, error) {
r, err := c.DoAPIPut(c.channelMemberRoute(channelId, userId)+"/notify_props", MapToJSON(props))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// AddChannelMember adds user to channel and return a channel member.
func (c *Client4) AddChannelMember(channelId, userId string) (*ChannelMember, *Response, error) {
requestBody := map[string]string{"user_id": userId}
r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *ChannelMember
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("AddChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// AddChannelMemberWithRootId adds user to channel and return a channel member. Post add to channel message has the postRootId.
func (c *Client4) AddChannelMemberWithRootId(channelId, userId, postRootId string) (*ChannelMember, *Response, error) {
requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId}
r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch *ChannelMember
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("AddChannelMemberWithRootId", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// RemoveUserFromChannel will delete the channel member object for a user, effectively removing the user from a channel.
func (c *Client4) RemoveUserFromChannel(channelId, userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.channelMemberRoute(channelId, userId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// AutocompleteChannelsForTeam will return an ordered list of channels autocomplete suggestions.
func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (ChannelList, *Response, error) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+"/autocomplete"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelList
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("AutocompleteChannelsForTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions.
func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (ChannelList, *Response, error) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+"/search_autocomplete"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelList
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("AutocompleteChannelsForTeamForSearch", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// GetTopChannelsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopChannelsForTeamSince(teamId string, timeRange string, page int, perPage int) (*TopChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamId)+"/top/channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topChannels *TopChannelList
if err := json.NewDecoder(r.Body).Decode(&topChannels); err != nil {
return nil, nil, NewAppError("GetTopChannelsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, BuildResponse(r), nil
}
// GetTopChannelsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopChannelsForUserSince(teamId string, timeRange string, page int, perPage int) (*TopChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topChannels *TopChannelList
if err := json.NewDecoder(r.Body).Decode(&topChannels); err != nil {
return nil, nil, NewAppError("GetTopChannelsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, BuildResponse(r), nil
}
// GetTopInactiveChannelsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopInactiveChannelsForTeamSince(teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamId)+"/top/inactive_channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topInactiveChannels *TopInactiveChannelList
if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil {
return nil, nil, NewAppError("GetTopInactiveChannelsForTeamSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topInactiveChannels, BuildResponse(r), nil
}
// GetTopInactiveChannelsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopInactiveChannelsForUserSince(teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/inactive_channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topInactiveChannels *TopInactiveChannelList
if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil {
return nil, nil, NewAppError("GetTopInactiveChannelsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topInactiveChannels, BuildResponse(r), nil
}
// Post Section
// CreatePost creates a post based on the provided post struct.
func (c *Client4) CreatePost(post *Post) (*Post, *Response, error) {
postJSON, err := json.Marshal(post)
if err != nil {
return nil, nil, NewAppError("CreatePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.postsRoute(), string(postJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Post
if r.StatusCode == http.StatusNotModified {
return &p, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("CreatePost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// CreatePostEphemeral creates a ephemeral post based on the provided post struct which is send to the given user id.
func (c *Client4) CreatePostEphemeral(post *PostEphemeral) (*Post, *Response, error) {
postJSON, err := json.Marshal(post)
if err != nil {
return nil, nil, NewAppError("CreatePostEphemeral", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.postsEphemeralRoute(), string(postJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Post
if r.StatusCode == http.StatusNotModified {
return &p, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("CreatePostEphemeral", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// UpdatePost updates a post based on the provided post struct.
func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response, error) {
postJSON, err := json.Marshal(post)
if err != nil {
return nil, nil, NewAppError("UpdatePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(c.postRoute(postId), string(postJSON))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Post
if r.StatusCode == http.StatusNotModified {
return &p, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("UpdatePost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// PatchPost partially updates a post. Any missing fields are not updated.
func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.postRoute(postId)+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Post
if r.StatusCode == http.StatusNotModified {
return &p, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("PatchPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// SetPostUnread marks channel where post belongs as unread on the time of the provided post.
func (c *Client4) SetPostUnread(userId string, postId string, collapsedThreadsSupported bool) (*Response, error) {
b, err := json.Marshal(map[string]bool{"collapsed_threads_supported": collapsedThreadsSupported})
if err != nil {
return nil, NewAppError("SetPostUnread", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.userRoute(userId)+c.postRoute(postId)+"/set_unread", b)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// SetPostReminder creates a post reminder for a given post at a specified time.
// The time needs to be in UTC epoch in seconds. It is always truncated to a
// 5 minute resolution minimum.
func (c *Client4) SetPostReminder(reminder *PostReminder) (*Response, error) {
b, err := json.Marshal(reminder)
if err != nil {
return nil, NewAppError("SetPostReminder", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.userRoute(reminder.UserId)+c.postRoute(reminder.PostId)+"/reminder", b)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PinPost pin a post based on provided post id string.
func (c *Client4) PinPost(postId string) (*Response, error) {
r, err := c.DoAPIPost(c.postRoute(postId)+"/pin", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UnpinPost unpin a post based on provided post id string.
func (c *Client4) UnpinPost(postId string) (*Response, error) {
r, err := c.DoAPIPost(c.postRoute(postId)+"/unpin", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetPost gets a single post.
func (c *Client4) GetPost(postId string, etag string) (*Post, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var post Post
if r.StatusCode == http.StatusNotModified {
return &post, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
return nil, nil, NewAppError("GetPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &post, BuildResponse(r), nil
}
// GetPostIncludeDeleted gets a single post, including deleted.
func (c *Client4) GetPostIncludeDeleted(postId string, etag string) (*Post, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"?include_deleted="+c.boolString(true), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var post Post
if r.StatusCode == http.StatusNotModified {
return &post, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
return nil, nil, NewAppError("GetPostIncludeDeleted", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &post, BuildResponse(r), nil
}
// DeletePost deletes a post from the provided post id string.
func (c *Client4) DeletePost(postId string) (*Response, error) {
r, err := c.DoAPIDelete(c.postRoute(postId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetPostThread gets a post with all the other posts in the same thread.
func (c *Client4) GetPostThread(postId string, etag string, collapsedThreads bool) (*PostList, *Response, error) {
url := c.postRoute(postId) + "/thread"
if collapsedThreads {
url += "?collapsedThreads=true"
}
r, err := c.DoAPIGet(url, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostThread", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostThreadWithOpts gets a post with all the other posts in the same thread.
func (c *Client4) GetPostThreadWithOpts(postID string, etag string, opts GetPostsOptions) (*PostList, *Response, error) {
urlVal := c.postRoute(postID) + "/thread"
values := url.Values{}
if opts.CollapsedThreads {
values.Set("collapsedThreads", "true")
}
if opts.CollapsedThreadsExtended {
values.Set("collapsedThreadsExtended", "true")
}
if opts.SkipFetchThreads {
values.Set("skipFetchThreads", "true")
}
if opts.PerPage != 0 {
values.Set("perPage", strconv.Itoa(opts.PerPage))
}
if opts.FromPost != "" {
values.Set("fromPost", opts.FromPost)
}
if opts.FromCreateAt != 0 {
values.Set("fromCreateAt", strconv.FormatInt(opts.FromCreateAt, 10))
}
if opts.Direction != "" {
values.Set("direction", opts.Direction)
}
urlVal += "?" + values.Encode()
r, err := c.DoAPIGet(urlVal, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostThread", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsForChannel gets a page of posts with an array for ordering for a channel.
func (c *Client4) GetPostsForChannel(channelId string, page, perPage int, etag string, collapsedThreads bool, includeDeleted bool) (*PostList, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
if collapsedThreads {
query += "&collapsedThreads=true"
}
if includeDeleted {
query += "&include_deleted=true"
}
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsForChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsByIds gets a list of posts by taking an array of post ids
func (c *Client4) GetPostsByIds(postIds []string) ([]*Post, *Response, error) {
js, err := json.Marshal(postIds)
if err != nil {
return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.postsRoute()+"/ids", string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Post
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsByIds", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetEditHistoryForPost gets a list of posts by taking a post ids
func (c *Client4) GetEditHistoryForPost(postId string) ([]*Post, *Response, error) {
js, err := json.Marshal(postId)
if err != nil {
return nil, nil, NewAppError("GetEditHistoryForPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIGet(c.postRoute(postId)+"/edit_history", string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Post
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetEditHistoryForPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetFlaggedPostsForUser returns flagged posts of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetFlaggedPostsForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetFlaggedPostsForUserInTeam returns flagged posts in team of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUserInTeam(userId string, teamId string, page int, perPage int) (*PostList, *Response, error) {
if !IsValidId(teamId) {
return nil, nil, NewAppError("GetFlaggedPostsForUserInTeam", "model.client.get_flagged_posts_in_team.missing_parameter.app_error", nil, "", http.StatusBadRequest)
}
query := fmt.Sprintf("?team_id=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetFlaggedPostsForUserInTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetFlaggedPostsForUserInChannel returns flagged posts in channel of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUserInChannel(userId string, channelId string, page int, perPage int) (*PostList, *Response, error) {
if !IsValidId(channelId) {
return nil, nil, NewAppError("GetFlaggedPostsForUserInChannel", "model.client.get_flagged_posts_in_channel.missing_parameter.app_error", nil, "", http.StatusBadRequest)
}
query := fmt.Sprintf("?channel_id=%v&page=%v&per_page=%v", channelId, page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetFlaggedPostsForUserInChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsSince gets posts created after a specified time as Unix time in milliseconds.
func (c *Client4) GetPostsSince(channelId string, time int64, collapsedThreads bool) (*PostList, *Response, error) {
query := fmt.Sprintf("?since=%v", time)
if collapsedThreads {
query += "&collapsedThreads=true"
}
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsAfter gets a page of posts that were posted after the post provided.
func (c *Client4) GetPostsAfter(channelId, postId string, page, perPage int, etag string, collapsedThreads bool, includeDeleted bool) (*PostList, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&after=%v", page, perPage, postId)
if collapsedThreads {
query += "&collapsedThreads=true"
}
if includeDeleted {
query += "&include_deleted=true"
}
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsAfter", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsBefore gets a page of posts that were posted before the post provided.
func (c *Client4) GetPostsBefore(channelId, postId string, page, perPage int, etag string, collapsedThreads bool, includeDeleted bool) (*PostList, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&before=%v", page, perPage, postId)
if collapsedThreads {
query += "&collapsedThreads=true"
}
if includeDeleted {
query += "&include_deleted=true"
}
r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsBefore", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// GetPostsAroundLastUnread gets a list of posts around last unread post by a user in a channel.
func (c *Client4) GetPostsAroundLastUnread(userId, channelId string, limitBefore, limitAfter int, collapsedThreads bool) (*PostList, *Response, error) {
query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter)
if collapsedThreads {
query += "&collapsedThreads=true"
}
r, err := c.DoAPIGet(c.userRoute(userId)+c.channelRoute(channelId)+"/posts/unread"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPostsAroundLastUnread", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// SearchFiles returns any posts with matching terms string.
func (c *Client4) SearchFiles(teamId string, terms string, isOrSearch bool) (*FileInfoList, *Response, error) {
params := SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
}
return c.SearchFilesWithParams(teamId, ¶ms)
}
// SearchFilesWithParams returns any posts with matching terms string.
func (c *Client4) SearchFilesWithParams(teamId string, params *SearchParameter) (*FileInfoList, *Response, error) {
js, err := json.Marshal(params)
if err != nil {
return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/files/search", string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list FileInfoList
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchFilesWithParams", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// SearchPosts returns any posts with matching terms string.
func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response, error) {
params := SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
}
return c.SearchPostsWithParams(teamId, ¶ms)
}
// SearchPostsWithParams returns any posts with matching terms string.
func (c *Client4) SearchPostsWithParams(teamId string, params *SearchParameter) (*PostList, *Response, error) {
js, err := json.Marshal(params)
if err != nil {
return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var route string
if teamId == "" {
route = c.postsRoute() + "/search"
} else {
route = c.teamRoute(teamId) + "/posts/search"
}
r, err := c.DoAPIPost(route, string(js))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PostList
if r.StatusCode == http.StatusNotModified {
return &list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchFilesWithParams", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &list, BuildResponse(r), nil
}
// SearchPostsWithMatches returns any posts with matching terms string, including.
func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response, error) {
requestBody := map[string]any{"terms": terms, "is_or_search": isOrSearch}
var route string
if teamId == "" {
route = c.postsRoute() + "/search"
} else {
route = c.teamRoute(teamId) + "/posts/search"
}
r, err := c.DoAPIPost(route, StringInterfaceToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var psr PostSearchResults
if err := json.NewDecoder(r.Body).Decode(&psr); err != nil {
return nil, nil, NewAppError("SearchPostsWithMatches", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &psr, BuildResponse(r), nil
}
// DoPostAction performs a post action.
func (c *Client4) DoPostAction(postId, actionId string) (*Response, error) {
r, err := c.DoAPIPost(c.postRoute(postId)+"/actions/"+actionId, "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DoPostActionWithCookie performs a post action with extra arguments
func (c *Client4) DoPostActionWithCookie(postId, actionId, selected, cookieStr string) (*Response, error) {
var body []byte
if selected != "" || cookieStr != "" {
var err error
body, err = json.Marshal(DoPostActionRequest{
SelectedOption: selected,
Cookie: cookieStr,
})
if err != nil {
return nil, NewAppError("DoPostActionWithCookie", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
r, err := c.DoAPIPost(c.postRoute(postId)+"/actions/"+actionId, string(body))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTopThreadsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopThreadsForTeamSince(teamId string, timeRange string, page int, perPage int) (*TopThreadList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamId)+"/top/threads"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topThreads *TopThreadList
if err := json.NewDecoder(r.Body).Decode(&topThreads); err != nil {
return nil, nil, NewAppError("GetTopThreadsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreads, BuildResponse(r), nil
}
// GetTopThreadsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopThreadsForUserSince(teamId string, timeRange string, page int, perPage int) (*TopThreadList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/threads"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topThreads *TopThreadList
if err := json.NewDecoder(r.Body).Decode(&topThreads); err != nil {
return nil, nil, NewAppError("GetTopThreadsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreads, BuildResponse(r), nil
}
// OpenInteractiveDialog sends a WebSocket event to a user's clients to
// open interactive dialogs, based on the provided trigger ID and other
// provided data. Used with interactive message buttons, menus and
// slash commands.
func (c *Client4) OpenInteractiveDialog(request OpenDialogRequest) (*Response, error) {
b, err := json.Marshal(request)
if err != nil {
return nil, NewAppError("OpenInteractiveDialog", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost("/actions/dialogs/open", string(b))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// SubmitInteractiveDialog will submit the provided dialog data to the integration
// configured by the URL. Used with the interactive dialogs integration feature.
func (c *Client4) SubmitInteractiveDialog(request SubmitDialogRequest) (*SubmitDialogResponse, *Response, error) {
b, err := json.Marshal(request)
if err != nil {
return nil, nil, NewAppError("SubmitInteractiveDialog", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost("/actions/dialogs/submit", string(b))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resp SubmitDialogResponse
json.NewDecoder(r.Body).Decode(&resp)
return &resp, BuildResponse(r), nil
}
// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post.
// This method is functionally equivalent to Client4.UploadFileAsRequestBody.
func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormField("channel_id")
if err != nil {
return nil, nil, err
}
_, err = io.Copy(part, strings.NewReader(channelId))
if err != nil {
return nil, nil, err
}
part, err = writer.CreateFormFile("files", filename)
if err != nil {
return nil, nil, err
}
_, err = io.Copy(part, bytes.NewBuffer(data))
if err != nil {
return nil, nil, err
}
err = writer.Close()
if err != nil {
return nil, nil, err
}
return c.DoUploadFile(c.filesRoute(), body.Bytes(), writer.FormDataContentType())
}
// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached
// to a post. This method is functionally equivalent to Client4.UploadFile.
func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response, error) {
return c.DoUploadFile(c.filesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data))
}
// GetFile gets the bytes for a file by id.
func (c *Client4) GetFile(fileId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// DownloadFile gets the bytes for a file by id, optionally adding headers to force the browser to download it.
func (c *Client4) DownloadFile(fileId string, download bool) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("?download=%v", download), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("DownloadFile", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetFileThumbnail gets the bytes for a file by id.
func (c *Client4) GetFileThumbnail(fileId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+"/thumbnail", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFileThumbnail", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// DownloadFileThumbnail gets the bytes for a file by id, optionally adding headers to force the browser to download it.
func (c *Client4) DownloadFileThumbnail(fileId string, download bool) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("/thumbnail?download=%v", download), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("DownloadFileThumbnail", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetFileLink gets the public link of a file by id.
func (c *Client4) GetFileLink(fileId string) (string, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+"/link", "")
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["link"], BuildResponse(r), nil
}
// GetFilePreview gets the bytes for a file by id.
func (c *Client4) GetFilePreview(fileId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+"/preview", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFilePreview", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// DownloadFilePreview gets the bytes for a file by id.
func (c *Client4) DownloadFilePreview(fileId string, download bool) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("/preview?download=%v", download), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("DownloadFilePreview", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetFileInfo gets all the file info objects.
func (c *Client4) GetFileInfo(fileId string) (*FileInfo, *Response, error) {
r, err := c.DoAPIGet(c.fileRoute(fileId)+"/info", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var fi FileInfo
if err := json.NewDecoder(r.Body).Decode(&fi); err != nil {
return nil, nil, NewAppError("GetFileInfo", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &fi, BuildResponse(r), nil
}
// GetFileInfosForPost gets all the file info objects attached to a post.
func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"/files/info", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*FileInfo
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetFileInfosForPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetFileInfosForPost gets all the file info objects attached to a post, including deleted
func (c *Client4) GetFileInfosForPostIncludeDeleted(postId string, etag string) ([]*FileInfo, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"/files/info"+"?include_deleted="+c.boolString(true), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*FileInfo
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetFileInfosForPostIncludeDeleted", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// General/System Section
// GenerateSupportPacket downloads the generated support packet
func (c *Client4) GenerateSupportPacket() ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.systemRoute()+"/support_packet", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above.
func (c *Client4) GetPing() (string, *Response, error) {
r, err := c.DoAPIGet(c.systemRoute()+"/ping", "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return StatusUnhealthy, BuildResponse(r), err
}
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["status"], BuildResponse(r), nil
}
// GetPingWithServerStatus will return ok if several basic server health checks
// all pass successfully.
func (c *Client4) GetPingWithServerStatus() (string, *Response, error) {
r, err := c.DoAPIGet(c.systemRoute()+"/ping?get_server_status="+c.boolString(true), "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return StatusUnhealthy, BuildResponse(r), err
}
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["status"], BuildResponse(r), nil
}
// GetPingWithFullServerStatus will return the full status if several basic server
// health checks all pass successfully.
func (c *Client4) GetPingWithFullServerStatus() (map[string]string, *Response, error) {
r, err := c.DoAPIGet(c.systemRoute()+"/ping?get_server_status="+c.boolString(true), "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return map[string]string{"status": StatusUnhealthy}, BuildResponse(r), err
}
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body), BuildResponse(r), nil
}
// TestEmail will attempt to connect to the configured SMTP server.
func (c *Client4) TestEmail(config *Config) (*Response, error) {
buf, err := json.Marshal(config)
if err != nil {
return nil, NewAppError("TestEmail", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.testEmailRoute(), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// TestSiteURL will test the validity of a site URL.
func (c *Client4) TestSiteURL(siteURL string) (*Response, error) {
requestBody := make(map[string]string)
requestBody["site_url"] = siteURL
r, err := c.DoAPIPost(c.testSiteURLRoute(), MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// TestS3Connection will attempt to connect to the AWS S3.
func (c *Client4) TestS3Connection(config *Config) (*Response, error) {
buf, err := json.Marshal(config)
if err != nil {
return nil, NewAppError("TestS3Connection", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.testS3Route(), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetConfig will retrieve the server config with some sanitized items.
func (c *Client4) GetConfig() (*Config, *Response, error) {
r, err := c.DoAPIGet(c.configRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cfg *Config
d := json.NewDecoder(r.Body)
return cfg, BuildResponse(r), d.Decode(&cfg)
}
// ReloadConfig will reload the server configuration.
func (c *Client4) ReloadConfig() (*Response, error) {
r, err := c.DoAPIPost(c.configRoute()+"/reload", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetOldClientConfig will retrieve the parts of the server configuration needed by the
// client, formatted in the old format.
func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response, error) {
r, err := c.DoAPIGet(c.configRoute()+"/client?format=old", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body), BuildResponse(r), nil
}
// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields
// are set to true if the corresponding config setting is set through an environment variable.
// Settings that haven't been set through environment variables will be missing from the map.
func (c *Client4) GetEnvironmentConfig() (map[string]any, *Response, error) {
r, err := c.DoAPIGet(c.configRoute()+"/environment", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return StringInterfaceFromJSON(r.Body), BuildResponse(r), nil
}
// GetOldClientLicense will retrieve the parts of the server license needed by the
// client, formatted in the old format.
func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response, error) {
r, err := c.DoAPIGet(c.licenseRoute()+"/client?format=old", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body), BuildResponse(r), nil
}
// DatabaseRecycle will recycle the connections. Discard current connection and get new one.
func (c *Client4) DatabaseRecycle() (*Response, error) {
r, err := c.DoAPIPost(c.databaseRoute()+"/recycle", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// InvalidateCaches will purge the cache and can affect the performance while is cleaning.
func (c *Client4) InvalidateCaches() (*Response, error) {
r, err := c.DoAPIPost(c.cacheRoute()+"/invalidate", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateConfig will update the server configuration.
func (c *Client4) UpdateConfig(config *Config) (*Config, *Response, error) {
buf, err := json.Marshal(config)
if err != nil {
return nil, nil, NewAppError("UpdateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.configRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cfg *Config
d := json.NewDecoder(r.Body)
return cfg, BuildResponse(r), d.Decode(&cfg)
}
// MigrateConfig will migrate existing config to the new one.
// DEPRECATED: The config migrate API has been moved to be a purely
// mmctl --local endpoint. This method will be removed in a
// future major release.
func (c *Client4) MigrateConfig(from, to string) (*Response, error) {
m := make(map[string]string, 2)
m["from"] = from
m["to"] = to
r, err := c.DoAPIPost(c.configRoute()+"/migrate", MapToJSON(m))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UploadLicenseFile will add a license file to the system.
func (c *Client4) UploadLicenseFile(data []byte) (*Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("license", "test-license.mattermost-license")
if err != nil {
return nil, NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = writer.Close(); err != nil {
return nil, NewAppError("UploadLicenseFile", "model.client.set_profile_user.writer.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rq, err := http.NewRequest("POST", c.APIURL+c.licenseRoute(), bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
return BuildResponse(rp), nil
}
// RemoveLicenseFile will remove the server license it exists. Note that this will
// disable all enterprise features.
func (c *Client4) RemoveLicenseFile() (*Response, error) {
r, err := c.DoAPIDelete(c.licenseRoute())
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetAnalyticsOld will retrieve analytics using the old format. New format is not
// available but the "/analytics" endpoint is reserved for it. The "name" argument is optional
// and defaults to "standard". The "teamId" argument is optional and will limit results
// to a specific team.
func (c *Client4) GetAnalyticsOld(name, teamId string) (AnalyticsRows, *Response, error) {
query := fmt.Sprintf("?name=%v&team_id=%v", name, teamId)
r, err := c.DoAPIGet(c.analyticsRoute()+"/old"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var rows AnalyticsRows
err = json.NewDecoder(r.Body).Decode(&rows)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetAnalyticsOld", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rows, BuildResponse(r), nil
}
// Webhooks Section
// CreateIncomingWebhook creates an incoming webhook for a channel.
func (c *Client4) CreateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response, error) {
buf, err := json.Marshal(hook)
if err != nil {
return nil, nil, NewAppError("CreateIncomingWebhook", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.incomingWebhooksRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var iw IncomingWebhook
if err := json.NewDecoder(r.Body).Decode(&iw); err != nil {
return nil, nil, NewAppError("CreateIncomingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &iw, BuildResponse(r), nil
}
// UpdateIncomingWebhook updates an incoming webhook for a channel.
func (c *Client4) UpdateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response, error) {
buf, err := json.Marshal(hook)
if err != nil {
return nil, nil, NewAppError("UpdateIncomingWebhook", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.incomingWebhookRoute(hook.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var iw IncomingWebhook
if err := json.NewDecoder(r.Body).Decode(&iw); err != nil {
return nil, nil, NewAppError("UpdateIncomingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &iw, BuildResponse(r), nil
}
// GetIncomingWebhooks returns a page of incoming webhooks on the system. Page counting starts at 0.
func (c *Client4) GetIncomingWebhooks(page int, perPage int, etag string) ([]*IncomingWebhook, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.incomingWebhooksRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var iwl []*IncomingWebhook
if r.StatusCode == http.StatusNotModified {
return iwl, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&iwl); err != nil {
return nil, nil, NewAppError("GetIncomingWebhooks", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return iwl, BuildResponse(r), nil
}
// GetIncomingWebhooksForTeam returns a page of incoming webhooks for a team. Page counting starts at 0.
func (c *Client4) GetIncomingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*IncomingWebhook, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
r, err := c.DoAPIGet(c.incomingWebhooksRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var iwl []*IncomingWebhook
if r.StatusCode == http.StatusNotModified {
return iwl, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&iwl); err != nil {
return nil, nil, NewAppError("GetIncomingWebhooksForTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return iwl, BuildResponse(r), nil
}
// GetIncomingWebhook returns an Incoming webhook given the hook ID.
func (c *Client4) GetIncomingWebhook(hookID string, etag string) (*IncomingWebhook, *Response, error) {
r, err := c.DoAPIGet(c.incomingWebhookRoute(hookID), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var iw IncomingWebhook
if r.StatusCode == http.StatusNotModified {
return &iw, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&iw); err != nil {
return nil, nil, NewAppError("GetIncomingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &iw, BuildResponse(r), nil
}
// DeleteIncomingWebhook deletes and Incoming Webhook given the hook ID.
func (c *Client4) DeleteIncomingWebhook(hookID string) (*Response, error) {
r, err := c.DoAPIDelete(c.incomingWebhookRoute(hookID))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// CreateOutgoingWebhook creates an outgoing webhook for a team or channel.
func (c *Client4) CreateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response, error) {
buf, err := json.Marshal(hook)
if err != nil {
return nil, nil, NewAppError("CreateOutgoingWebhook", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.outgoingWebhooksRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ow OutgoingWebhook
if err := json.NewDecoder(r.Body).Decode(&ow); err != nil {
return nil, nil, NewAppError("CreateOutgoingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ow, BuildResponse(r), nil
}
// UpdateOutgoingWebhook creates an outgoing webhook for a team or channel.
func (c *Client4) UpdateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response, error) {
buf, err := json.Marshal(hook)
if err != nil {
return nil, nil, NewAppError("UpdateOutgoingWebhook", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.outgoingWebhookRoute(hook.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ow OutgoingWebhook
if err := json.NewDecoder(r.Body).Decode(&ow); err != nil {
return nil, nil, NewAppError("UpdateOutgoingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ow, BuildResponse(r), nil
}
// GetOutgoingWebhooks returns a page of outgoing webhooks on the system. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooks(page int, perPage int, etag string) ([]*OutgoingWebhook, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var owl []*OutgoingWebhook
if r.StatusCode == http.StatusNotModified {
return owl, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&owl); err != nil {
return nil, nil, NewAppError("GetOutgoingWebhooks", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return owl, BuildResponse(r), nil
}
// GetOutgoingWebhook outgoing webhooks on the system requested by Hook Id.
func (c *Client4) GetOutgoingWebhook(hookId string) (*OutgoingWebhook, *Response, error) {
r, err := c.DoAPIGet(c.outgoingWebhookRoute(hookId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ow OutgoingWebhook
if err := json.NewDecoder(r.Body).Decode(&ow); err != nil {
return nil, nil, NewAppError("GetOutgoingWebhook", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ow, BuildResponse(r), nil
}
// GetOutgoingWebhooksForChannel returns a page of outgoing webhooks for a channel. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooksForChannel(channelId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&channel_id=%v", page, perPage, channelId)
r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var owl []*OutgoingWebhook
if r.StatusCode == http.StatusNotModified {
return owl, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&owl); err != nil {
return nil, nil, NewAppError("GetOutgoingWebhooksForChannel", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return owl, BuildResponse(r), nil
}
// GetOutgoingWebhooksForTeam returns a page of outgoing webhooks for a team. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var owl []*OutgoingWebhook
if r.StatusCode == http.StatusNotModified {
return owl, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&owl); err != nil {
return nil, nil, NewAppError("GetOutgoingWebhooksForTeam", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return owl, BuildResponse(r), nil
}
// RegenOutgoingHookToken regenerate the outgoing webhook token.
func (c *Client4) RegenOutgoingHookToken(hookId string) (*OutgoingWebhook, *Response, error) {
r, err := c.DoAPIPost(c.outgoingWebhookRoute(hookId)+"/regen_token", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ow OutgoingWebhook
if err := json.NewDecoder(r.Body).Decode(&ow); err != nil {
return nil, nil, NewAppError("RegenOutgoingHookToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &ow, BuildResponse(r), nil
}
// DeleteOutgoingWebhook delete the outgoing webhook on the system requested by Hook Id.
func (c *Client4) DeleteOutgoingWebhook(hookId string) (*Response, error) {
r, err := c.DoAPIDelete(c.outgoingWebhookRoute(hookId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Preferences Section
// GetPreferences returns the user's preferences.
func (c *Client4) GetPreferences(userId string) (Preferences, *Response, error) {
r, err := c.DoAPIGet(c.preferencesRoute(userId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var prefs Preferences
if err := json.NewDecoder(r.Body).Decode(&prefs); err != nil {
return nil, nil, NewAppError("GetPreferences", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return prefs, BuildResponse(r), nil
}
// UpdatePreferences saves the user's preferences.
func (c *Client4) UpdatePreferences(userId string, preferences Preferences) (*Response, error) {
buf, err := json.Marshal(preferences)
if err != nil {
return nil, NewAppError("UpdatePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.preferencesRoute(userId), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeletePreferences deletes the user's preferences.
func (c *Client4) DeletePreferences(userId string, preferences Preferences) (*Response, error) {
buf, err := json.Marshal(preferences)
if err != nil {
return nil, NewAppError("DeletePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.preferencesRoute(userId)+"/delete", buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetPreferencesByCategory returns the user's preferences from the provided category string.
func (c *Client4) GetPreferencesByCategory(userId string, category string) (Preferences, *Response, error) {
url := fmt.Sprintf(c.preferencesRoute(userId)+"/%s", category)
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var prefs Preferences
if err := json.NewDecoder(r.Body).Decode(&prefs); err != nil {
return nil, nil, NewAppError("GetPreferencesByCategory", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return prefs, BuildResponse(r), nil
}
// GetPreferenceByCategoryAndName returns the user's preferences from the provided category and preference name string.
func (c *Client4) GetPreferenceByCategoryAndName(userId string, category string, preferenceName string) (*Preference, *Response, error) {
url := fmt.Sprintf(c.preferencesRoute(userId)+"/%s/name/%v", category, preferenceName)
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var pref Preference
if err := json.NewDecoder(r.Body).Decode(&pref); err != nil {
return nil, nil, NewAppError("GetPreferenceByCategoryAndName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &pref, BuildResponse(r), nil
}
// SAML Section
// GetSamlMetadata returns metadata for the SAML configuration.
func (c *Client4) GetSamlMetadata() (string, *Response, error) {
r, err := c.DoAPIGet(c.samlRoute()+"/metadata", "")
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(r.Body)
if err != nil {
return "", BuildResponse(r), err
}
return buf.String(), BuildResponse(r), nil
}
func fileToMultipart(data []byte, filename string) ([]byte, *multipart.Writer, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("certificate", filename)
if err != nil {
return nil, nil, err
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, nil, err
}
if err := writer.Close(); err != nil {
return nil, nil, err
}
return body.Bytes(), writer, nil
}
// UploadSamlIdpCertificate will upload an IDP certificate for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlIdpCertificate(data []byte, filename string) (*Response, error) {
body, writer, err := fileToMultipart(data, filename)
if err != nil {
return nil, NewAppError("UploadSamlIdpCertificate", "model.client.upload_saml_cert.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
_, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/idp", body, writer.FormDataContentType())
return resp, err
}
// UploadSamlPublicCertificate will upload a public certificate for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlPublicCertificate(data []byte, filename string) (*Response, error) {
body, writer, err := fileToMultipart(data, filename)
if err != nil {
return nil, NewAppError("UploadSamlPublicCertificate", "model.client.upload_saml_cert.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
_, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/public", body, writer.FormDataContentType())
return resp, err
}
// UploadSamlPrivateCertificate will upload a private key for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlPrivateCertificate(data []byte, filename string) (*Response, error) {
body, writer, err := fileToMultipart(data, filename)
if err != nil {
return nil, NewAppError("UploadSamlPrivateCertificate", "model.client.upload_saml_cert.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
_, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/private", body, writer.FormDataContentType())
return resp, err
}
// DeleteSamlIdpCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlIdpCertificate() (*Response, error) {
r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/idp")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeleteSamlPublicCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlPublicCertificate() (*Response, error) {
r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/public")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeleteSamlPrivateCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlPrivateCertificate() (*Response, error) {
r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/private")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetSamlCertificateStatus returns metadata for the SAML configuration.
func (c *Client4) GetSamlCertificateStatus() (*SamlCertificateStatus, *Response, error) {
r, err := c.DoAPIGet(c.samlRoute()+"/certificate/status", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var status SamlCertificateStatus
if err := json.NewDecoder(r.Body).Decode(&status); err != nil {
return nil, nil, NewAppError("GetSamlCertificateStatus", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &status, BuildResponse(r), nil
}
func (c *Client4) GetSamlMetadataFromIdp(samlMetadataURL string) (*SamlMetadataResponse, *Response, error) {
requestBody := make(map[string]string)
requestBody["saml_metadata_url"] = samlMetadataURL
r, err := c.DoAPIPost(c.samlRoute()+"/metadatafromidp", MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resp SamlMetadataResponse
if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
return nil, nil, NewAppError("GetSamlMetadataFromIdp", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &resp, BuildResponse(r), nil
}
// ResetSamlAuthDataToEmail resets the AuthData field of SAML users to their Email.
func (c *Client4) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (int64, *Response, error) {
params := map[string]any{
"include_deleted": includeDeleted,
"dry_run": dryRun,
"user_ids": userIDs,
}
b, err := json.Marshal(params)
if err != nil {
return 0, nil, NewAppError("ResetSamlAuthDataToEmail", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.samlRoute()+"/reset_auth_data", b)
if err != nil {
return 0, BuildResponse(r), err
}
defer closeBody(r)
respBody := map[string]int64{}
err = json.NewDecoder(r.Body).Decode(&respBody)
if err != nil {
return 0, BuildResponse(r), NewAppError("Api4.ResetSamlAuthDataToEmail", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return respBody["num_affected"], BuildResponse(r), nil
}
// Compliance Section
// CreateComplianceReport creates an incoming webhook for a channel.
func (c *Client4) CreateComplianceReport(report *Compliance) (*Compliance, *Response, error) {
buf, err := json.Marshal(report)
if err != nil {
return nil, nil, NewAppError("CreateComplianceReport", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.complianceReportsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var comp Compliance
if err := json.NewDecoder(r.Body).Decode(&comp); err != nil {
return nil, nil, NewAppError("CreateComplianceReport", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &comp, BuildResponse(r), nil
}
// GetComplianceReports returns list of compliance reports.
func (c *Client4) GetComplianceReports(page, perPage int) (Compliances, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.complianceReportsRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var comp Compliances
if err := json.NewDecoder(r.Body).Decode(&comp); err != nil {
return nil, nil, NewAppError("GetComplianceReports", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return comp, BuildResponse(r), nil
}
// GetComplianceReport returns a compliance report.
func (c *Client4) GetComplianceReport(reportId string) (*Compliance, *Response, error) {
r, err := c.DoAPIGet(c.complianceReportRoute(reportId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var comp Compliance
if err := json.NewDecoder(r.Body).Decode(&comp); err != nil {
return nil, nil, NewAppError("GetComplianceReport", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &comp, BuildResponse(r), nil
}
// DownloadComplianceReport returns a full compliance report as a file.
func (c *Client4) DownloadComplianceReport(reportId string) ([]byte, *Response, error) {
rq, err := http.NewRequest("GET", c.APIURL+c.complianceReportDownloadRoute(reportId), nil)
if err != nil {
return nil, nil, err
}
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, "BEARER "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
data, err := io.ReadAll(rp.Body)
if err != nil {
return nil, BuildResponse(rp), NewAppError("DownloadComplianceReport", "model.client.read_file.app_error", nil, "", rp.StatusCode).Wrap(err)
}
return data, BuildResponse(rp), nil
}
// Cluster Section
// GetClusterStatus returns the status of all the configured cluster nodes.
func (c *Client4) GetClusterStatus() ([]*ClusterInfo, *Response, error) {
r, err := c.DoAPIGet(c.clusterRoute()+"/status", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*ClusterInfo
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetClusterStatus", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// LDAP Section
// SyncLdap will force a sync with the configured LDAP server.
// If includeRemovedMembers is true, then group members who left or were removed from a
// synced team/channel will be re-joined; otherwise, they will be excluded.
func (c *Client4) SyncLdap(includeRemovedMembers bool) (*Response, error) {
reqBody, err := json.Marshal(map[string]any{
"include_removed_members": includeRemovedMembers,
})
if err != nil {
return nil, NewAppError("SyncLdap", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.ldapRoute()+"/sync", reqBody)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// TestLdap will attempt to connect to the configured LDAP server and return OK if configured
// correctly.
func (c *Client4) TestLdap() (*Response, error) {
r, err := c.DoAPIPost(c.ldapRoute()+"/test", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetLdapGroups retrieves the immediate child groups of the given parent group.
func (c *Client4) GetLdapGroups() ([]*Group, *Response, error) {
path := fmt.Sprintf("%s/groups", c.ldapRoute())
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
responseData := struct {
Count int `json:"count"`
Groups []*Group `json:"groups"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
return nil, BuildResponse(r), NewAppError("Api4.GetLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for i := range responseData.Groups {
responseData.Groups[i].DisplayName = *responseData.Groups[i].Name
}
return responseData.Groups, BuildResponse(r), nil
}
// LinkLdapGroup creates or undeletes a Mattermost group and associates it to the given LDAP group DN.
func (c *Client4) LinkLdapGroup(dn string) (*Group, *Response, error) {
path := fmt.Sprintf("%s/groups/%s/link", c.ldapRoute(), dn)
r, err := c.DoAPIPost(path, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g Group
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("LinkLdapGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &g, BuildResponse(r), nil
}
// UnlinkLdapGroup deletes the Mattermost group associated with the given LDAP group DN.
func (c *Client4) UnlinkLdapGroup(dn string) (*Group, *Response, error) {
path := fmt.Sprintf("%s/groups/%s/link", c.ldapRoute(), dn)
r, err := c.DoAPIDelete(path)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g Group
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("UnlinkLdapGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &g, BuildResponse(r), nil
}
// MigrateIdLdap migrates the LDAP enabled users to given attribute
func (c *Client4) MigrateIdLdap(toAttribute string) (*Response, error) {
r, err := c.DoAPIPost(c.ldapRoute()+"/migrateid", MapToJSON(map[string]string{
"toAttribute": toAttribute,
}))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetGroupsByChannel retrieves the Mattermost Groups associated with a given channel
func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response, error) {
path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.channelRoute(channelId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
responseData := struct {
Groups []*GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
return nil, 0, BuildResponse(r), NewAppError("Api4.GetGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return responseData.Groups, responseData.Count, BuildResponse(r), nil
}
// GetGroupsByTeam retrieves the Mattermost Groups associated with a given team
func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response, error) {
path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.teamRoute(teamId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
responseData := struct {
Groups []*GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
return nil, 0, BuildResponse(r), NewAppError("Api4.GetGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return responseData.Groups, responseData.Count, BuildResponse(r), nil
}
// GetGroupsAssociatedToChannelsByTeam retrieves the Mattermost Groups associated with channels in a given team
func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupSearchOpts) (map[string][]*GroupWithSchemeAdmin, *Response, error) {
path := fmt.Sprintf("%s/groups_by_channels?q=%v&filter_allow_reference=%v", c.teamRoute(teamId), opts.Q, opts.FilterAllowReference)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
responseData := struct {
GroupsAssociatedToChannels map[string][]*GroupWithSchemeAdmin `json:"groups"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
return nil, BuildResponse(r), NewAppError("Api4.GetGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return responseData.GroupsAssociatedToChannels, BuildResponse(r), nil
}
// GetGroups retrieves Mattermost Groups
func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) {
path := fmt.Sprintf(
"%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v",
c.groupsRoute(),
opts.IncludeMemberCount,
opts.NotAssociatedToTeam,
opts.NotAssociatedToChannel,
opts.FilterAllowReference,
opts.Q,
opts.FilterParentTeamPermitted,
opts.Source,
opts.IncludeChannelMemberCount,
opts.IncludeTimezones,
)
if opts.Since > 0 {
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
}
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Group
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetGroups", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetGroupsByUserId retrieves Mattermost Groups for a user
func (c *Client4) GetGroupsByUserId(userId string) ([]*Group, *Response, error) {
path := fmt.Sprintf(
"%s/%v/groups",
c.usersRoute(),
userId,
)
r, err := c.DoAPIGet(path, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Group
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetGroupsByUserId", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
func (c *Client4) MigrateAuthToLdap(fromAuthService string, matchField string, force bool) (*Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/migrate_auth/ldap", StringInterfaceToJSON(map[string]any{
"from": fromAuthService,
"force": force,
"match_field": matchField,
}))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) MigrateAuthToSaml(fromAuthService string, usersMap map[string]string, auto bool) (*Response, error) {
r, err := c.DoAPIPost(c.usersRoute()+"/migrate_auth/saml", StringInterfaceToJSON(map[string]any{
"from": fromAuthService,
"auto": auto,
"matches": usersMap,
}))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UploadLdapPublicCertificate will upload a public certificate for LDAP and set the config to use it.
func (c *Client4) UploadLdapPublicCertificate(data []byte) (*Response, error) {
body, writer, err := fileToMultipart(data, LdapPublicCertificateName)
if err != nil {
return nil, NewAppError("UploadLdapPublicCertificate", "model.client.upload_ldap_cert.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
_, resp, err := c.DoUploadFile(c.ldapRoute()+"/certificate/public", body, writer.FormDataContentType())
return resp, err
}
// UploadLdapPrivateCertificate will upload a private key for LDAP and set the config to use it.
func (c *Client4) UploadLdapPrivateCertificate(data []byte) (*Response, error) {
body, writer, err := fileToMultipart(data, LdapPrivateKeyName)
if err != nil {
return nil, NewAppError("UploadLdapPrivateCertificate", "model.client.upload_Ldap_cert.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
_, resp, err := c.DoUploadFile(c.ldapRoute()+"/certificate/private", body, writer.FormDataContentType())
return resp, err
}
// DeleteLdapPublicCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP.
func (c *Client4) DeleteLdapPublicCertificate() (*Response, error) {
r, err := c.DoAPIDelete(c.ldapRoute() + "/certificate/public")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeleteLDAPPrivateCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP.
func (c *Client4) DeleteLdapPrivateCertificate() (*Response, error) {
r, err := c.DoAPIDelete(c.ldapRoute() + "/certificate/private")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Audits Section
// GetAudits returns a list of audits for the whole system.
func (c *Client4) GetAudits(page int, perPage int, etag string) (Audits, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet("/audits"+query, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var audits Audits
err = json.NewDecoder(r.Body).Decode(&audits)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetAudits", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return audits, BuildResponse(r), nil
}
// Brand Section
// GetBrandImage retrieves the previously uploaded brand image.
func (c *Client4) GetBrandImage() ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.brandRoute()+"/image", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
if r.StatusCode >= 300 {
return nil, BuildResponse(r), AppErrorFromJSON(r.Body)
}
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetBrandImage", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// DeleteBrandImage deletes the brand image for the system.
func (c *Client4) DeleteBrandImage() (*Response, error) {
r, err := c.DoAPIDelete(c.brandRoute() + "/image")
if err != nil {
return BuildResponse(r), err
}
return BuildResponse(r), nil
}
// UploadBrandImage sets the brand image for the system.
func (c *Client4) UploadBrandImage(data []byte) (*Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "brand.png")
if err != nil {
return nil, NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = writer.Close(); err != nil {
return nil, NewAppError("UploadBrandImage", "model.client.set_profile_user.writer.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rq, err := http.NewRequest("POST", c.APIURL+c.brandRoute()+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
return BuildResponse(rp), nil
}
// Logs Section
// GetLogs page of logs as a string array.
func (c *Client4) GetLogs(page, perPage int) ([]string, *Response, error) {
query := fmt.Sprintf("?page=%v&logs_per_page=%v", page, perPage)
r, err := c.DoAPIGet("/logs"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
// PostLog is a convenience Web Service call so clients can log messages into
// the server-side logs. For example we typically log javascript error messages
// into the server-side. It returns the log message if the logging was successful.
func (c *Client4) PostLog(message map[string]string) (map[string]string, *Response, error) {
r, err := c.DoAPIPost("/logs", MapToJSON(message))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body), BuildResponse(r), nil
}
// OAuth Section
// CreateOAuthApp will register a new OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) CreateOAuthApp(app *OAuthApp) (*OAuthApp, *Response, error) {
buf, err := json.Marshal(app)
if err != nil {
return nil, nil, NewAppError("CreateOAuthApp", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.oAuthAppsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var oapp OAuthApp
if err := json.NewDecoder(r.Body).Decode(&oapp); err != nil {
return nil, nil, NewAppError("CreateOAuthApp", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &oapp, BuildResponse(r), nil
}
// UpdateOAuthApp updates a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) UpdateOAuthApp(app *OAuthApp) (*OAuthApp, *Response, error) {
buf, err := json.Marshal(app)
if err != nil {
return nil, nil, NewAppError("UpdateOAuthApp", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.oAuthAppRoute(app.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var oapp OAuthApp
if err := json.NewDecoder(r.Body).Decode(&oapp); err != nil {
return nil, nil, NewAppError("UpdateOAuthApp", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &oapp, BuildResponse(r), nil
}
// GetOAuthApps gets a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthApps(page, perPage int) ([]*OAuthApp, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.oAuthAppsRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*OAuthApp
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetOAuthApps", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetOAuthApp gets a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthApp(appId string) (*OAuthApp, *Response, error) {
r, err := c.DoAPIGet(c.oAuthAppRoute(appId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var oapp OAuthApp
if err := json.NewDecoder(r.Body).Decode(&oapp); err != nil {
return nil, nil, NewAppError("GetOAuthApp", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &oapp, BuildResponse(r), nil
}
// GetOAuthAppInfo gets a sanitized version of a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthAppInfo(appId string) (*OAuthApp, *Response, error) {
r, err := c.DoAPIGet(c.oAuthAppRoute(appId)+"/info", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var oapp OAuthApp
if err := json.NewDecoder(r.Body).Decode(&oapp); err != nil {
return nil, nil, NewAppError("GetOAuthAppInfo", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &oapp, BuildResponse(r), nil
}
// DeleteOAuthApp deletes a registered OAuth 2.0 client application.
func (c *Client4) DeleteOAuthApp(appId string) (*Response, error) {
r, err := c.DoAPIDelete(c.oAuthAppRoute(appId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RegenerateOAuthAppSecret regenerates the client secret for a registered OAuth 2.0 client application.
func (c *Client4) RegenerateOAuthAppSecret(appId string) (*OAuthApp, *Response, error) {
r, err := c.DoAPIPost(c.oAuthAppRoute(appId)+"/regen_secret", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var oapp OAuthApp
if err := json.NewDecoder(r.Body).Decode(&oapp); err != nil {
return nil, nil, NewAppError("RegenerateOAuthAppSecret", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &oapp, BuildResponse(r), nil
}
// GetAuthorizedOAuthAppsForUser gets a page of OAuth 2.0 client applications the user has authorized to use access their account.
func (c *Client4) GetAuthorizedOAuthAppsForUser(userId string, page, perPage int) ([]*OAuthApp, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.userRoute(userId)+"/oauth/apps/authorized"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*OAuthApp
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetAuthorizedOAuthAppsForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// AuthorizeOAuthApp will authorize an OAuth 2.0 client application to access a user's account and provide a redirect link to follow.
func (c *Client4) AuthorizeOAuthApp(authRequest *AuthorizeRequest) (string, *Response, error) {
buf, err := json.Marshal(authRequest)
if err != nil {
return "", BuildResponse(nil), NewAppError("AuthorizeOAuthApp", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIRequestBytes(http.MethodPost, c.URL+"/oauth/authorize", buf, "")
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["redirect"], BuildResponse(r), nil
}
// DeauthorizeOAuthApp will deauthorize an OAuth 2.0 client application from accessing a user's account.
func (c *Client4) DeauthorizeOAuthApp(appId string) (*Response, error) {
requestData := map[string]string{"client_id": appId}
r, err := c.DoAPIRequest(http.MethodPost, c.URL+"/oauth/deauthorize", MapToJSON(requestData), "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint.
func (c *Client4) GetOAuthAccessToken(data url.Values) (*AccessResponse, *Response, error) {
url := c.URL + "/oauth/access_token"
rq, err := http.NewRequest(http.MethodPost, url, strings.NewReader(data.Encode()))
if err != nil {
return nil, nil, err
}
rq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
var ar *AccessResponse
err = json.NewDecoder(rp.Body).Decode(&ar)
if err != nil {
return nil, BuildResponse(rp), NewAppError(url, "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ar, BuildResponse(rp), nil
}
// Elasticsearch Section
// TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured.
// correctly.
func (c *Client4) TestElasticsearch() (*Response, error) {
r, err := c.DoAPIPost(c.elasticsearchRoute()+"/test", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PurgeElasticsearchIndexes immediately deletes all Elasticsearch indexes.
func (c *Client4) PurgeElasticsearchIndexes() (*Response, error) {
r, err := c.DoAPIPost(c.elasticsearchRoute()+"/purge_indexes", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Bleve Section
// PurgeBleveIndexes immediately deletes all Bleve indexes.
func (c *Client4) PurgeBleveIndexes() (*Response, error) {
r, err := c.DoAPIPost(c.bleveRoute()+"/purge_indexes", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Data Retention Section
// GetDataRetentionPolicy will get the current global data retention policy details.
func (c *Client4) GetDataRetentionPolicy() (*GlobalRetentionPolicy, *Response, error) {
r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policy", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p GlobalRetentionPolicy
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("GetDataRetentionPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// GetDataRetentionPolicyByID will get the details for the granular data retention policy with the specified ID.
func (c *Client4) GetDataRetentionPolicyByID(policyID string) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) {
r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p RetentionPolicyWithTeamAndChannelCounts
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("GetDataRetentionPolicyByID", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// GetDataRetentionPoliciesCount will get the total number of granular data retention policies.
func (c *Client4) GetDataRetentionPoliciesCount() (int64, *Response, error) {
type CountBody struct {
TotalCount int64 `json:"total_count"`
}
r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policies_count", "")
if err != nil {
return 0, BuildResponse(r), err
}
var countObj CountBody
err = json.NewDecoder(r.Body).Decode(&countObj)
if err != nil {
return 0, nil, NewAppError("Client4.GetDataRetentionPoliciesCount", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return countObj.TotalCount, BuildResponse(r), nil
}
// GetDataRetentionPolicies will get the current granular data retention policies' details.
func (c *Client4) GetDataRetentionPolicies(page, perPage int) (*RetentionPolicyWithTeamAndChannelCountsList, *Response, error) {
query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage)
r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policies"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p RetentionPolicyWithTeamAndChannelCountsList
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("GetDataRetentionPolicies", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// CreateDataRetentionPolicy will create a new granular data retention policy which will be applied to
// the specified teams and channels. The Id field of `policy` must be empty.
func (c *Client4) CreateDataRetentionPolicy(policy *RetentionPolicyWithTeamAndChannelIDs) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) {
policyJSON, err := json.Marshal(policy)
if err != nil {
return nil, nil, NewAppError("CreateDataRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.dataRetentionRoute()+"/policies", policyJSON)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p RetentionPolicyWithTeamAndChannelCounts
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("CreateDataRetentionPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// DeleteDataRetentionPolicy will delete the granular data retention policy with the specified ID.
func (c *Client4) DeleteDataRetentionPolicy(policyID string) (*Response, error) {
r, err := c.DoAPIDelete(c.dataRetentionPolicyRoute(policyID))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PatchDataRetentionPolicy will patch the granular data retention policy with the specified ID.
// The Id field of `patch` must be non-empty.
func (c *Client4) PatchDataRetentionPolicy(patch *RetentionPolicyWithTeamAndChannelIDs) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) {
patchJSON, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchDataRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPatchBytes(c.dataRetentionPolicyRoute(patch.ID), patchJSON)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p RetentionPolicyWithTeamAndChannelCounts
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("PatchDataRetentionPolicy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
// GetTeamsForRetentionPolicy will get the teams to which the specified policy is currently applied.
func (c *Client4) GetTeamsForRetentionPolicy(policyID string, page, perPage int) (*TeamsWithCount, *Response, error) {
query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage)
r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID)+"/teams"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
var teams *TeamsWithCount
err = json.NewDecoder(r.Body).Decode(&teams)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.GetTeamsForRetentionPolicy", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return teams, BuildResponse(r), nil
}
// SearchTeamsForRetentionPolicy will search the teams to which the specified policy is currently applied.
func (c *Client4) SearchTeamsForRetentionPolicy(policyID string, term string) ([]*Team, *Response, error) {
body, err := json.Marshal(map[string]any{"term": term})
if err != nil {
return nil, nil, NewAppError("SearchTeamsForRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/teams/search", body)
if err != nil {
return nil, BuildResponse(r), err
}
var teams []*Team
err = json.NewDecoder(r.Body).Decode(&teams)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.SearchTeamsForRetentionPolicy", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return teams, BuildResponse(r), nil
}
// AddTeamsToRetentionPolicy will add the specified teams to the granular data retention policy
// with the specified ID.
func (c *Client4) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) (*Response, error) {
body, err := json.Marshal(teamIDs)
if err != nil {
return nil, NewAppError("AddTeamsToRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/teams", body)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RemoveTeamsFromRetentionPolicy will remove the specified teams from the granular data retention policy
// with the specified ID.
func (c *Client4) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) (*Response, error) {
body, err := json.Marshal(teamIDs)
if err != nil {
return nil, NewAppError("RemoveTeamsFromRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIDeleteBytes(c.dataRetentionPolicyRoute(policyID)+"/teams", body)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetChannelsForRetentionPolicy will get the channels to which the specified policy is currently applied.
func (c *Client4) GetChannelsForRetentionPolicy(policyID string, page, perPage int) (*ChannelsWithCount, *Response, error) {
query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage)
r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID)+"/channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
var channels *ChannelsWithCount
err = json.NewDecoder(r.Body).Decode(&channels)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.GetChannelsForRetentionPolicy", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return channels, BuildResponse(r), nil
}
// SearchChannelsForRetentionPolicy will search the channels to which the specified policy is currently applied.
func (c *Client4) SearchChannelsForRetentionPolicy(policyID string, term string) (ChannelListWithTeamData, *Response, error) {
body, err := json.Marshal(map[string]any{"term": term})
if err != nil {
return nil, nil, NewAppError("SearchChannelsForRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/channels/search", body)
if err != nil {
return nil, BuildResponse(r), err
}
var channels ChannelListWithTeamData
err = json.NewDecoder(r.Body).Decode(&channels)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.SearchChannelsForRetentionPolicy", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return channels, BuildResponse(r), nil
}
// AddChannelsToRetentionPolicy will add the specified channels to the granular data retention policy
// with the specified ID.
func (c *Client4) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) (*Response, error) {
body, err := json.Marshal(channelIDs)
if err != nil {
return nil, NewAppError("AddChannelsToRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/channels", body)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RemoveChannelsFromRetentionPolicy will remove the specified channels from the granular data retention policy
// with the specified ID.
func (c *Client4) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) (*Response, error) {
body, err := json.Marshal(channelIDs)
if err != nil {
return nil, NewAppError("RemoveChannelsFromRetentionPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIDeleteBytes(c.dataRetentionPolicyRoute(policyID)+"/channels", body)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTeamPoliciesForUser will get the data retention policies for the teams to which a user belongs.
func (c *Client4) GetTeamPoliciesForUser(userID string, offset, limit int) (*RetentionPolicyForTeamList, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userID)+"/data_retention/team_policies", "")
if err != nil {
return nil, BuildResponse(r), err
}
var teams RetentionPolicyForTeamList
err = json.NewDecoder(r.Body).Decode(&teams)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.GetTeamPoliciesForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return &teams, BuildResponse(r), nil
}
// GetChannelPoliciesForUser will get the data retention policies for the channels to which a user belongs.
func (c *Client4) GetChannelPoliciesForUser(userID string, offset, limit int) (*RetentionPolicyForChannelList, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userID)+"/data_retention/channel_policies", "")
if err != nil {
return nil, BuildResponse(r), err
}
var channels RetentionPolicyForChannelList
err = json.NewDecoder(r.Body).Decode(&channels)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.GetChannelPoliciesForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return &channels, BuildResponse(r), nil
}
// Drafts Sections
// UpsertDraft will create a new draft or update a draft if it already exists
func (c *Client4) UpsertDraft(draft *Draft) (*Draft, *Response, error) {
buf, err := json.Marshal(draft)
if err != nil {
return nil, nil, NewAppError("UpsertDraft", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.draftsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var df Draft
err = json.NewDecoder(r.Body).Decode(&df)
if err != nil {
return nil, nil, NewAppError("UpsertDraft", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &df, BuildResponse(r), err
}
// GetDrafts will get all drafts for a user
func (c *Client4) GetDrafts(userId, teamId string) ([]*Draft, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+c.teamRoute(teamId)+"/drafts", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var drafts []*Draft
err = json.NewDecoder(r.Body).Decode(&drafts)
if err != nil {
return nil, nil, NewAppError("GetDrafts", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return drafts, BuildResponse(r), nil
}
func (c *Client4) DeleteDraft(userId, channelId, rootId string) (*Draft, *Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId) + c.channelRoute(channelId) + "/drafts")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var df *Draft
err = json.NewDecoder(r.Body).Decode(&df)
if err != nil {
return nil, BuildResponse(r), NewAppError("DeleteDraft", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return df, BuildResponse(r), nil
}
// Commands Section
// CreateCommand will create a new command if the user have the right permissions.
func (c *Client4) CreateCommand(cmd *Command) (*Command, *Response, error) {
buf, err := json.Marshal(cmd)
if err != nil {
return nil, nil, NewAppError("CreateCommand", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.commandsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var command Command
if err := json.NewDecoder(r.Body).Decode(&command); err != nil {
return nil, nil, NewAppError("CreateCommand", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &command, BuildResponse(r), nil
}
// UpdateCommand updates a command based on the provided Command struct.
func (c *Client4) UpdateCommand(cmd *Command) (*Command, *Response, error) {
buf, err := json.Marshal(cmd)
if err != nil {
return nil, nil, NewAppError("UpdateCommand", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.commandRoute(cmd.Id), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var command Command
if err := json.NewDecoder(r.Body).Decode(&command); err != nil {
return nil, nil, NewAppError("UpdateCommand", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &command, BuildResponse(r), nil
}
// MoveCommand moves a command to a different team.
func (c *Client4) MoveCommand(teamId string, commandId string) (*Response, error) {
cmr := CommandMoveRequest{TeamId: teamId}
buf, err := json.Marshal(cmr)
if err != nil {
return nil, NewAppError("MoveCommand", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.commandMoveRoute(commandId), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DeleteCommand deletes a command based on the provided command id string.
func (c *Client4) DeleteCommand(commandId string) (*Response, error) {
r, err := c.DoAPIDelete(c.commandRoute(commandId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// ListCommands will retrieve a list of commands available in the team.
func (c *Client4) ListCommands(teamId string, customOnly bool) ([]*Command, *Response, error) {
query := fmt.Sprintf("?team_id=%v&custom_only=%v", teamId, customOnly)
r, err := c.DoAPIGet(c.commandsRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Command
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("ListCommands", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// ListCommandAutocompleteSuggestions will retrieve a list of suggestions for a userInput.
func (c *Client4) ListCommandAutocompleteSuggestions(userInput, teamId string) ([]AutocompleteSuggestion, *Response, error) {
query := fmt.Sprintf("/commands/autocomplete_suggestions?user_input=%v", userInput)
r, err := c.DoAPIGet(c.teamRoute(teamId)+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []AutocompleteSuggestion
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("ListCommandAutocompleteSuggestions", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetCommandById will retrieve a command by id.
func (c *Client4) GetCommandById(cmdId string) (*Command, *Response, error) {
url := fmt.Sprintf("%s/%s", c.commandsRoute(), cmdId)
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var command Command
if err := json.NewDecoder(r.Body).Decode(&command); err != nil {
return nil, nil, NewAppError("GetCommandById", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &command, BuildResponse(r), nil
}
// ExecuteCommand executes a given slash command.
func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *Response, error) {
commandArgs := &CommandArgs{
ChannelId: channelId,
Command: command,
}
buf, err := json.Marshal(commandArgs)
if err != nil {
return nil, nil, NewAppError("ExecuteCommand", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.commandsRoute()+"/execute", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
response, err := CommandResponseFromJSON(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("ExecuteCommand", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return response, BuildResponse(r), nil
}
// ExecuteCommandWithTeam executes a given slash command against the specified team.
// Use this when executing slash commands in a DM/GM, since the team id cannot be inferred in that case.
func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*CommandResponse, *Response, error) {
commandArgs := &CommandArgs{
ChannelId: channelId,
TeamId: teamId,
Command: command,
}
buf, err := json.Marshal(commandArgs)
if err != nil {
return nil, nil, NewAppError("ExecuteCommandWithTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.commandsRoute()+"/execute", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
response, err := CommandResponseFromJSON(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("ExecuteCommandWithTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return response, BuildResponse(r), nil
}
// ListAutocompleteCommands will retrieve a list of commands available in the team.
func (c *Client4) ListAutocompleteCommands(teamId string) ([]*Command, *Response, error) {
r, err := c.DoAPIGet(c.teamAutoCompleteCommandsRoute(teamId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Command
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("ListAutocompleteCommands", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// RegenCommandToken will create a new token if the user have the right permissions.
func (c *Client4) RegenCommandToken(commandId string) (string, *Response, error) {
r, err := c.DoAPIPut(c.commandRoute(commandId)+"/regen_token", "")
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["token"], BuildResponse(r), nil
}
// Status Section
// GetUserStatus returns a user based on the provided user id string.
func (c *Client4) GetUserStatus(userId, etag string) (*Status, *Response, error) {
r, err := c.DoAPIGet(c.userStatusRoute(userId), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s Status
if r.StatusCode == http.StatusNotModified {
return &s, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("GetUserStatus", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetUsersStatusesByIds returns a list of users status based on the provided user ids.
func (c *Client4) GetUsersStatusesByIds(userIds []string) ([]*Status, *Response, error) {
r, err := c.DoAPIPost(c.userStatusesRoute()+"/ids", ArrayToJSON(userIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Status
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsersStatusesByIds", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// UpdateUserStatus sets a user's status based on the provided user id string.
func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, *Response, error) {
buf, err := json.Marshal(userStatus)
if err != nil {
return nil, nil, NewAppError("UpdateUserStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.userStatusRoute(userId), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s Status
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("UpdateUserStatus", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// UpdateUserCustomStatus sets a user's custom status based on the provided user id string.
// The returned CustomStatus object is the same as the one passed, and it should be just
// ignored. It's only kept to maintain compatibility.
func (c *Client4) UpdateUserCustomStatus(userId string, userCustomStatus *CustomStatus) (*CustomStatus, *Response, error) {
buf, err := json.Marshal(userCustomStatus)
if err != nil {
return nil, nil, NewAppError("UpdateUserCustomStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.userStatusRoute(userId)+"/custom", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
// This is returning the same status which was passed.
// The API was incorrectly designed to return a status returned from the server,
// but the server doesn't return anything except an OK.
return userCustomStatus, BuildResponse(r), nil
}
// RemoveUserCustomStatus remove a user's custom status based on the provided user id string.
func (c *Client4) RemoveUserCustomStatus(userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userStatusRoute(userId) + "/custom")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RemoveRecentUserCustomStatus remove a recent user's custom status based on the provided user id string.
func (c *Client4) RemoveRecentUserCustomStatus(userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userStatusRoute(userId) + "/custom/recent")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Emoji Section
// CreateEmoji will save an emoji to the server if the current user has permission
// to do so. If successful, the provided emoji will be returned with its Id field
// filled in. Otherwise, an error will be returned.
func (c *Client4) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", filename)
if err != nil {
return nil, nil, err
}
_, err = io.Copy(part, bytes.NewBuffer(image))
if err != nil {
return nil, nil, err
}
emojiJSON, err := json.Marshal(emoji)
if err != nil {
return nil, nil, NewAppError("CreateEmoji", "api.marshal_error", nil, "", 0).Wrap(err)
}
if err := writer.WriteField("emoji", string(emojiJSON)); err != nil {
return nil, nil, err
}
if err := writer.Close(); err != nil {
return nil, nil, err
}
return c.DoEmojiUploadFile(c.emojisRoute(), body.Bytes(), writer.FormDataContentType())
}
// GetEmojiList returns a page of custom emoji on the system.
func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.emojisRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Emoji
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetEmojiList", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetSortedEmojiList returns a page of custom emoji on the system sorted based on the sort
// parameter, blank for no sorting and "name" to sort by emoji names.
func (c *Client4) GetSortedEmojiList(page, perPage int, sort string) ([]*Emoji, *Response, error) {
query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort)
r, err := c.DoAPIGet(c.emojisRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Emoji
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetSortedEmojiList", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// DeleteEmoji delete an custom emoji on the provided emoji id string.
func (c *Client4) DeleteEmoji(emojiId string) (*Response, error) {
r, err := c.DoAPIDelete(c.emojiRoute(emojiId))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetEmoji returns a custom emoji based on the emojiId string.
func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response, error) {
r, err := c.DoAPIGet(c.emojiRoute(emojiId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var e Emoji
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
return nil, nil, NewAppError("GetEmoji", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &e, BuildResponse(r), nil
}
// GetEmojiByName returns a custom emoji based on the name string.
func (c *Client4) GetEmojiByName(name string) (*Emoji, *Response, error) {
r, err := c.DoAPIGet(c.emojiByNameRoute(name), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var e Emoji
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
return nil, nil, NewAppError("GetEmojiByName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &e, BuildResponse(r), nil
}
// GetEmojiImage returns the emoji image.
func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.emojiRoute(emojiId)+"/image", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetEmojiImage", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// SearchEmoji returns a list of emoji matching some search criteria.
func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response, error) {
buf, err := json.Marshal(search)
if err != nil {
return nil, nil, NewAppError("SearchEmoji", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.emojisRoute()+"/search", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Emoji
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("SearchEmoji", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// AutocompleteEmoji returns a list of emoji starting with or matching name.
func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response, error) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoAPIGet(c.emojisRoute()+"/autocomplete"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Emoji
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("AutocompleteEmoji", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// Reaction Section
// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned.
func (c *Client4) SaveReaction(reaction *Reaction) (*Reaction, *Response, error) {
buf, err := json.Marshal(reaction)
if err != nil {
return nil, nil, NewAppError("SaveReaction", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.reactionsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var re Reaction
if err := json.NewDecoder(r.Body).Decode(&re); err != nil {
return nil, nil, NewAppError("SaveReaction", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &re, BuildResponse(r), nil
}
// GetReactions returns a list of reactions to a post.
func (c *Client4) GetReactions(postId string) ([]*Reaction, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"/reactions", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Reaction
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetReactions", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// DeleteReaction deletes reaction of a user in a post.
func (c *Client4) DeleteReaction(reaction *Reaction) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(reaction.UserId) + c.postRoute(reaction.PostId) + fmt.Sprintf("/reactions/%v", reaction.EmojiName))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// FetchBulkReactions returns a map of postIds and corresponding reactions
func (c *Client4) GetBulkReactions(postIds []string) (map[string][]*Reaction, *Response, error) {
r, err := c.DoAPIPost(c.postsRoute()+"/ids/reactions", ArrayToJSON(postIds))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
reactions := map[string][]*Reaction{}
if err := json.NewDecoder(r.Body).Decode(&reactions); err != nil {
return nil, nil, NewAppError("GetBulkReactions", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return reactions, BuildResponse(r), nil
}
func (c *Client4) GetTopReactionsForTeamSince(teamId string, timeRange string, page int, perPage int) (*TopReactionList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamId)+"/top/reactions"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topReactions *TopReactionList
if err := json.NewDecoder(r.Body).Decode(&topReactions); err != nil {
return nil, nil, NewAppError("GetTopReactionsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactions, BuildResponse(r), nil
}
func (c *Client4) GetTopReactionsForUserSince(teamId string, timeRange string, page int, perPage int) (*TopReactionList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/reactions"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topReactions *TopReactionList
if err := json.NewDecoder(r.Body).Decode(&topReactions); err != nil {
return nil, nil, NewAppError("GetTopReactionsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactions, BuildResponse(r), nil
}
func (c *Client4) GetTopDMsForUserSince(timeRange string, page int, perPage int) (*TopDMList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/dms"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topDMs *TopDMList
if jsonErr := json.NewDecoder(r.Body).Decode(&topDMs); jsonErr != nil {
return nil, nil, NewAppError("GetTopReactionsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topDMs, BuildResponse(r), nil
}
// Timezone Section
// GetSupportedTimezone returns a page of supported timezones on the system.
func (c *Client4) GetSupportedTimezone() ([]string, *Response, error) {
r, err := c.DoAPIGet(c.timezonesRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var timezones []string
json.NewDecoder(r.Body).Decode(&timezones)
return timezones, BuildResponse(r), nil
}
// Open Graph Metadata Section
// OpenGraph return the open graph metadata for a particular url if the site have the metadata.
func (c *Client4) OpenGraph(url string) (map[string]string, *Response, error) {
requestBody := make(map[string]string)
requestBody["url"] = url
r, err := c.DoAPIPost(c.openGraphRoute(), MapToJSON(requestBody))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body), BuildResponse(r), nil
}
// Jobs Section
// GetJob gets a single job.
func (c *Client4) GetJob(id string) (*Job, *Response, error) {
r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/%v", id), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var j Job
if err := json.NewDecoder(r.Body).Decode(&j); err != nil {
return nil, nil, NewAppError("GetJob", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &j, BuildResponse(r), nil
}
// GetJobs gets all jobs, sorted with the job that was created most recently first.
func (c *Client4) GetJobs(page int, perPage int) ([]*Job, *Response, error) {
r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Job
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetJobs", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetJobsByType gets all jobs of a given type, sorted with the job that was created most recently first.
func (c *Client4) GetJobsByType(jobType string, page int, perPage int) ([]*Job, *Response, error) {
r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/type/%v?page=%v&per_page=%v", jobType, page, perPage), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Job
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetJobsByType", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// CreateJob creates a job based on the provided job struct.
func (c *Client4) CreateJob(job *Job) (*Job, *Response, error) {
buf, err := json.Marshal(job)
if err != nil {
return nil, nil, NewAppError("CreateJob", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.jobsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var j Job
if err := json.NewDecoder(r.Body).Decode(&j); err != nil {
return nil, nil, NewAppError("CreateJob", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &j, BuildResponse(r), nil
}
// CancelJob requests the cancellation of the job with the provided Id.
func (c *Client4) CancelJob(jobId string) (*Response, error) {
r, err := c.DoAPIPost(c.jobsRoute()+fmt.Sprintf("/%v/cancel", jobId), "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DownloadJob downloads the results of the job
func (c *Client4) DownloadJob(jobId string) ([]byte, *Response, error) {
r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/%v/download", jobId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// Roles Section
// GetAllRoles returns a list of all the roles.
func (c *Client4) GetAllRoles() ([]*Role, *Response, error) {
r, err := c.DoAPIGet(c.rolesRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Role
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetAllRoles", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetRole gets a single role by ID.
func (c *Client4) GetRole(id string) (*Role, *Response, error) {
r, err := c.DoAPIGet(c.rolesRoute()+fmt.Sprintf("/%v", id), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var role Role
if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
return nil, nil, NewAppError("GetRole", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &role, BuildResponse(r), nil
}
// GetRoleByName gets a single role by Name.
func (c *Client4) GetRoleByName(name string) (*Role, *Response, error) {
r, err := c.DoAPIGet(c.rolesRoute()+fmt.Sprintf("/name/%v", name), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var role Role
if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
return nil, nil, NewAppError("GetRoleByName", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &role, BuildResponse(r), nil
}
// GetRolesByNames returns a list of roles based on the provided role names.
func (c *Client4) GetRolesByNames(roleNames []string) ([]*Role, *Response, error) {
r, err := c.DoAPIPost(c.rolesRoute()+"/names", ArrayToJSON(roleNames))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Role
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetRolesByNames", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// PatchRole partially updates a role in the system. Any missing fields are not updated.
func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchRole", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.rolesRoute()+fmt.Sprintf("/%v/patch", roleId), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var role Role
if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
return nil, nil, NewAppError("PatchRole", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &role, BuildResponse(r), nil
}
// Schemes Section
// CreateScheme creates a new Scheme.
func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response, error) {
buf, err := json.Marshal(scheme)
if err != nil {
return nil, nil, NewAppError("CreateScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.schemesRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s Scheme
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("CreateScheme", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetScheme gets a single scheme by ID.
func (c *Client4) GetScheme(id string) (*Scheme, *Response, error) {
r, err := c.DoAPIGet(c.schemeRoute(id), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s Scheme
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("GetScheme", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetSchemes ets all schemes, sorted with the most recently created first, optionally filtered by scope.
func (c *Client4) GetSchemes(scope string, page int, perPage int) ([]*Scheme, *Response, error) {
r, err := c.DoAPIGet(c.schemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Scheme
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetSchemes", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// DeleteScheme deletes a single scheme by ID.
func (c *Client4) DeleteScheme(id string) (*Response, error) {
r, err := c.DoAPIDelete(c.schemeRoute(id))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// PatchScheme partially updates a scheme in the system. Any missing fields are not updated.
func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response, error) {
buf, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.schemeRoute(id)+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s Scheme
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("PatchScheme", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetTeamsForScheme gets the teams using this scheme, sorted alphabetically by display name.
func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response, error) {
r, err := c.DoAPIGet(c.schemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Team
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetTeamsForScheme", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// GetChannelsForScheme gets the channels using this scheme, sorted alphabetically by display name.
func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response, error) {
r, err := c.DoAPIGet(c.schemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch ChannelList
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelsForScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// Plugin Section
// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin.
func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response, error) {
return c.uploadPlugin(file, false)
}
func (c *Client4) UploadPluginForced(file io.Reader) (*Manifest, *Response, error) {
return c.uploadPlugin(file, true)
}
func (c *Client4) uploadPlugin(file io.Reader, force bool) (*Manifest, *Response, error) {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
if force {
err := writer.WriteField("force", c.boolString(true))
if err != nil {
return nil, nil, err
}
}
part, err := writer.CreateFormFile("plugin", "plugin.tar.gz")
if err != nil {
return nil, nil, err
}
if _, err = io.Copy(part, file); err != nil {
return nil, nil, err
}
if err = writer.Close(); err != nil {
return nil, nil, err
}
rq, err := http.NewRequest("POST", c.APIURL+c.pluginsRoute(), body)
if err != nil {
return nil, nil, err
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if c.AuthToken != "" {
rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HTTPClient.Do(rq)
if err != nil {
return nil, BuildResponse(rp), err
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body)
}
var m Manifest
if err := json.NewDecoder(rp.Body).Decode(&m); err != nil {
return nil, nil, NewAppError("uploadPlugin", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &m, BuildResponse(rp), nil
}
func (c *Client4) InstallPluginFromURL(downloadURL string, force bool) (*Manifest, *Response, error) {
forceStr := c.boolString(force)
url := fmt.Sprintf("%s?plugin_download_url=%s&force=%s", c.pluginsRoute()+"/install_from_url", url.QueryEscape(downloadURL), forceStr)
r, err := c.DoAPIPost(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var m Manifest
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
return nil, nil, NewAppError("InstallPluginFromUrl", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &m, BuildResponse(r), nil
}
// InstallMarketplacePlugin will install marketplace plugin.
func (c *Client4) InstallMarketplacePlugin(request *InstallMarketplacePluginRequest) (*Manifest, *Response, error) {
buf, err := json.Marshal(request)
if err != nil {
return nil, nil, NewAppError("InstallMarketplacePlugin", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.pluginsRoute()+"/marketplace", string(buf))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var m Manifest
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
return nil, nil, NewAppError("InstallMarketplacePlugin", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &m, BuildResponse(r), nil
}
// GetPlugins will return a list of plugin manifests for currently active plugins.
func (c *Client4) GetPlugins() (*PluginsResponse, *Response, error) {
r, err := c.DoAPIGet(c.pluginsRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var resp PluginsResponse
if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
return nil, nil, NewAppError("GetPlugins", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &resp, BuildResponse(r), nil
}
// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting
// to the administrator via the system console.
func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response, error) {
r, err := c.DoAPIGet(c.pluginsRoute()+"/statuses", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list PluginStatuses
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetPluginStatuses", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// RemovePlugin will disable and delete a plugin.
func (c *Client4) RemovePlugin(id string) (*Response, error) {
r, err := c.DoAPIDelete(c.pluginRoute(id))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetWebappPlugins will return a list of plugins that the webapp should download.
func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response, error) {
r, err := c.DoAPIGet(c.pluginsRoute()+"/webapp", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*Manifest
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetWebappPlugins", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// EnablePlugin will enable an plugin installed.
func (c *Client4) EnablePlugin(id string) (*Response, error) {
r, err := c.DoAPIPost(c.pluginRoute(id)+"/enable", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// DisablePlugin will disable an enabled plugin.
func (c *Client4) DisablePlugin(id string) (*Response, error) {
r, err := c.DoAPIPost(c.pluginRoute(id)+"/disable", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetMarketplacePlugins will return a list of plugins that an admin can install.
func (c *Client4) GetMarketplacePlugins(filter *MarketplacePluginFilter) ([]*MarketplacePlugin, *Response, error) {
route := c.pluginsRoute() + "/marketplace"
u, err := url.Parse(route)
if err != nil {
return nil, nil, err
}
filter.ApplyToURL(u)
r, err := c.DoAPIGet(u.String(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
plugins, err := MarketplacePluginsFromReader(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError(route, "model.client.parse_plugins.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return plugins, BuildResponse(r), nil
}
// UpdateChannelScheme will update a channel's scheme.
func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (*Response, error) {
sip := &SchemeIDPatch{SchemeID: &schemeId}
buf, err := json.Marshal(sip)
if err != nil {
return nil, NewAppError("UpdateChannelScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.channelSchemeRoute(channelId), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// UpdateTeamScheme will update a team's scheme.
func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (*Response, error) {
sip := &SchemeIDPatch{SchemeID: &schemeId}
buf, err := json.Marshal(sip)
if err != nil {
return nil, NewAppError("UpdateTeamScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.teamSchemeRoute(teamId), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetRedirectLocation retrieves the value of the 'Location' header of an HTTP response for a given URL.
func (c *Client4) GetRedirectLocation(urlParam, etag string) (string, *Response, error) {
url := fmt.Sprintf("%s?url=%s", c.redirectLocationRoute(), url.QueryEscape(urlParam))
r, err := c.DoAPIGet(url, etag)
if err != nil {
return "", BuildResponse(r), err
}
defer closeBody(r)
return MapFromJSON(r.Body)["location"], BuildResponse(r), nil
}
// SetServerBusy will mark the server as busy, which disables non-critical services for `secs` seconds.
func (c *Client4) SetServerBusy(secs int) (*Response, error) {
url := fmt.Sprintf("%s?seconds=%d", c.serverBusyRoute(), secs)
r, err := c.DoAPIPost(url, "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// ClearServerBusy will mark the server as not busy.
func (c *Client4) ClearServerBusy() (*Response, error) {
r, err := c.DoAPIDelete(c.serverBusyRoute())
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetServerBusy returns the current ServerBusyState including the time when a server marked busy
// will automatically have the flag cleared.
func (c *Client4) GetServerBusy() (*ServerBusyState, *Response, error) {
r, err := c.DoAPIGet(c.serverBusyRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var sbs ServerBusyState
if err := json.NewDecoder(r.Body).Decode(&sbs); err != nil {
return nil, nil, NewAppError("GetServerBusy", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &sbs, BuildResponse(r), nil
}
// RegisterTermsOfServiceAction saves action performed by a user against a specific terms of service.
func (c *Client4) RegisterTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*Response, error) {
url := c.userTermsOfServiceRoute(userId)
data := map[string]any{"termsOfServiceId": termsOfServiceId, "accepted": accepted}
r, err := c.DoAPIPost(url, StringInterfaceToJSON(data))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetTermsOfService fetches the latest terms of service
func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response, error) {
url := c.termsOfServiceRoute()
r, err := c.DoAPIGet(url, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tos TermsOfService
if err := json.NewDecoder(r.Body).Decode(&tos); err != nil {
return nil, nil, NewAppError("GetTermsOfService", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tos, BuildResponse(r), nil
}
// GetUserTermsOfService fetches user's latest terms of service action if the latest action was for acceptance.
func (c *Client4) GetUserTermsOfService(userId, etag string) (*UserTermsOfService, *Response, error) {
url := c.userTermsOfServiceRoute(userId)
r, err := c.DoAPIGet(url, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var u UserTermsOfService
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, nil, NewAppError("GetUserTermsOfService", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &u, BuildResponse(r), nil
}
// CreateTermsOfService creates new terms of service.
func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response, error) {
url := c.termsOfServiceRoute()
data := map[string]any{"text": text}
r, err := c.DoAPIPost(url, StringInterfaceToJSON(data))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var tos TermsOfService
if err := json.NewDecoder(r.Body).Decode(&tos); err != nil {
return nil, nil, NewAppError("CreateTermsOfService", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &tos, BuildResponse(r), nil
}
func (c *Client4) GetGroup(groupID, etag string) (*Group, *Response, error) {
r, err := c.DoAPIGet(c.groupRoute(groupID), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g Group
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("GetGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &g, BuildResponse(r), nil
}
func (c *Client4) CreateGroup(group *Group) (*Group, *Response, error) {
groupJSON, err := json.Marshal(group)
if err != nil {
return nil, nil, NewAppError("CreateGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes("/groups", groupJSON)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Group
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("CreateGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
func (c *Client4) DeleteGroup(groupID string) (*Group, *Response, error) {
r, err := c.DoAPIDelete(c.groupRoute(groupID))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Group
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
return nil, nil, NewAppError("DeleteGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &p, BuildResponse(r), nil
}
func (c *Client4) RestoreGroup(groupID string, etag string) (*Group, *Response, error) {
r, err := c.DoAPIPost(c.groupRoute(groupID)+"/restore", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var p Group
if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil {
return nil, nil, NewAppError("DeleteGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return &p, BuildResponse(r), nil
}
func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Response, error) {
payload, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(c.groupRoute(groupID)+"/patch", string(payload))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g Group
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("PatchGroup", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &g, BuildResponse(r), nil
}
func (c *Client4) UpsertGroupMembers(groupID string, userIds *GroupModifyMembers) ([]*GroupMember, *Response, error) {
payload, err := json.Marshal(userIds)
if err != nil {
return nil, nil, NewAppError("UpsertGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.groupRoute(groupID)+"/members", payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g []*GroupMember
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("UpsertGroupMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return g, BuildResponse(r), nil
}
func (c *Client4) DeleteGroupMembers(groupID string, userIds *GroupModifyMembers) ([]*GroupMember, *Response, error) {
payload, err := json.Marshal(userIds)
if err != nil {
return nil, nil, NewAppError("DeleteGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIDeleteBytes(c.groupRoute(groupID)+"/members", payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var g []*GroupMember
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
return nil, nil, NewAppError("DeleteGroupMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return g, BuildResponse(r), nil
}
func (c *Client4) LinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response, error) {
payload, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("LinkGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
url := fmt.Sprintf("%s/link", c.groupSyncableRoute(groupID, syncableID, syncableType))
r, err := c.DoAPIPost(url, string(payload))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var gs GroupSyncable
if err := json.NewDecoder(r.Body).Decode(&gs); err != nil {
return nil, nil, NewAppError("LinkGroupSyncable", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &gs, BuildResponse(r), nil
}
func (c *Client4) UnlinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType) (*Response, error) {
url := fmt.Sprintf("%s/link", c.groupSyncableRoute(groupID, syncableID, syncableType))
r, err := c.DoAPIDelete(url)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, etag string) (*GroupSyncable, *Response, error) {
r, err := c.DoAPIGet(c.groupSyncableRoute(groupID, syncableID, syncableType), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var gs GroupSyncable
if err := json.NewDecoder(r.Body).Decode(&gs); err != nil {
return nil, nil, NewAppError("GetGroupSyncable", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &gs, BuildResponse(r), nil
}
func (c *Client4) GetGroupSyncables(groupID string, syncableType GroupSyncableType, etag string) ([]*GroupSyncable, *Response, error) {
r, err := c.DoAPIGet(c.groupSyncablesRoute(groupID, syncableType), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*GroupSyncable
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetGroupSyncables", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response, error) {
payload, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(c.groupSyncableRoute(groupID, syncableID, syncableType)+"/patch", string(payload))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var gs GroupSyncable
if err := json.NewDecoder(r.Body).Decode(&gs); err != nil {
return nil, nil, NewAppError("PatchGroupSyncable", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &gs, BuildResponse(r), nil
}
func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response, error) {
groupIDStr := strings.Join(groupIDs, ",")
query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamID)+"/members_minus_group_members"+query, etag)
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
var ugc UsersWithGroupsAndCount
if err := json.NewDecoder(r.Body).Decode(&ugc); err != nil {
return nil, 0, nil, NewAppError("TeamMembersMinusGroupMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ugc.Users, ugc.Count, BuildResponse(r), nil
}
func (c *Client4) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response, error) {
groupIDStr := strings.Join(groupIDs, ",")
query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage)
r, err := c.DoAPIGet(c.channelRoute(channelID)+"/members_minus_group_members"+query, etag)
if err != nil {
return nil, 0, BuildResponse(r), err
}
defer closeBody(r)
var ugc UsersWithGroupsAndCount
if err := json.NewDecoder(r.Body).Decode(&ugc); err != nil {
return nil, 0, nil, NewAppError("ChannelMembersMinusGroupMembers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ugc.Users, ugc.Count, BuildResponse(r), nil
}
func (c *Client4) PatchConfig(config *Config) (*Config, *Response, error) {
buf, err := json.Marshal(config)
if err != nil {
return nil, nil, NewAppError("PatchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.configRoute()+"/patch", buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cfg *Config
d := json.NewDecoder(r.Body)
return cfg, BuildResponse(r), d.Decode(&cfg)
}
func (c *Client4) GetChannelModerations(channelID string, etag string) ([]*ChannelModeration, *Response, error) {
r, err := c.DoAPIGet(c.channelRoute(channelID)+"/moderations", etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*ChannelModeration
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
func (c *Client4) PatchChannelModerations(channelID string, patch []*ChannelModerationPatch) ([]*ChannelModeration, *Response, error) {
payload, err := json.Marshal(patch)
if err != nil {
return nil, nil, NewAppError("PatchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(c.channelRoute(channelID)+"/moderations/patch", string(payload))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*ChannelModeration
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("PatchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
func (c *Client4) GetKnownUsers() ([]string, *Response, error) {
r, err := c.DoAPIGet(c.usersRoute()+"/known", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var userIds []string
json.NewDecoder(r.Body).Decode(&userIds)
return userIds, BuildResponse(r), nil
}
// PublishUserTyping publishes a user is typing websocket event based on the provided TypingRequest.
func (c *Client4) PublishUserTyping(userID string, typingRequest TypingRequest) (*Response, error) {
buf, err := json.Marshal(typingRequest)
if err != nil {
return nil, NewAppError("PublishUserTyping", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.publishUserTypingRoute(userID), buf)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response, error) {
r, err := c.DoAPIGet(c.channelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ch []*ChannelMemberCountByGroup
err = json.NewDecoder(r.Body).Decode(&ch)
if err != nil {
return nil, BuildResponse(r), NewAppError("GetChannelMemberCountsByGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return ch, BuildResponse(r), nil
}
// RequestTrialLicense will request a trial license and install it in the server
func (c *Client4) RequestTrialLicense(users int) (*Response, error) {
b, err := json.Marshal(map[string]any{"users": users, "terms_accepted": true})
if err != nil {
return nil, NewAppError("RequestTrialLicense", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost("/trial-license", string(b))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetGroupStats retrieves stats for a Mattermost Group
func (c *Client4) GetGroupStats(groupID string) (*GroupStats, *Response, error) {
r, err := c.DoAPIGet(c.groupRoute(groupID)+"/stats", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var gs GroupStats
if err := json.NewDecoder(r.Body).Decode(&gs); err != nil {
return nil, nil, NewAppError("GetGroupStats", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &gs, BuildResponse(r), nil
}
func (c *Client4) GetSidebarCategoriesForTeamForUser(userID, teamID, etag string) (*OrderedSidebarCategories, *Response, error) {
route := c.userCategoryRoute(userID, teamID)
r, err := c.DoAPIGet(route, etag)
if err != nil {
return nil, BuildResponse(r), err
}
var cat *OrderedSidebarCategories
err = json.NewDecoder(r.Body).Decode(&cat)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.GetSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return cat, BuildResponse(r), nil
}
func (c *Client4) CreateSidebarCategoryForTeamForUser(userID, teamID string, category *SidebarCategoryWithChannels) (*SidebarCategoryWithChannels, *Response, error) {
payload, err := json.Marshal(category)
if err != nil {
return nil, nil, NewAppError("CreateSidebarCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
route := c.userCategoryRoute(userID, teamID)
r, err := c.DoAPIPostBytes(route, payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cat *SidebarCategoryWithChannels
err = json.NewDecoder(r.Body).Decode(&cat)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.CreateSidebarCategoryForTeamForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return cat, BuildResponse(r), nil
}
func (c *Client4) UpdateSidebarCategoriesForTeamForUser(userID, teamID string, categories []*SidebarCategoryWithChannels) ([]*SidebarCategoryWithChannels, *Response, error) {
payload, err := json.Marshal(categories)
if err != nil {
return nil, nil, NewAppError("UpdateSidebarCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
route := c.userCategoryRoute(userID, teamID)
r, err := c.DoAPIPutBytes(route, payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cat []*SidebarCategoryWithChannels
err = json.NewDecoder(r.Body).Decode(&cat)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return cat, BuildResponse(r), nil
}
func (c *Client4) GetSidebarCategoryOrderForTeamForUser(userID, teamID, etag string) ([]string, *Response, error) {
route := c.userCategoryRoute(userID, teamID) + "/order"
r, err := c.DoAPIGet(route, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
func (c *Client4) UpdateSidebarCategoryOrderForTeamForUser(userID, teamID string, order []string) ([]string, *Response, error) {
payload, err := json.Marshal(order)
if err != nil {
return nil, nil, NewAppError("UpdateSidebarCategoryOrderForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
route := c.userCategoryRoute(userID, teamID) + "/order"
r, err := c.DoAPIPutBytes(route, payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
func (c *Client4) GetSidebarCategoryForTeamForUser(userID, teamID, categoryID, etag string) (*SidebarCategoryWithChannels, *Response, error) {
route := c.userCategoryRoute(userID, teamID) + "/" + categoryID
r, err := c.DoAPIGet(route, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cat *SidebarCategoryWithChannels
err = json.NewDecoder(r.Body).Decode(&cat)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return cat, BuildResponse(r), nil
}
func (c *Client4) UpdateSidebarCategoryForTeamForUser(userID, teamID, categoryID string, category *SidebarCategoryWithChannels) (*SidebarCategoryWithChannels, *Response, error) {
payload, err := json.Marshal(category)
if err != nil {
return nil, nil, NewAppError("UpdateSidebarCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
route := c.userCategoryRoute(userID, teamID) + "/" + categoryID
r, err := c.DoAPIPutBytes(route, payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cat *SidebarCategoryWithChannels
err = json.NewDecoder(r.Body).Decode(&cat)
if err != nil {
return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, "", r.StatusCode).Wrap(err)
}
return cat, BuildResponse(r), nil
}
// CheckIntegrity performs a database integrity check.
func (c *Client4) CheckIntegrity() ([]IntegrityCheckResult, *Response, error) {
r, err := c.DoAPIPost("/integrity", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var results []IntegrityCheckResult
if err := json.NewDecoder(r.Body).Decode(&results); err != nil {
return nil, BuildResponse(r), NewAppError("Api4.CheckIntegrity", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return results, BuildResponse(r), nil
}
func (c *Client4) GetNotices(lastViewed int64, teamId string, client NoticeClientType, clientVersion, locale, etag string) (NoticeMessages, *Response, error) {
url := fmt.Sprintf("/system/notices/%s?lastViewed=%d&client=%s&clientVersion=%s&locale=%s", teamId, lastViewed, client, clientVersion, locale)
r, err := c.DoAPIGet(url, etag)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
notices, err := UnmarshalProductNoticeMessages(r.Body)
if err != nil {
return nil, BuildResponse(r), err
}
return notices, BuildResponse(r), nil
}
func (c *Client4) MarkNoticesViewed(ids []string) (*Response, error) {
r, err := c.DoAPIPut("/system/notices/view", ArrayToJSON(ids))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) CompleteOnboarding(request *CompleteOnboardingRequest) (*Response, error) {
buf, err := json.Marshal(request)
if err != nil {
return nil, NewAppError("CompleteOnboarding", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.systemRoute()+"/onboarding/complete", string(buf))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// CreateUpload creates a new upload session.
func (c *Client4) CreateUpload(us *UploadSession) (*UploadSession, *Response, error) {
buf, err := json.Marshal(us)
if err != nil {
return nil, nil, NewAppError("CreateUpload", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.uploadsRoute(), buf)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s UploadSession
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("CreateUpload", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetUpload returns the upload session for the specified uploadId.
func (c *Client4) GetUpload(uploadId string) (*UploadSession, *Response, error) {
r, err := c.DoAPIGet(c.uploadRoute(uploadId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var s UploadSession
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
return nil, nil, NewAppError("GetUpload", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &s, BuildResponse(r), nil
}
// GetUploadsForUser returns the upload sessions created by the specified
// userId.
func (c *Client4) GetUploadsForUser(userId string) ([]*UploadSession, *Response, error) {
r, err := c.DoAPIGet(c.userRoute(userId)+"/uploads", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*UploadSession
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUploadsForUser", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// UploadData performs an upload. On success it returns
// a FileInfo object.
func (c *Client4) UploadData(uploadId string, data io.Reader) (*FileInfo, *Response, error) {
url := c.uploadRoute(uploadId)
r, err := c.DoAPIRequestReader("POST", c.APIURL+url, data, nil)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var fi FileInfo
if r.StatusCode == http.StatusNoContent {
return nil, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&fi); err != nil {
return nil, nil, NewAppError("UploadData", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &fi, BuildResponse(r), nil
}
func (c *Client4) UpdatePassword(userId, currentPassword, newPassword string) (*Response, error) {
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Cloud Section
func (c *Client4) GetCloudProducts() ([]*Product, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/products", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cloudProducts []*Product
json.NewDecoder(r.Body).Decode(&cloudProducts)
return cloudProducts, BuildResponse(r), nil
}
func (c *Client4) GetSelfHostedProducts() ([]*Product, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/products/selfhosted", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var products []*Product
json.NewDecoder(r.Body).Decode(&products)
return products, BuildResponse(r), nil
}
func (c *Client4) GetProductLimits() (*ProductLimits, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/limits", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var productLimits *ProductLimits
json.NewDecoder(r.Body).Decode(&productLimits)
return productLimits, BuildResponse(r), nil
}
func (c *Client4) CreateCustomerPayment() (*StripeSetupIntent, *Response, error) {
r, err := c.DoAPIPost(c.cloudRoute()+"/payment", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var setupIntent *StripeSetupIntent
json.NewDecoder(r.Body).Decode(&setupIntent)
return setupIntent, BuildResponse(r), nil
}
func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodRequest) (*Response, error) {
json, err := json.Marshal(confirmRequest)
if err != nil {
return nil, NewAppError("ConfirmCustomerPayment", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.cloudRoute()+"/payment/confirm", json)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) RequestCloudTrial(cloudTrialRequest *StartCloudTrialRequest) (*Subscription, *Response, error) {
payload, err := json.Marshal(cloudTrialRequest)
if err != nil {
return nil, nil, NewAppError("RequestCloudTrial", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.cloudRoute()+"/request-trial", payload)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var subscription *Subscription
json.NewDecoder(r.Body).Decode(&subscription)
return subscription, BuildResponse(r), nil
}
func (c *Client4) ValidateWorkspaceBusinessEmail() (*Response, error) {
r, err := c.DoAPIPost(c.cloudRoute()+"/validate-workspace-business-email", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) NotifyAdmin(nr *NotifyAdminToUpgradeRequest) (int, error) {
nrJSON, err := json.Marshal(nr)
if err != nil {
return 0, err
}
r, err := c.DoAPIPost("/users/notify-admin", string(nrJSON))
if err != nil {
return r.StatusCode, err
}
closeBody(r)
return r.StatusCode, nil
}
func (c *Client4) TriggerNotifyAdmin(nr *NotifyAdminToUpgradeRequest) (int, error) {
nrJSON, err := json.Marshal(nr)
if err != nil {
return 0, err
}
r, err := c.DoAPIPost("/users/trigger-notify-admin-posts", string(nrJSON))
if err != nil {
return r.StatusCode, err
}
closeBody(r)
return r.StatusCode, nil
}
func (c *Client4) ValidateBusinessEmail(email *ValidateBusinessEmailRequest) (*Response, error) {
payload, _ := json.Marshal(email)
r, err := c.DoAPIPostBytes(c.cloudRoute()+"/validate-business-email", payload)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/customer", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cloudCustomer *CloudCustomer
json.NewDecoder(r.Body).Decode(&cloudCustomer)
return cloudCustomer, BuildResponse(r), nil
}
func (c *Client4) GetSubscriptionStatus(licenseId string) (*SubscriptionLicenseSelfServeStatusResponse, *Response, error) {
r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/self-serve-status", licenseId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var status *SubscriptionLicenseSelfServeStatusResponse
json.NewDecoder(r.Body).Decode(&status)
return status, BuildResponse(r), nil
}
func (c *Client4) GetSubscription() (*Subscription, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/subscription", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var subscription *Subscription
json.NewDecoder(r.Body).Decode(&subscription)
return subscription, BuildResponse(r), nil
}
func (c *Client4) GetInvoicesForSubscription() ([]*Invoice, *Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/subscription/invoices", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var invoices []*Invoice
json.NewDecoder(r.Body).Decode(&invoices)
return invoices, BuildResponse(r), nil
}
func (c *Client4) UpdateCloudCustomer(customerInfo *CloudCustomerInfo) (*CloudCustomer, *Response, error) {
customerBytes, err := json.Marshal(customerInfo)
if err != nil {
return nil, nil, NewAppError("UpdateCloudCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.cloudRoute()+"/customer", customerBytes)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var customer *CloudCustomer
json.NewDecoder(r.Body).Decode(&customer)
return customer, BuildResponse(r), nil
}
func (c *Client4) UpdateCloudCustomerAddress(address *Address) (*CloudCustomer, *Response, error) {
addressBytes, err := json.Marshal(address)
if err != nil {
return nil, nil, NewAppError("UpdateCloudCustomerAddress", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPutBytes(c.cloudRoute()+"/customer/address", addressBytes)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var customer *CloudCustomer
json.NewDecoder(r.Body).Decode(&customer)
return customer, BuildResponse(r), nil
}
func (c *Client4) BootstrapSelfHostedSignup(req BootstrapSelfHostedSignupRequest) (*BootstrapSelfHostedSignupResponse, *Response, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, nil, NewAppError("BootstrapSelfHostedSignup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.hostedCustomerRoute()+"/bootstrap", reqBytes)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var res *BootstrapSelfHostedSignupResponse
json.NewDecoder(r.Body).Decode(&res)
return res, BuildResponse(r), nil
}
func (c *Client4) ListImports() ([]string, *Response, error) {
r, err := c.DoAPIGet(c.importsRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
func (c *Client4) ListExports() ([]string, *Response, error) {
r, err := c.DoAPIGet(c.exportsRoute(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
return ArrayFromJSON(r.Body), BuildResponse(r), nil
}
func (c *Client4) DeleteExport(name string) (*Response, error) {
r, err := c.DoAPIDelete(c.exportRoute(name))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) DownloadExport(name string, wr io.Writer, offset int64) (int64, *Response, error) {
var headers map[string]string
if offset > 0 {
headers = map[string]string{
HeaderRange: fmt.Sprintf("bytes=%d-", offset),
}
}
r, err := c.DoAPIRequestWithHeaders(http.MethodGet, c.APIURL+c.exportRoute(name), "", headers)
if err != nil {
return 0, BuildResponse(r), err
}
defer closeBody(r)
n, err := io.Copy(wr, r.Body)
if err != nil {
return n, BuildResponse(r), NewAppError("DownloadExport", "model.client.copy.app_error", nil, "", r.StatusCode).Wrap(err)
}
return n, BuildResponse(r), nil
}
func (c *Client4) GetUserThreads(userId, teamId string, options GetUserThreadsOpts) (*Threads, *Response, error) {
v := url.Values{}
if options.Since != 0 {
v.Set("since", fmt.Sprintf("%d", options.Since))
}
if options.Before != "" {
v.Set("before", options.Before)
}
if options.After != "" {
v.Set("after", options.After)
}
if options.PageSize != 0 {
v.Set("per_page", fmt.Sprintf("%d", options.PageSize))
}
if options.Extended {
v.Set("extended", "true")
}
if options.Deleted {
v.Set("deleted", "true")
}
if options.Unread {
v.Set("unread", "true")
}
if options.ThreadsOnly {
v.Set("threadsOnly", "true")
}
if options.TotalsOnly {
v.Set("totalsOnly", "true")
}
url := c.userThreadsRoute(userId, teamId)
if len(v) > 0 {
url += "?" + v.Encode()
}
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var threads Threads
json.NewDecoder(r.Body).Decode(&threads)
return &threads, BuildResponse(r), nil
}
func (c *Client4) GetUserThread(userId, teamId, threadId string, extended bool) (*ThreadResponse, *Response, error) {
url := c.userThreadRoute(userId, teamId, threadId)
if extended {
url += "?extended=true"
}
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var thread ThreadResponse
json.NewDecoder(r.Body).Decode(&thread)
return &thread, BuildResponse(r), nil
}
func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) (*Response, error) {
r, err := c.DoAPIPut(fmt.Sprintf("%s/read", c.userThreadsRoute(userId, teamId)), "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) SetThreadUnreadByPostId(userId, teamId, threadId, postId string) (*ThreadResponse, *Response, error) {
r, err := c.DoAPIPost(fmt.Sprintf("%s/set_unread/%s", c.userThreadRoute(userId, teamId, threadId), postId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var thread ThreadResponse
json.NewDecoder(r.Body).Decode(&thread)
return &thread, BuildResponse(r), nil
}
func (c *Client4) UpdateThreadReadForUser(userId, teamId, threadId string, timestamp int64) (*ThreadResponse, *Response, error) {
r, err := c.DoAPIPut(fmt.Sprintf("%s/read/%d", c.userThreadRoute(userId, teamId, threadId), timestamp), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var thread ThreadResponse
json.NewDecoder(r.Body).Decode(&thread)
return &thread, BuildResponse(r), nil
}
func (c *Client4) UpdateThreadFollowForUser(userId, teamId, threadId string, state bool) (*Response, error) {
var err error
var r *http.Response
if state {
r, err = c.DoAPIPut(c.userThreadRoute(userId, teamId, threadId)+"/following", "")
} else {
r, err = c.DoAPIDelete(c.userThreadRoute(userId, teamId, threadId) + "/following")
}
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) GetAllSharedChannels(teamID string, page, perPage int) ([]*SharedChannel, *Response, error) {
url := fmt.Sprintf("%s/%s?page=%d&per_page=%d", c.sharedChannelsRoute(), teamID, page, perPage)
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var channels []*SharedChannel
json.NewDecoder(r.Body).Decode(&channels)
return channels, BuildResponse(r), nil
}
func (c *Client4) GetRemoteClusterInfo(remoteID string) (RemoteClusterInfo, *Response, error) {
url := fmt.Sprintf("%s/remote_info/%s", c.sharedChannelsRoute(), remoteID)
r, err := c.DoAPIGet(url, "")
if err != nil {
return RemoteClusterInfo{}, BuildResponse(r), err
}
defer closeBody(r)
var rci RemoteClusterInfo
json.NewDecoder(r.Body).Decode(&rci)
return rci, BuildResponse(r), nil
}
func (c *Client4) GetAncillaryPermissions(subsectionPermissions []string) ([]string, *Response, error) {
var returnedPermissions []string
url := fmt.Sprintf("%s/ancillary?subsection_permissions=%s", c.permissionsRoute(), strings.Join(subsectionPermissions, ","))
r, err := c.DoAPIGet(url, "")
if err != nil {
return returnedPermissions, BuildResponse(r), err
}
defer closeBody(r)
json.NewDecoder(r.Body).Decode(&returnedPermissions)
return returnedPermissions, BuildResponse(r), nil
}
func (c *Client4) GetUsersWithInvalidEmails(page, perPage int) ([]*User, *Response, error) {
query := fmt.Sprintf("/invalid_emails?page=%v&per_page=%v", page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []*User
if r.StatusCode == http.StatusNotModified {
return list, BuildResponse(r), nil
}
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
func (c *Client4) GetAppliedSchemaMigrations() ([]AppliedMigration, *Response, error) {
r, err := c.DoAPIGet(c.systemRoute()+"/schema/version", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var list []AppliedMigration
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, BuildResponse(r), nil
}
// Usage Section
// GetPostsUsage returns rounded off total usage of posts for the instance
func (c *Client4) GetPostsUsage() (*PostsUsage, *Response, error) {
r, err := c.DoAPIGet(c.usageRoute()+"/posts", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var usage *PostsUsage
err = json.NewDecoder(r.Body).Decode(&usage)
return usage, BuildResponse(r), err
}
// GetStorageUsage returns the file storage usage for the instance,
// rounded down the most signigicant digit
func (c *Client4) GetStorageUsage() (*StorageUsage, *Response, error) {
r, err := c.DoAPIGet(c.usageRoute()+"/storage", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var usage *StorageUsage
err = json.NewDecoder(r.Body).Decode(&usage)
return usage, BuildResponse(r), err
}
// GetTeamsUsage returns total usage of teams for the instance
func (c *Client4) GetTeamsUsage() (*TeamsUsage, *Response, error) {
r, err := c.DoAPIGet(c.usageRoute()+"/teams", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var usage *TeamsUsage
err = json.NewDecoder(r.Body).Decode(&usage)
return usage, BuildResponse(r), err
}
func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page int, perPage int) (*NewTeamMembersList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.teamRoute(teamID)+"/top/team_members"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var newTeamMembersList *NewTeamMembersList
if jsonErr := json.NewDecoder(r.Body).Decode(&newTeamMembersList); jsonErr != nil {
return nil, nil, NewAppError("GetNewTeamMembersSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return newTeamMembersList, BuildResponse(r), nil
}
func (c *Client4) SelfHostedSignupAvailable() (*Response, error) {
r, err := c.DoAPIGet(c.hostedCustomerRoute()+"/signup_available", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) SelfHostedSignupCustomer(form *SelfHostedCustomerForm) (*Response, *SelfHostedSignupCustomerResponse, error) {
payloadBytes, err := json.Marshal(form)
if err != nil {
return nil, nil, NewAppError("SelfHostedSignupCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/customer", string(payloadBytes))
if err != nil {
return BuildResponse(r), nil, err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
response := SelfHostedSignupCustomerResponse{}
err = json.Unmarshal(data, &response)
if err != nil {
return BuildResponse(r), nil, err
}
return BuildResponse(r), &response, nil
}
func (c *Client4) SelfHostedSignupConfirm(form *SelfHostedConfirmPaymentMethodRequest) (*Response, *SelfHostedSignupConfirmClientResponse, error) {
payloadBytes, err := json.Marshal(form)
if err != nil {
return nil, nil, NewAppError("SelfHostedSignupConfirm", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/confirm", string(payloadBytes))
if err != nil {
return BuildResponse(r), nil, err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
response := SelfHostedSignupConfirmClientResponse{}
err = json.Unmarshal(data, &response)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
return BuildResponse(r), &response, nil
}
func (c *Client4) GetSelfHostedInvoices() (*Response, []*Invoice, error) {
r, err := c.DoAPIGet(c.hostedCustomerRoute()+"/invoices", "")
if err != nil {
return BuildResponse(r), nil, err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
invoices := []*Invoice{}
err = json.Unmarshal(data, &invoices)
if err != nil {
return BuildResponse(r), nil, err
}
defer closeBody(r)
return BuildResponse(r), invoices, nil
}
func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) {
r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var info *PostInfo
if err = json.NewDecoder(r.Body).Decode(&info); err != nil {
return nil, nil, NewAppError("GetPostInfo", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return info, BuildResponse(r), nil
}
func (c *Client4) AcknowledgePost(postId, userId string) (*PostAcknowledgement, *Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+c.postRoute(postId)+"/ack", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ack *PostAcknowledgement
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
return nil, nil, NewAppError("AcknowledgePost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return ack, BuildResponse(r), nil
}
func (c *Client4) UnacknowledgePost(postId, userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId) + c.postRoute(postId) + "/ack")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) {
r, err := c.DoAPIPost(c.ldapRoute()+"/users/"+userID+"/group_sync_memberships", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) CheckCWSConnection(userId string) (*Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/healthz", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Worktemplates sections
func (c *Client4) worktemplatesRoute() string {
return "/worktemplates"
}
// GetWorktemplateCategories returns categories of worktemplates
func (c *Client4) GetWorktemplateCategories() ([]*WorkTemplateCategory, *Response, error) {
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var categories []*WorkTemplateCategory
err = json.NewDecoder(r.Body).Decode(&categories)
return categories, BuildResponse(r), err
}
func (c *Client4) GetWorkTemplatesByCategory(category string) ([]*WorkTemplate, *Response, error) {
r, err := c.DoAPIGet(c.worktemplatesRoute()+"/categories/"+category+"/templates", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var templates []*WorkTemplate
err = json.NewDecoder(r.Body).Decode(&templates)
return templates, BuildResponse(r), err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"strings"
)
const (
EventTypeFailedPayment = "failed-payment"
EventTypeFailedPaymentNoCard = "failed-payment-no-card"
EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email"
EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email"
EventTypeSubscriptionChanged = "subscription-changed"
EventTypeTriggerDelinquencyEmail = "trigger-delinquency-email"
)
const UpcomingInvoice = "upcoming"
var MockCWS string
type BillingScheme string
const (
BillingSchemePerSeat = BillingScheme("per_seat")
BillingSchemeFlatFee = BillingScheme("flat_fee")
BillingSchemeSalesServe = BillingScheme("sales_serve")
)
type RecurringInterval string
const (
RecurringIntervalYearly = RecurringInterval("year")
RecurringIntervalMonthly = RecurringInterval("month")
)
type SubscriptionFamily string
const (
SubscriptionFamilyCloud = SubscriptionFamily("cloud")
SubscriptionFamilyOnPrem = SubscriptionFamily("on-prem")
)
type ProductSku string
const (
SkuStarterGov = ProductSku("starter-gov")
SkuProfessionalGov = ProductSku("professional-gov")
SkuEnterpriseGov = ProductSku("enterprise-gov")
SkuStarter = ProductSku("starter")
SkuProfessional = ProductSku("professional")
SkuEnterprise = ProductSku("enterprise")
SkuCloudStarter = ProductSku("cloud-starter")
SkuCloudProfessional = ProductSku("cloud-professional")
SkuCloudEnterprise = ProductSku("cloud-enterprise")
)
// Product model represents a product on the cloud system.
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PricePerSeat float64 `json:"price_per_seat"`
AddOns []*AddOn `json:"add_ons"`
SKU string `json:"sku"`
PriceID string `json:"price_id"`
Family SubscriptionFamily `json:"product_family"`
RecurringInterval RecurringInterval `json:"recurring_interval"`
BillingScheme BillingScheme `json:"billing_scheme"`
CrossSellsTo string `json:"cross_sells_to"`
}
type UserFacingProduct struct {
ID string `json:"id"`
Name string `json:"name"`
SKU string `json:"sku"`
PricePerSeat float64 `json:"price_per_seat"`
RecurringInterval RecurringInterval `json:"recurring_interval"`
CrossSellsTo string `json:"cross_sells_to"`
}
// AddOn represents an addon to a product.
type AddOn struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
PricePerSeat float64 `json:"price_per_seat"`
}
// StripeSetupIntent represents the SetupIntent model from Stripe for updating payment methods.
type StripeSetupIntent struct {
ID string `json:"id"`
ClientSecret string `json:"client_secret"`
}
// ConfirmPaymentMethodRequest contains the fields for the customer payment update API.
type ConfirmPaymentMethodRequest struct {
StripeSetupIntentID string `json:"stripe_setup_intent_id"`
SubscriptionID string `json:"subscription_id"`
}
// Customer model represents a customer on the system.
type CloudCustomer struct {
CloudCustomerInfo
ID string `json:"id"`
CreatorID string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
BillingAddress *Address `json:"billing_address"`
CompanyAddress *Address `json:"company_address"`
PaymentMethod *PaymentMethod `json:"payment_method"`
}
type StartCloudTrialRequest struct {
Email string `json:"email"`
SubscriptionID string `json:"subscription_id"`
}
type ValidateBusinessEmailRequest struct {
Email string `json:"email"`
}
type ValidateBusinessEmailResponse struct {
IsValid bool `json:"is_valid"`
}
type SubscriptionLicenseSelfServeStatusResponse struct {
IsExpandable bool `json:"is_expandable"`
IsRenewable bool `json:"is_renewable"`
}
// CloudCustomerInfo represents editable info of a customer.
type CloudCustomerInfo struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
ContactFirstName string `json:"contact_first_name,omitempty"`
ContactLastName string `json:"contact_last_name,omitempty"`
NumEmployees int `json:"num_employees"`
CloudAltPaymentMethod string `json:"monthly_subscription_alt_payment_method"`
}
// Address model represents a customer's address.
type Address struct {
City string `json:"city"`
Country string `json:"country"`
Line1 string `json:"line1"`
Line2 string `json:"line2"`
PostalCode string `json:"postal_code"`
State string `json:"state"`
}
// PaymentMethod represents methods of payment for a customer.
type PaymentMethod struct {
Type string `json:"type"`
LastFour string `json:"last_four"`
ExpMonth int `json:"exp_month"`
ExpYear int `json:"exp_year"`
CardBrand string `json:"card_brand"`
Name string `json:"name"`
}
// Subscription model represents a subscription on the system.
type Subscription struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
ProductID string `json:"product_id"`
AddOns []string `json:"add_ons"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
CreateAt int64 `json:"create_at"`
Seats int `json:"seats"`
Status string `json:"status"`
DNS string `json:"dns"`
LastInvoice *Invoice `json:"last_invoice"`
UpcomingInvoice *Invoice `json:"upcoming_invoice"`
IsFreeTrial string `json:"is_free_trial"`
TrialEndAt int64 `json:"trial_end_at"`
DelinquentSince *int64 `json:"delinquent_since"`
OriginallyLicensedSeats int `json:"originally_licensed_seats"`
ComplianceBlocked string `json:"compliance_blocked"`
}
// Subscription History model represents true up event in a yearly subscription
type SubscriptionHistory struct {
ID string `json:"id"`
SubscriptionID string `json:"subscription_id"`
Seats int `json:"seats"`
CreateAt int64 `json:"create_at"`
}
type SubscriptionHistoryChange struct {
SubscriptionID string `json:"subscription_id"`
Seats int `json:"seats"`
CreateAt int64 `json:"create_at"`
}
// GetWorkSpaceNameFromDNS returns the work space name. For example from test.mattermost.cloud.com, it returns test
func (s *Subscription) GetWorkSpaceNameFromDNS() string {
return strings.Split(s.DNS, ".")[0]
}
// Invoice model represents a cloud invoice
type Invoice struct {
ID string `json:"id"`
Number string `json:"number"`
CreateAt int64 `json:"create_at"`
Total int64 `json:"total"`
Tax int64 `json:"tax"`
Status string `json:"status"`
Description string `json:"description"`
PeriodStart int64 `json:"period_start"`
PeriodEnd int64 `json:"period_end"`
SubscriptionID string `json:"subscription_id"`
Items []*InvoiceLineItem `json:"line_items"`
CurrentProductName string `json:"current_product_name"`
}
// InvoiceLineItem model represents a cloud invoice lineitem tied to an invoice.
type InvoiceLineItem struct {
PriceID string `json:"price_id"`
Total int64 `json:"total"`
Quantity float64 `json:"quantity"`
PricePerUnit int64 `json:"price_per_unit"`
Description string `json:"description"`
Type string `json:"type"`
Metadata map[string]any `json:"metadata"`
}
type DelinquencyEmailTrigger struct {
EmailToTrigger string `json:"email_to_send"`
}
type DelinquencyEmail string
const (
DelinquencyEmail7 DelinquencyEmail = "7"
DelinquencyEmail14 DelinquencyEmail = "14"
DelinquencyEmail30 DelinquencyEmail = "30"
DelinquencyEmail45 DelinquencyEmail = "45"
DelinquencyEmail60 DelinquencyEmail = "60"
DelinquencyEmail75 DelinquencyEmail = "75"
DelinquencyEmail90 DelinquencyEmail = "90"
)
type CWSWebhookPayload struct {
Event string `json:"event"`
FailedPayment *FailedPayment `json:"failed_payment"`
CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
ProductLimits *ProductLimits `json:"product_limits"`
Subscription *Subscription `json:"subscription"`
SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
DelinquencyEmail *DelinquencyEmailTrigger `json:"delinquency_email"`
}
type FailedPayment struct {
CardBrand string `json:"card_brand"`
LastFour string `json:"last_four"`
FailureMessage string `json:"failure_message"`
}
// CloudWorkspaceOwner is part of the CWS Webhook payload that contains information about the user that created the workspace from the CWS
type CloudWorkspaceOwner struct {
UserName string `json:"username"`
}
type SubscriptionChange struct {
ProductID string `json:"product_id"`
Seats int `json:"seats"`
Feedback *Feedback `json:"downgrade_feedback"`
ShippingAddress *Address `json:"shipping_address"`
}
// TODO remove BoardsLimits.
// It is not used for real.
// Focalboard has some lingering code using this struct
// https://github.com/mattermost/mattermost-server/v6/server/boards/blob/fd4cf95f8ac9ba616864b25bf91bb1e4ec21335a/server/app/cloud.go#L86
// we should remove this struct once that code is removed.
type BoardsLimits struct {
Cards *int `json:"cards"`
Views *int `json:"views"`
}
type FilesLimits struct {
TotalStorage *int64 `json:"total_storage"`
}
type MessagesLimits struct {
History *int `json:"history"`
}
type TeamsLimits struct {
Active *int `json:"active"`
}
type ProductLimits struct {
// TODO remove Boards property.
// It is not used for real.
// Focalboard has some lingering code using this property
// https://github.com/mattermost/mattermost-server/v6/server/boards/blob/fd4cf95f8ac9ba616864b25bf91bb1e4ec21335a/server/app/cloud.go#L86
// we should remove this property once that code is removed.
Boards *BoardsLimits `json:"boards,omitempty"`
Files *FilesLimits `json:"files,omitempty"`
Messages *MessagesLimits `json:"messages,omitempty"`
Teams *TeamsLimits `json:"teams,omitempty"`
}
// CreateSubscriptionRequest is the parameters for the API request to create a subscription.
type CreateSubscriptionRequest struct {
ProductID string `json:"product_id"`
AddOns []string `json:"add_ons"`
Seats int `json:"seats"`
Total float64 `json:"total"`
InternalPurchaseOrder string `json:"internal_purchase_order"`
DiscountID string `json:"discount_id"`
}
type Feedback struct {
Reason string `json:"reason"`
Comments string `json:"comments"`
}
type WorkspaceDeletionRequest struct {
SubscriptionID string `json:"subscription_id"`
Feedback *Feedback `json:"delete_feedback"`
}
func (p *Product) IsYearly() bool {
return p.RecurringInterval == RecurringIntervalYearly
}
func (p *Product) IsMonthly() bool {
return p.RecurringInterval == RecurringIntervalMonthly
}
func (df *Feedback) ToMap() map[string]any {
var res map[string]any
feedback, err := json.Marshal(df)
if err != nil {
return res
}
err = json.Unmarshal(feedback, &res)
if err != nil {
return res
}
return res
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"os"
)
const (
CDSOfflineAfterMillis = 1000 * 60 * 30 // 30 minutes
CDSTypeApp = "mattermost_app"
)
type ClusterDiscovery struct {
Id string `json:"id"`
Type string `json:"type"`
ClusterName string `json:"cluster_name"`
Hostname string `json:"hostname"`
GossipPort int32 `json:"gossip_port"`
Port int32 `json:"port"`
CreateAt int64 `json:"create_at"`
LastPingAt int64 `json:"last_ping_at"`
}
func (o *ClusterDiscovery) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
o.LastPingAt = o.CreateAt
}
}
func (o *ClusterDiscovery) AutoFillHostname() {
// attempt to set the hostname from the OS
if o.Hostname == "" {
if hn, err := os.Hostname(); err == nil {
o.Hostname = hn
}
}
}
func (o *ClusterDiscovery) AutoFillIPAddress(iface string, ipAddress string) {
// attempt to set the hostname to the first non-local IP address
if o.Hostname == "" {
if ipAddress != "" {
o.Hostname = ipAddress
} else {
o.Hostname = GetServerIPAddress(iface)
}
}
}
func (o *ClusterDiscovery) IsEqual(in *ClusterDiscovery) bool {
if in == nil {
return false
}
if o.Type != in.Type {
return false
}
if o.ClusterName != in.ClusterName {
return false
}
if o.Hostname != in.Hostname {
return false
}
return true
}
func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bool) []*ClusterDiscovery {
copy := make([]*ClusterDiscovery, 0)
for _, v := range vs {
if f(v) {
copy = append(copy, v)
}
}
return copy
}
func (o *ClusterDiscovery) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.ClusterName == "" {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.name.app_error", nil, "", http.StatusBadRequest)
}
if o.Type == "" {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.type.app_error", nil, "", http.StatusBadRequest)
}
if o.Hostname == "" {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.hostname.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if o.LastPingAt == 0 {
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.last_ping_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"strings"
)
const (
CommandMethodPost = "P"
CommandMethodGet = "G"
MinTriggerLength = 1
MaxTriggerLength = 128
)
type Command struct {
Id string `json:"id"`
Token string `json:"token"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
CreatorId string `json:"creator_id"`
TeamId string `json:"team_id"`
Trigger string `json:"trigger"`
Method string `json:"method"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
AutoComplete bool `json:"auto_complete"`
AutoCompleteDesc string `json:"auto_complete_desc"`
AutoCompleteHint string `json:"auto_complete_hint"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
URL string `json:"url"`
// PluginId records the id of the plugin that created this Command. If it is blank, the Command
// was not created by a plugin.
PluginId string `json:"plugin_id"`
AutocompleteData *AutocompleteData `db:"-" json:"autocomplete_data,omitempty"`
// AutocompleteIconData is a base64 encoded svg
AutocompleteIconData string `db:"-" json:"autocomplete_icon_data,omitempty"`
}
func (o *Command) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
"creator_id": o.CreatorId,
"team_id": o.TeamId,
"trigger": o.Trigger,
"username": o.Username,
"icon_url": o.IconURL,
"auto_complete": o.AutoComplete,
"auto_complete_desc": o.AutoCompleteDesc,
"auto_complete_hint": o.AutoCompleteHint,
"display_name": o.DisplayName,
"description": o.Description,
"url": o.URL,
}
}
func (o *Command) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("Command.IsValid", "model.command.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Token) != 26 {
return NewAppError("Command.IsValid", "model.command.is_valid.token.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Command.IsValid", "model.command.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Command.IsValid", "model.command.is_valid.update_at.app_error", nil, "", http.StatusBadRequest)
}
// If the CreatorId is blank, this should be a command created by a plugin.
if o.CreatorId == "" && !IsValidPluginId(o.PluginId) {
return NewAppError("Command.IsValid", "model.command.is_valid.plugin_id.app_error", nil, "", http.StatusBadRequest)
}
// If the PluginId is blank, this should be a command associated with a userId.
if o.PluginId == "" && !IsValidId(o.CreatorId) {
return NewAppError("Command.IsValid", "model.command.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreatorId != "" && o.PluginId != "" {
return NewAppError("Command.IsValid", "model.command.is_valid.plugin_id.app_error", nil, "command cannot have both a CreatorId and a PluginId", http.StatusBadRequest)
}
if !IsValidId(o.TeamId) {
return NewAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Trigger) < MinTriggerLength || len(o.Trigger) > MaxTriggerLength || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") {
return NewAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "", http.StatusBadRequest)
}
if o.URL == "" || len(o.URL) > 1024 {
return NewAppError("Command.IsValid", "model.command.is_valid.url.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidHTTPURL(o.URL) {
return NewAppError("Command.IsValid", "model.command.is_valid.url_http.app_error", nil, "", http.StatusBadRequest)
}
if !(o.Method == CommandMethodGet || o.Method == CommandMethodPost) {
return NewAppError("Command.IsValid", "model.command.is_valid.method.app_error", nil, "", http.StatusBadRequest)
}
if len(o.DisplayName) > 64 {
return NewAppError("Command.IsValid", "model.command.is_valid.display_name.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Description) > 128 {
return NewAppError("Command.IsValid", "model.command.is_valid.description.app_error", nil, "", http.StatusBadRequest)
}
if o.AutocompleteData != nil {
if err := o.AutocompleteData.IsValid(); err != nil {
return NewAppError("Command.IsValid", "model.command.is_valid.autocomplete_data.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
return nil
}
func (o *Command) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.Token == "" {
o.Token = NewId()
}
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
}
func (o *Command) PreUpdate() {
o.UpdateAt = GetMillis()
}
func (o *Command) Sanitize() {
o.Token = ""
o.CreatorId = ""
o.Method = ""
o.URL = ""
o.Username = ""
o.IconURL = ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type CommandArgs struct {
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
RootId string `json:"root_id"`
ParentId string `json:"parent_id"`
TriggerId string `json:"trigger_id,omitempty"`
Command string `json:"command"`
SiteURL string `json:"-"`
T i18n.TranslateFunc `json:"-"`
UserMentions UserMentionMap `json:"-"`
ChannelMentions ChannelMentionMap `json:"-"`
// DO NOT USE Session field is deprecated. MM-26398
Session Session `json:"-"`
}
func (o *CommandArgs) Auditable() map[string]interface{} {
return map[string]interface{}{
"user_id": o.UserId,
"channel_id": o.ChannelId,
"team_id": o.TeamId,
"root_id": o.RootId,
"parent_id": o.ParentId,
"trigger_id": o.TriggerId,
"command": o.Command,
"site_url": o.SiteURL,
}
}
// AddUserMention adds or overrides an entry in UserMentions with name username
// and identifier userId
func (o *CommandArgs) AddUserMention(username, userId string) {
if o.UserMentions == nil {
o.UserMentions = make(UserMentionMap)
}
o.UserMentions[username] = userId
}
// AddChannelMention adds or overrides an entry in ChannelMentions with name
// channelName and identifier channelId
func (o *CommandArgs) AddChannelMention(channelName, channelId string) {
if o.ChannelMentions == nil {
o.ChannelMentions = make(ChannelMentionMap)
}
o.ChannelMentions[channelName] = channelId
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"net/url"
"path"
"reflect"
"strings"
"github.com/pkg/errors"
)
// AutocompleteArgType describes autocomplete argument type
type AutocompleteArgType string
// Argument types
const (
AutocompleteArgTypeText AutocompleteArgType = "TextInput"
AutocompleteArgTypeStaticList AutocompleteArgType = "StaticList"
AutocompleteArgTypeDynamicList AutocompleteArgType = "DynamicList"
)
// AutocompleteData describes slash command autocomplete information.
type AutocompleteData struct {
// Trigger of the command
Trigger string
// Hint of a command
Hint string
// Text displayed to the user to help with the autocomplete description
HelpText string
// Role of the user who should be able to see the autocomplete info of this command
RoleID string
// Arguments of the command. Arguments can be named or positional.
// If they are positional order in the list matters, if they are named order does not matter.
// All arguments should be either named or positional, no mixing allowed.
Arguments []*AutocompleteArg
// Subcommands of the command
SubCommands []*AutocompleteData
}
// AutocompleteArg describes an argument of the command. Arguments can be named or positional.
// If Name is empty string Argument is positional otherwise it is named argument.
// Named arguments are passed as --Name Argument_Value.
type AutocompleteArg struct {
// Name of the argument
Name string
// Text displayed to the user to help with the autocomplete
HelpText string
// Type of the argument
Type AutocompleteArgType
// Required determines if argument is optional or not.
Required bool
// Actual data of the argument (depends on the Type)
Data any
}
// AutocompleteTextArg describes text user can input as an argument.
type AutocompleteTextArg struct {
// Hint of the input text
Hint string
// Regex pattern to match
Pattern string
}
// AutocompleteListItem describes an item in the AutocompleteStaticListArg.
type AutocompleteListItem struct {
Item string
Hint string
HelpText string
}
// AutocompleteStaticListArg is used to input one of the arguments from the list,
// for example [yes, no], [on, off], and so on.
type AutocompleteStaticListArg struct {
PossibleArguments []AutocompleteListItem
}
// AutocompleteDynamicListArg is used when user wants to download possible argument list from the URL.
type AutocompleteDynamicListArg struct {
FetchURL string
}
// AutocompleteSuggestion describes a single suggestion item sent to the front-end
// Example: for user input `/jira cre` -
// Complete might be `/jira create`
// Suggestion might be `create`,
// Hint might be `[issue text]`,
// Description might be `Create a new Issue`
type AutocompleteSuggestion struct {
// Complete describes completed suggestion
Complete string
// Suggestion describes what user might want to input next
Suggestion string
// Hint describes a hint about the suggested input
Hint string
// Description of the command or a suggestion
Description string
// IconData is base64 encoded svg image
IconData string
}
// NewAutocompleteData returns new Autocomplete data.
func NewAutocompleteData(trigger, hint, helpText string) *AutocompleteData {
return &AutocompleteData{
Trigger: trigger,
Hint: hint,
HelpText: helpText,
RoleID: SystemUserRoleId,
Arguments: []*AutocompleteArg{},
SubCommands: []*AutocompleteData{},
}
}
// AddCommand adds a subcommand to the autocomplete data.
func (ad *AutocompleteData) AddCommand(command *AutocompleteData) {
ad.SubCommands = append(ad.SubCommands, command)
}
// AddTextArgument adds positional AutocompleteArgTypeText argument to the command.
func (ad *AutocompleteData) AddTextArgument(helpText, hint, pattern string) {
ad.AddNamedTextArgument("", helpText, hint, pattern, true)
}
// AddNamedTextArgument adds named AutocompleteArgTypeText argument to the command.
func (ad *AutocompleteData) AddNamedTextArgument(name, helpText, hint, pattern string, required bool) {
argument := AutocompleteArg{
Name: name,
HelpText: helpText,
Type: AutocompleteArgTypeText,
Required: required,
Data: &AutocompleteTextArg{Hint: hint, Pattern: pattern},
}
ad.Arguments = append(ad.Arguments, &argument)
}
// AddStaticListArgument adds positional AutocompleteArgTypeStaticList argument to the command.
func (ad *AutocompleteData) AddStaticListArgument(helpText string, required bool, items []AutocompleteListItem) {
ad.AddNamedStaticListArgument("", helpText, required, items)
}
// AddNamedStaticListArgument adds named AutocompleteArgTypeStaticList argument to the command.
func (ad *AutocompleteData) AddNamedStaticListArgument(name, helpText string, required bool, items []AutocompleteListItem) {
argument := AutocompleteArg{
Name: name,
HelpText: helpText,
Type: AutocompleteArgTypeStaticList,
Required: required,
Data: &AutocompleteStaticListArg{PossibleArguments: items},
}
ad.Arguments = append(ad.Arguments, &argument)
}
// AddDynamicListArgument adds positional AutocompleteArgTypeDynamicList argument to the command.
func (ad *AutocompleteData) AddDynamicListArgument(helpText, url string, required bool) {
ad.AddNamedDynamicListArgument("", helpText, url, required)
}
// AddNamedDynamicListArgument adds named AutocompleteArgTypeDynamicList argument to the command.
func (ad *AutocompleteData) AddNamedDynamicListArgument(name, helpText, url string, required bool) {
argument := AutocompleteArg{
Name: name,
HelpText: helpText,
Type: AutocompleteArgTypeDynamicList,
Required: required,
Data: &AutocompleteDynamicListArg{FetchURL: url},
}
ad.Arguments = append(ad.Arguments, &argument)
}
// Equals method checks if command is the same.
func (ad *AutocompleteData) Equals(command *AutocompleteData) bool {
if !(ad.Trigger == command.Trigger && ad.HelpText == command.HelpText && ad.RoleID == command.RoleID && ad.Hint == command.Hint) {
return false
}
if len(ad.Arguments) != len(command.Arguments) || len(ad.SubCommands) != len(command.SubCommands) {
return false
}
for i := range ad.Arguments {
if !ad.Arguments[i].Equals(command.Arguments[i]) {
return false
}
}
for i := range ad.SubCommands {
if !ad.SubCommands[i].Equals(command.SubCommands[i]) {
return false
}
}
return true
}
// UpdateRelativeURLsForPluginCommands method updates relative urls for plugin commands
func (ad *AutocompleteData) UpdateRelativeURLsForPluginCommands(baseURL *url.URL) error {
for _, arg := range ad.Arguments {
if arg.Type != AutocompleteArgTypeDynamicList {
continue
}
dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg)
if !ok {
return errors.New("Not a proper DynamicList type argument")
}
dynamicListURL, err := url.Parse(dynamicList.FetchURL)
if err != nil {
return errors.Wrapf(err, "FetchURL is not a proper url")
}
if !dynamicListURL.IsAbs() {
absURL := &url.URL{}
*absURL = *baseURL
absURL.Path = path.Join(absURL.Path, dynamicList.FetchURL)
dynamicList.FetchURL = absURL.String()
}
}
for _, command := range ad.SubCommands {
err := command.UpdateRelativeURLsForPluginCommands(baseURL)
if err != nil {
return err
}
}
return nil
}
// IsValid method checks if autocomplete data is valid.
func (ad *AutocompleteData) IsValid() error {
if ad == nil {
return errors.New("No nil commands are allowed in AutocompleteData")
}
if ad.Trigger == "" {
return errors.New("An empty command name in the autocomplete data")
}
if strings.ToLower(ad.Trigger) != ad.Trigger {
return errors.New("Command should be lowercase")
}
roles := []string{SystemAdminRoleId, SystemUserRoleId, ""}
if stringNotInSlice(ad.RoleID, roles) {
return errors.New("Wrong role in the autocomplete data")
}
if len(ad.Arguments) > 0 && len(ad.SubCommands) > 0 {
return errors.New("Command can't have arguments and subcommands")
}
if len(ad.Arguments) > 0 {
namedArgumentIndex := -1
for i, arg := range ad.Arguments {
if arg.Name != "" { // it's a named argument
if namedArgumentIndex == -1 { // first named argument
namedArgumentIndex = i
}
} else { // it's a positional argument
if namedArgumentIndex != -1 {
return errors.New("Named argument should not be before positional argument")
}
}
if arg.Type == AutocompleteArgTypeDynamicList {
dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg)
if !ok {
return errors.New("Not a proper DynamicList type argument")
}
_, err := url.Parse(dynamicList.FetchURL)
if err != nil {
return errors.Wrapf(err, "FetchURL is not a proper url")
}
} else if arg.Type == AutocompleteArgTypeStaticList {
staticList, ok := arg.Data.(*AutocompleteStaticListArg)
if !ok {
return errors.New("Not a proper StaticList type argument")
}
for _, arg := range staticList.PossibleArguments {
if arg.Item == "" {
return errors.New("Possible argument name not set in StaticList argument")
}
}
} else if arg.Type == AutocompleteArgTypeText {
if _, ok := arg.Data.(*AutocompleteTextArg); !ok {
return errors.New("Not a proper TextInput type argument")
}
if arg.Name == "" && !arg.Required {
return errors.New("Positional argument can not be optional")
}
}
}
}
for _, command := range ad.SubCommands {
err := command.IsValid()
if err != nil {
return err
}
}
return nil
}
// Equals method checks if argument is the same.
func (a *AutocompleteArg) Equals(arg *AutocompleteArg) bool {
if a.Name != arg.Name ||
a.HelpText != arg.HelpText ||
a.Type != arg.Type ||
a.Required != arg.Required ||
!reflect.DeepEqual(a.Data, arg.Data) {
return false
}
return true
}
// UnmarshalJSON will unmarshal argument
func (a *AutocompleteArg) UnmarshalJSON(b []byte) error {
var arg map[string]any
if err := json.Unmarshal(b, &arg); err != nil {
return errors.Wrapf(err, "Can't unmarshal argument %s", string(b))
}
var ok bool
a.Name, ok = arg["Name"].(string)
if !ok {
return errors.Errorf("No field Name in the argument %s", string(b))
}
a.HelpText, ok = arg["HelpText"].(string)
if !ok {
return errors.Errorf("No field HelpText in the argument %s", string(b))
}
t, ok := arg["Type"].(string)
if !ok {
return errors.Errorf("No field Type in the argument %s", string(b))
}
a.Type = AutocompleteArgType(t)
a.Required, ok = arg["Required"].(bool)
if !ok {
return errors.Errorf("No field Required in the argument %s", string(b))
}
data, ok := arg["Data"]
if !ok {
return errors.Errorf("No field Data in the argument %s", string(b))
}
if a.Type == AutocompleteArgTypeText {
m, ok := data.(map[string]any)
if !ok {
return errors.Errorf("Wrong Data type in the TextInput argument %s", string(b))
}
pattern, ok := m["Pattern"].(string)
if !ok {
return errors.Errorf("No field Pattern in the TextInput argument %s", string(b))
}
hint, ok := m["Hint"].(string)
if !ok {
return errors.Errorf("No field Hint in the TextInput argument %s", string(b))
}
a.Data = &AutocompleteTextArg{Hint: hint, Pattern: pattern}
} else if a.Type == AutocompleteArgTypeStaticList {
m, ok := data.(map[string]any)
if !ok {
return errors.Errorf("Wrong Data type in the StaticList argument %s", string(b))
}
list, ok := m["PossibleArguments"].([]any)
if !ok {
return errors.Errorf("No field PossibleArguments in the StaticList argument %s", string(b))
}
possibleArguments := []AutocompleteListItem{}
for i := range list {
args, ok := list[i].(map[string]any)
if !ok {
return errors.Errorf("Wrong AutocompleteStaticListItem type in the StaticList argument %s", string(b))
}
item, ok := args["Item"].(string)
if !ok {
return errors.Errorf("No field Item in the StaticList's possible arguments %s", string(b))
}
hint, ok := args["Hint"].(string)
if !ok {
return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b))
}
helpText, ok := args["HelpText"].(string)
if !ok {
return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b))
}
possibleArguments = append(possibleArguments, AutocompleteListItem{
Item: item,
Hint: hint,
HelpText: helpText,
})
}
a.Data = &AutocompleteStaticListArg{PossibleArguments: possibleArguments}
} else if a.Type == AutocompleteArgTypeDynamicList {
m, ok := data.(map[string]any)
if !ok {
return errors.Errorf("Wrong type in the DynamicList argument %s", string(b))
}
url, ok := m["FetchURL"].(string)
if !ok {
return errors.Errorf("No field FetchURL in the DynamicList's argument %s", string(b))
}
a.Data = &AutocompleteDynamicListArg{FetchURL: url}
}
return nil
}
func stringNotInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return false
}
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/jsonutils"
)
const (
CommandResponseTypeInChannel = "in_channel"
CommandResponseTypeEphemeral = "ephemeral"
)
type CommandResponse struct {
ResponseType string `json:"response_type"`
Text string `json:"text"`
Username string `json:"username"`
ChannelId string `json:"channel_id"`
IconURL string `json:"icon_url"`
Type string `json:"type"`
Props StringInterface `json:"props"`
GotoLocation string `json:"goto_location"`
TriggerId string `json:"trigger_id"`
SkipSlackParsing bool `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text.
Attachments []*SlackAttachment `json:"attachments"`
ExtraResponses []*CommandResponse `json:"extra_responses"`
}
func CommandResponseFromHTTPBody(contentType string, body io.Reader) (*CommandResponse, error) {
if strings.TrimSpace(strings.Split(contentType, ";")[0]) == "application/json" {
return CommandResponseFromJSON(body)
}
if b, err := io.ReadAll(body); err == nil {
return CommandResponseFromPlainText(string(b)), nil
}
return nil, nil
}
func CommandResponseFromPlainText(text string) *CommandResponse {
return &CommandResponse{
Text: text,
}
}
func CommandResponseFromJSON(data io.Reader) (*CommandResponse, error) {
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
var o CommandResponse
err = json.Unmarshal(b, &o)
if err != nil {
return nil, jsonutils.HumanizeJSONError(err, b)
}
o.Attachments = StringifySlackFieldValue(o.Attachments)
if o.ExtraResponses != nil {
for _, resp := range o.ExtraResponses {
resp.Attachments = StringifySlackFieldValue(resp.Attachments)
}
}
return &o, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
type CommandWebhook struct {
Id string
CreateAt int64
CommandId string
UserId string
ChannelId string
RootId string
UseCount int
}
const (
CommandWebhookLifetime = 1000 * 60 * 30
)
func (o *CommandWebhook) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
}
func (o *CommandWebhook) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.CommandId) {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.command_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.user_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if o.RootId != "" && !IsValidId(o.RootId) {
return NewAppError("CommandWebhook.IsValid", "model.command_hook.root_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"strings"
)
const (
ComplianceStatusCreated = "created"
ComplianceStatusRunning = "running"
ComplianceStatusFinished = "finished"
ComplianceStatusFailed = "failed"
ComplianceStatusRemoved = "removed"
ComplianceTypeDaily = "daily"
ComplianceTypeAdhoc = "adhoc"
)
type Compliance struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UserId string `json:"user_id"`
Status string `json:"status"`
Count int `json:"count"`
Desc string `json:"desc"`
Type string `json:"type"`
StartAt int64 `json:"start_at"`
EndAt int64 `json:"end_at"`
Keywords string `json:"keywords"`
Emails string `json:"emails"`
}
func (c *Compliance) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": c.Id,
"create_at": c.CreateAt,
"user_id": c.UserId,
"status": c.Status,
"count": c.Count,
"desc": c.Desc,
"type": c.Type,
"start_at": c.StartAt,
"end_at": c.EndAt,
"keywords": c.Keywords,
"emails": c.Emails,
}
}
type Compliances []Compliance
// ComplianceExportCursor is used for paginated iteration of posts
// for compliance export.
// We need to keep track of the last post ID in addition to the last post
// CreateAt to break ties when two posts have the same CreateAt.
type ComplianceExportCursor struct {
LastChannelsQueryPostCreateAt int64
LastChannelsQueryPostID string
ChannelsQueryCompleted bool
LastDirectMessagesQueryPostCreateAt int64
LastDirectMessagesQueryPostID string
DirectMessagesQueryCompleted bool
}
func (c *Compliance) PreSave() {
if c.Id == "" {
c.Id = NewId()
}
if c.Status == "" {
c.Status = ComplianceStatusCreated
}
c.Count = 0
c.Emails = NormalizeEmail(c.Emails)
c.Keywords = strings.ToLower(c.Keywords)
c.CreateAt = GetMillis()
}
func (c *Compliance) DeepCopy() *Compliance {
copy := *c
return ©
}
func (c *Compliance) JobName() string {
jobName := c.Type
if c.Type == ComplianceTypeDaily {
jobName += "-" + c.Desc
}
jobName += "-" + c.Id
return jobName
}
func (c *Compliance) IsValid() *AppError {
if !IsValidId(c.Id) {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if c.CreateAt == 0 {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if len(c.Desc) > 512 || c.Desc == "" {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.desc.app_error", nil, "", http.StatusBadRequest)
}
if c.StartAt == 0 {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.start_at.app_error", nil, "", http.StatusBadRequest)
}
if c.EndAt == 0 {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.end_at.app_error", nil, "", http.StatusBadRequest)
}
if c.EndAt <= c.StartAt {
return NewAppError("Compliance.IsValid", "model.compliance.is_valid.start_end_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"regexp"
"time"
)
type CompliancePost struct {
// From Team
TeamName string
TeamDisplayName string
// From Channel
ChannelName string
ChannelDisplayName string
ChannelType string
// From User
UserUsername string
UserEmail string
UserNickname string
// From Post
PostId string
PostCreateAt int64
PostUpdateAt int64
PostDeleteAt int64
PostRootId string
PostOriginalId string
PostMessage string
PostType string
PostProps string
PostHashtags string
PostFileIds string
IsBot bool
}
func CompliancePostHeader() []string {
return []string{
"TeamName",
"TeamDisplayName",
"ChannelName",
"ChannelDisplayName",
"ChannelType",
"UserUsername",
"UserEmail",
"UserNickname",
"UserType",
"PostId",
"PostCreateAt",
"PostUpdateAt",
"PostDeleteAt",
"PostRootId",
"PostOriginalId",
"PostMessage",
"PostType",
"PostProps",
"PostHashtags",
"PostFileIds",
}
}
func cleanComplianceStrings(in string) string {
if matched, _ := regexp.MatchString("^\\s*(=|\\+|\\-)", in); matched {
return "'" + in
}
return in
}
func (cp *CompliancePost) Row() []string {
postDeleteAt := ""
if cp.PostDeleteAt > 0 {
postDeleteAt = time.Unix(0, cp.PostDeleteAt*int64(1000*1000)).Format(time.RFC3339)
}
postUpdateAt := ""
if cp.PostUpdateAt != cp.PostCreateAt {
postUpdateAt = time.Unix(0, cp.PostUpdateAt*int64(1000*1000)).Format(time.RFC3339)
}
userType := "user"
if cp.IsBot {
userType = "bot"
}
return []string{
cleanComplianceStrings(cp.TeamName),
cleanComplianceStrings(cp.TeamDisplayName),
cleanComplianceStrings(cp.ChannelName),
cleanComplianceStrings(cp.ChannelDisplayName),
cleanComplianceStrings(cp.ChannelType),
cleanComplianceStrings(cp.UserUsername),
cleanComplianceStrings(cp.UserEmail),
cleanComplianceStrings(cp.UserNickname),
userType,
cp.PostId,
time.Unix(0, cp.PostCreateAt*int64(1000*1000)).Format(time.RFC3339),
postUpdateAt,
postDeleteAt,
cp.PostRootId,
cp.PostOriginalId,
cleanComplianceStrings(cp.PostMessage),
cp.PostType,
cp.PostProps,
cp.PostHashtags,
cp.PostFileIds,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/tls"
"encoding/json"
"io"
"math"
"net"
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/mattermost/ldap"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
ConnSecurityNone = ""
ConnSecurityPlain = "PLAIN"
ConnSecurityTLS = "TLS"
ConnSecurityStarttls = "STARTTLS"
ImageDriverLocal = "local"
ImageDriverS3 = "amazons3"
DatabaseDriverMysql = "mysql"
DatabaseDriverPostgres = "postgres"
SearchengineElasticsearch = "elasticsearch"
MinioAccessKey = "minioaccesskey"
MinioSecretKey = "miniosecretkey"
MinioBucket = "mattermost-test"
PasswordMaximumLength = 64
PasswordMinimumLength = 5
ServiceGitlab = "gitlab"
ServiceGoogle = "google"
ServiceOffice365 = "office365"
ServiceOpenid = "openid"
GenericNoChannelNotification = "generic_no_channel"
GenericNotification = "generic"
GenericNotificationServer = "https://push-test.mattermost.com"
MmSupportAdvisorAddress = "support-advisor@mattermost.com"
FullNotification = "full"
IdLoadedNotification = "id_loaded"
DirectMessageAny = "any"
DirectMessageTeam = "team"
ShowUsername = "username"
ShowNicknameFullName = "nickname_full_name"
ShowFullName = "full_name"
PermissionsAll = "all"
PermissionsChannelAdmin = "channel_admin"
PermissionsTeamAdmin = "team_admin"
PermissionsSystemAdmin = "system_admin"
FakeSetting = "********************************"
RestrictEmojiCreationAll = "all"
RestrictEmojiCreationAdmin = "admin"
RestrictEmojiCreationSystemAdmin = "system_admin"
PermissionsDeletePostAll = "all"
PermissionsDeletePostTeamAdmin = "team_admin"
PermissionsDeletePostSystemAdmin = "system_admin"
GroupUnreadChannelsDisabled = "disabled"
GroupUnreadChannelsDefaultOn = "default_on"
GroupUnreadChannelsDefaultOff = "default_off"
CollapsedThreadsDisabled = "disabled"
CollapsedThreadsDefaultOn = "default_on"
CollapsedThreadsDefaultOff = "default_off"
CollapsedThreadsAlwaysOn = "always_on"
EmailBatchingBufferSize = 256
EmailBatchingInterval = 30
EmailNotificationContentsFull = "full"
EmailNotificationContentsGeneric = "generic"
EmailSMTPDefaultServer = "localhost"
EmailSMTPDefaultPort = "10025"
SitenameMaxLength = 30
ServiceSettingsDefaultSiteURL = "http://localhost:8065"
ServiceSettingsDefaultTLSCertFile = ""
ServiceSettingsDefaultTLSKeyFile = ""
ServiceSettingsDefaultReadTimeout = 300
ServiceSettingsDefaultWriteTimeout = 300
ServiceSettingsDefaultIdleTimeout = 60
ServiceSettingsDefaultMaxLoginAttempts = 10
ServiceSettingsDefaultAllowCorsFrom = ""
ServiceSettingsDefaultListenAndAddress = ":8065"
ServiceSettingsDefaultGfycatAPIKey = "2_KtH_W5"
ServiceSettingsDefaultGfycatAPISecret = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof"
ServiceSettingsDefaultDeveloperFlags = ""
TeamSettingsDefaultSiteName = "Mattermost"
TeamSettingsDefaultMaxUsersPerTeam = 50
TeamSettingsDefaultCustomBrandText = ""
TeamSettingsDefaultCustomDescriptionText = ""
TeamSettingsDefaultUserStatusAwayTimeout = 300
SqlSettingsDefaultDataSource = "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes"
FileSettingsDefaultDirectory = "./data/"
ImportSettingsDefaultDirectory = "./import"
ImportSettingsDefaultRetentionDays = 30
ExportSettingsDefaultDirectory = "./export"
ExportSettingsDefaultRetentionDays = 30
EmailSettingsDefaultFeedbackOrganization = ""
SupportSettingsDefaultTermsOfServiceLink = "https://mattermost.com/terms-of-use/"
SupportSettingsDefaultPrivacyPolicyLink = "https://mattermost.com/privacy-policy/"
SupportSettingsDefaultAboutLink = "https://docs.mattermost.com/about/product.html/"
SupportSettingsDefaultHelpLink = "https://mattermost.com/default-help/"
SupportSettingsDefaultReportAProblemLink = "https://mattermost.com/default-report-a-problem/"
SupportSettingsDefaultSupportEmail = ""
SupportSettingsDefaultReAcceptancePeriod = 365
LdapSettingsDefaultFirstNameAttribute = ""
LdapSettingsDefaultLastNameAttribute = ""
LdapSettingsDefaultEmailAttribute = ""
LdapSettingsDefaultUsernameAttribute = ""
LdapSettingsDefaultNicknameAttribute = ""
LdapSettingsDefaultIdAttribute = ""
LdapSettingsDefaultPositionAttribute = ""
LdapSettingsDefaultLoginFieldName = ""
LdapSettingsDefaultGroupDisplayNameAttribute = ""
LdapSettingsDefaultGroupIdAttribute = ""
LdapSettingsDefaultPictureAttribute = ""
SamlSettingsDefaultIdAttribute = ""
SamlSettingsDefaultGuestAttribute = ""
SamlSettingsDefaultAdminAttribute = ""
SamlSettingsDefaultFirstNameAttribute = ""
SamlSettingsDefaultLastNameAttribute = ""
SamlSettingsDefaultEmailAttribute = ""
SamlSettingsDefaultUsernameAttribute = ""
SamlSettingsDefaultNicknameAttribute = ""
SamlSettingsDefaultLocaleAttribute = ""
SamlSettingsDefaultPositionAttribute = ""
SamlSettingsSignatureAlgorithmSha1 = "RSAwithSHA1"
SamlSettingsSignatureAlgorithmSha256 = "RSAwithSHA256"
SamlSettingsSignatureAlgorithmSha512 = "RSAwithSHA512"
SamlSettingsDefaultSignatureAlgorithm = SamlSettingsSignatureAlgorithmSha1
SamlSettingsCanonicalAlgorithmC14n = "Canonical1.0"
SamlSettingsCanonicalAlgorithmC14n11 = "Canonical1.1"
SamlSettingsDefaultCanonicalAlgorithm = SamlSettingsCanonicalAlgorithmC14n
NativeappSettingsDefaultAppDownloadLink = "https://mattermost.com/download/#mattermostApps"
NativeappSettingsDefaultAndroidAppDownloadLink = "https://mattermost.com/mattermost-android-app/"
NativeappSettingsDefaultIosAppDownloadLink = "https://mattermost.com/mattermost-ios-app/"
ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000
AnalyticsSettingsDefaultMaxUsersForStatistics = 2500
AnnouncementSettingsDefaultBannerColor = "#f2a93b"
AnnouncementSettingsDefaultBannerTextColor = "#333333"
AnnouncementSettingsDefaultNoticesJsonURL = "https://notices.mattermost.com/"
AnnouncementSettingsDefaultNoticesFetchFrequencySeconds = 3600
TeamSettingsDefaultTeamText = "default"
ElasticsearchSettingsDefaultConnectionURL = "http://localhost:9200"
ElasticsearchSettingsDefaultUsername = "elastic"
ElasticsearchSettingsDefaultPassword = "changeme"
ElasticsearchSettingsDefaultPostIndexReplicas = 1
ElasticsearchSettingsDefaultPostIndexShards = 1
ElasticsearchSettingsDefaultChannelIndexReplicas = 1
ElasticsearchSettingsDefaultChannelIndexShards = 1
ElasticsearchSettingsDefaultUserIndexReplicas = 1
ElasticsearchSettingsDefaultUserIndexShards = 1
ElasticsearchSettingsDefaultAggregatePostsAfterDays = 365
ElasticsearchSettingsDefaultPostsAggregatorJobStartTime = "03:00"
ElasticsearchSettingsDefaultIndexPrefix = ""
ElasticsearchSettingsDefaultLiveIndexingBatchSize = 1
ElasticsearchSettingsDefaultRequestTimeoutSeconds = 30
ElasticsearchSettingsDefaultBatchSize = 10000
BleveSettingsDefaultIndexDir = ""
BleveSettingsDefaultBatchSize = 10000
DataRetentionSettingsDefaultMessageRetentionDays = 365
DataRetentionSettingsDefaultFileRetentionDays = 365
DataRetentionSettingsDefaultBoardsRetentionDays = 365
DataRetentionSettingsDefaultDeletionJobStartTime = "02:00"
DataRetentionSettingsDefaultBatchSize = 3000
PluginSettingsDefaultDirectory = "./plugins"
PluginSettingsDefaultClientDirectory = "./client/plugins"
PluginSettingsDefaultEnableMarketplace = true
PluginSettingsDefaultMarketplaceURL = "https://api.integrations.mattermost.com"
PluginSettingsOldMarketplaceURL = "https://marketplace.integrations.mattermost.com"
ComplianceExportTypeCsv = "csv"
ComplianceExportTypeActiance = "actiance"
ComplianceExportTypeGlobalrelay = "globalrelay"
ComplianceExportTypeGlobalrelayZip = "globalrelay-zip"
GlobalrelayCustomerTypeA9 = "A9"
GlobalrelayCustomerTypeA10 = "A10"
ClientSideCertCheckPrimaryAuth = "primary"
ClientSideCertCheckSecondaryAuth = "secondary"
ImageProxyTypeLocal = "local"
ImageProxyTypeAtmosCamo = "atmos/camo"
GoogleSettingsDefaultScope = "profile email"
GoogleSettingsDefaultAuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
GoogleSettingsDefaultTokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"
GoogleSettingsDefaultUserAPIEndpoint = "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata"
Office365SettingsDefaultScope = "User.Read"
Office365SettingsDefaultAuthEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
Office365SettingsDefaultTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
Office365SettingsDefaultUserAPIEndpoint = "https://graph.microsoft.com/v1.0/me"
CloudSettingsDefaultCwsURL = "https://customers.mattermost.com"
CloudSettingsDefaultCwsAPIURL = "https://portal.internal.prod.cloud.mattermost.com"
// TODO: update to "https://portal.test.cloud.mattermost.com" when ready to use test license key
CloudSettingsDefaultCwsURLTest = "https://customers.mattermost.com"
// TODO: update to // "https://api.internal.test.cloud.mattermost.com" when ready to use test license key
CloudSettingsDefaultCwsAPIURLTest = "https://portal.internal.prod.cloud.mattermost.com"
OpenidSettingsDefaultScope = "profile openid email"
LocalModeSocketPath = "/var/tmp/mattermost_local.socket"
)
func GetDefaultAppCustomURLSchemes() []string {
return []string{"mmauth://", "mmauthbeta://"}
}
var ServerTLSSupportedCiphers = map[string]uint16{
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
}
type ServiceSettings struct {
SiteURL *string `access:"environment_web_server,authentication_saml,write_restrictable"`
WebsocketURL *string `access:"write_restrictable,cloud_restrictable"`
LicenseFileLocation *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
ListenAddress *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` // telemetry: none
ConnectionSecurity *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
TLSCertFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
TLSKeyFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
TLSMinVer *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
TLSStrictTransport *bool `access:"write_restrictable,cloud_restrictable"`
// In seconds.
TLSStrictTransportMaxAge *int64 `access:"write_restrictable,cloud_restrictable"` // telemetry: none
TLSOverwriteCiphers []string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
UseLetsEncrypt *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"`
LetsEncryptCertificateCacheFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` // telemetry: none
Forward80To443 *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"`
TrustedProxyIPHeader []string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
ReadTimeout *int `access:"environment_web_server,write_restrictable,cloud_restrictable"`
WriteTimeout *int `access:"environment_web_server,write_restrictable,cloud_restrictable"`
IdleTimeout *int `access:"write_restrictable,cloud_restrictable"`
MaximumLoginAttempts *int `access:"authentication_password,write_restrictable,cloud_restrictable"`
GoroutineHealthThreshold *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none
EnableOAuthServiceProvider *bool `access:"integrations_integration_management"`
EnableIncomingWebhooks *bool `access:"integrations_integration_management"`
EnableOutgoingWebhooks *bool `access:"integrations_integration_management"`
EnableCommands *bool `access:"integrations_integration_management"`
EnablePostUsernameOverride *bool `access:"integrations_integration_management"`
EnablePostIconOverride *bool `access:"integrations_integration_management"`
GoogleDeveloperKey *string `access:"site_posts,write_restrictable,cloud_restrictable"`
EnableLinkPreviews *bool `access:"site_posts"`
EnablePermalinkPreviews *bool `access:"site_posts"`
RestrictLinkPreviews *string `access:"site_posts"`
EnableTesting *bool `access:"environment_developer,write_restrictable,cloud_restrictable"`
EnableDeveloper *bool `access:"environment_developer,write_restrictable,cloud_restrictable"`
DeveloperFlags *string `access:"environment_developer"`
EnableClientPerformanceDebugging *bool `access:"environment_developer,write_restrictable,cloud_restrictable"`
EnableOpenTracing *bool `access:"write_restrictable,cloud_restrictable"`
EnableSecurityFixAlert *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"`
EnableInsecureOutgoingConnections *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"`
AllowedUntrustedInternalConnections *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
EnableMultifactorAuthentication *bool `access:"authentication_mfa"`
EnforceMultifactorAuthentication *bool `access:"authentication_mfa"`
EnableUserAccessTokens *bool `access:"integrations_integration_management"`
AllowCorsFrom *string `access:"integrations_cors,write_restrictable,cloud_restrictable"`
CorsExposedHeaders *string `access:"integrations_cors,write_restrictable,cloud_restrictable"`
CorsAllowCredentials *bool `access:"integrations_cors,write_restrictable,cloud_restrictable"`
CorsDebug *bool `access:"integrations_cors,write_restrictable,cloud_restrictable"`
AllowCookiesForSubdomains *bool `access:"write_restrictable,cloud_restrictable"`
ExtendSessionLengthWithActivity *bool `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
// Deprecated
SessionLengthWebInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` // telemetry: none
SessionLengthWebInHours *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
// Deprecated
SessionLengthMobileInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` // telemetry: none
SessionLengthMobileInHours *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
// Deprecated
SessionLengthSSOInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` // telemetry: none
SessionLengthSSOInHours *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
SessionCacheInMinutes *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
SessionIdleTimeoutInMinutes *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"`
WebsocketSecurePort *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none
WebsocketPort *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none
WebserverMode *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
EnableGifPicker *bool `access:"integrations_gif"`
GfycatAPIKey *string `access:"integrations_gif"`
GfycatAPISecret *string `access:"integrations_gif"`
EnableCustomEmoji *bool `access:"site_emoji"`
EnableEmojiPicker *bool `access:"site_emoji"`
PostEditTimeLimit *int `access:"user_management_permissions"`
TimeBetweenUserTypingUpdatesMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"`
EnablePostSearch *bool `access:"write_restrictable,cloud_restrictable"`
EnableFileSearch *bool `access:"write_restrictable"`
MinimumHashtagLength *int `access:"environment_database,write_restrictable,cloud_restrictable"`
EnableUserTypingMessages *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
EnableChannelViewedMessages *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
EnableUserStatuses *bool `access:"write_restrictable,cloud_restrictable"`
ExperimentalEnableAuthenticationTransfer *bool `access:"experimental_features"`
ClusterLogTimeoutMilliseconds *int `access:"write_restrictable,cloud_restrictable"`
EnablePreviewFeatures *bool `access:"experimental_features"`
EnableTutorial *bool `access:"experimental_features"`
EnableOnboardingFlow *bool `access:"experimental_features"`
ExperimentalEnableDefaultChannelLeaveJoinMessages *bool `access:"experimental_features"`
ExperimentalGroupUnreadChannels *string `access:"experimental_features"`
EnableAPITeamDeletion *bool
EnableAPITriggerAdminNotifications *bool
EnableAPIUserDeletion *bool
ExperimentalEnableHardenedMode *bool `access:"experimental_features"`
ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
EnableEmailInvitations *bool `access:"authentication_signup"`
DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations_bot_accounts"`
EnableBotAccountCreation *bool `access:"integrations_bot_accounts"`
EnableSVGs *bool `access:"site_posts"`
EnableLatex *bool `access:"site_posts"`
EnableInlineLatex *bool `access:"site_posts"`
PostPriority *bool `access:"site_posts"`
EnableAPIChannelDeletion *bool
EnableLocalMode *bool `access:"cloud_restrictable"`
LocalModeSocketLocation *string `access:"cloud_restrictable"` // telemetry: none
EnableAWSMetering *bool // telemetry: none
SplitKey *string `access:"experimental_feature_flags,write_restrictable"` // telemetry: none
FeatureFlagSyncIntervalSeconds *int `access:"experimental_feature_flags,write_restrictable"` // telemetry: none
DebugSplit *bool `access:"experimental_feature_flags,write_restrictable"` // telemetry: none
ThreadAutoFollow *bool `access:"experimental_features"`
CollapsedThreads *string `access:"experimental_features"`
ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
EnableCustomGroups *bool `access:"site_users_and_teams"`
SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"`
AllowSyncedDrafts *bool `access:"site_posts"`
SelfHostedExpansion *bool `access:"write_restrictable,cloud_restrictable"`
}
func (s *ServiceSettings) SetDefaults(isUpdate bool) {
if s.EnableEmailInvitations == nil {
// If the site URL is also not present then assume this is a clean install
if s.SiteURL == nil {
s.EnableEmailInvitations = NewBool(false)
} else {
s.EnableEmailInvitations = NewBool(true)
}
}
if s.SiteURL == nil {
if s.EnableDeveloper != nil && *s.EnableDeveloper {
s.SiteURL = NewString(ServiceSettingsDefaultSiteURL)
} else {
s.SiteURL = NewString("")
}
}
if s.WebsocketURL == nil {
s.WebsocketURL = NewString("")
}
if s.LicenseFileLocation == nil {
s.LicenseFileLocation = NewString("")
}
if s.ListenAddress == nil {
s.ListenAddress = NewString(ServiceSettingsDefaultListenAndAddress)
}
if s.EnableLinkPreviews == nil {
s.EnableLinkPreviews = NewBool(true)
}
if s.EnablePermalinkPreviews == nil {
s.EnablePermalinkPreviews = NewBool(true)
}
if s.RestrictLinkPreviews == nil {
s.RestrictLinkPreviews = NewString("")
}
if s.EnableTesting == nil {
s.EnableTesting = NewBool(false)
}
if s.EnableDeveloper == nil {
s.EnableDeveloper = NewBool(false)
}
if s.DeveloperFlags == nil {
s.DeveloperFlags = NewString("")
}
if s.EnableClientPerformanceDebugging == nil {
s.EnableClientPerformanceDebugging = NewBool(false)
}
if s.EnableOpenTracing == nil {
s.EnableOpenTracing = NewBool(false)
}
if s.EnableSecurityFixAlert == nil {
s.EnableSecurityFixAlert = NewBool(true)
}
if s.EnableInsecureOutgoingConnections == nil {
s.EnableInsecureOutgoingConnections = NewBool(false)
}
if s.AllowedUntrustedInternalConnections == nil {
s.AllowedUntrustedInternalConnections = NewString("")
}
if s.EnableMultifactorAuthentication == nil {
s.EnableMultifactorAuthentication = NewBool(false)
}
if s.EnforceMultifactorAuthentication == nil {
s.EnforceMultifactorAuthentication = NewBool(false)
}
if s.EnableUserAccessTokens == nil {
s.EnableUserAccessTokens = NewBool(false)
}
if s.GoroutineHealthThreshold == nil {
s.GoroutineHealthThreshold = NewInt(-1)
}
if s.GoogleDeveloperKey == nil {
s.GoogleDeveloperKey = NewString("")
}
if s.EnableOAuthServiceProvider == nil {
s.EnableOAuthServiceProvider = NewBool(true)
}
if s.EnableIncomingWebhooks == nil {
s.EnableIncomingWebhooks = NewBool(true)
}
if s.EnableOutgoingWebhooks == nil {
s.EnableOutgoingWebhooks = NewBool(true)
}
if s.ConnectionSecurity == nil {
s.ConnectionSecurity = NewString("")
}
if s.TLSKeyFile == nil {
s.TLSKeyFile = NewString(ServiceSettingsDefaultTLSKeyFile)
}
if s.TLSCertFile == nil {
s.TLSCertFile = NewString(ServiceSettingsDefaultTLSCertFile)
}
if s.TLSMinVer == nil {
s.TLSMinVer = NewString("1.2")
}
if s.TLSStrictTransport == nil {
s.TLSStrictTransport = NewBool(false)
}
if s.TLSStrictTransportMaxAge == nil {
s.TLSStrictTransportMaxAge = NewInt64(63072000)
}
if s.TLSOverwriteCiphers == nil {
s.TLSOverwriteCiphers = []string{}
}
if s.UseLetsEncrypt == nil {
s.UseLetsEncrypt = NewBool(false)
}
if s.LetsEncryptCertificateCacheFile == nil {
s.LetsEncryptCertificateCacheFile = NewString("./config/letsencrypt.cache")
}
if s.ReadTimeout == nil {
s.ReadTimeout = NewInt(ServiceSettingsDefaultReadTimeout)
}
if s.WriteTimeout == nil {
s.WriteTimeout = NewInt(ServiceSettingsDefaultWriteTimeout)
}
if s.IdleTimeout == nil {
s.IdleTimeout = NewInt(ServiceSettingsDefaultIdleTimeout)
}
if s.MaximumLoginAttempts == nil {
s.MaximumLoginAttempts = NewInt(ServiceSettingsDefaultMaxLoginAttempts)
}
if s.Forward80To443 == nil {
s.Forward80To443 = NewBool(false)
}
if s.TrustedProxyIPHeader == nil {
s.TrustedProxyIPHeader = []string{}
}
if s.TimeBetweenUserTypingUpdatesMilliseconds == nil {
s.TimeBetweenUserTypingUpdatesMilliseconds = NewInt64(5000)
}
if s.EnablePostSearch == nil {
s.EnablePostSearch = NewBool(true)
}
if s.EnableFileSearch == nil {
s.EnableFileSearch = NewBool(true)
}
if s.MinimumHashtagLength == nil {
s.MinimumHashtagLength = NewInt(3)
}
if s.EnableUserTypingMessages == nil {
s.EnableUserTypingMessages = NewBool(true)
}
if s.EnableChannelViewedMessages == nil {
s.EnableChannelViewedMessages = NewBool(true)
}
if s.EnableUserStatuses == nil {
s.EnableUserStatuses = NewBool(true)
}
if s.ClusterLogTimeoutMilliseconds == nil {
s.ClusterLogTimeoutMilliseconds = NewInt(2000)
}
if s.EnableTutorial == nil {
s.EnableTutorial = NewBool(true)
}
if s.EnableOnboardingFlow == nil {
s.EnableOnboardingFlow = NewBool(true)
}
// Must be manually enabled for existing installations.
if s.ExtendSessionLengthWithActivity == nil {
s.ExtendSessionLengthWithActivity = NewBool(!isUpdate)
}
if s.SessionLengthWebInDays == nil {
if isUpdate {
s.SessionLengthWebInDays = NewInt(180)
} else {
s.SessionLengthWebInDays = NewInt(30)
}
}
if s.SessionLengthWebInHours == nil {
var webTTLDays int
if s.SessionLengthWebInDays == nil {
if isUpdate {
webTTLDays = 180
} else {
webTTLDays = 30
}
} else {
webTTLDays = *s.SessionLengthWebInDays
}
s.SessionLengthWebInHours = NewInt(webTTLDays * 24)
}
if s.SessionLengthMobileInDays == nil {
if isUpdate {
s.SessionLengthMobileInDays = NewInt(180)
} else {
s.SessionLengthMobileInDays = NewInt(30)
}
}
if s.SessionLengthMobileInHours == nil {
var mobileTTLDays int
if s.SessionLengthMobileInDays == nil {
if isUpdate {
mobileTTLDays = 180
} else {
mobileTTLDays = 30
}
} else {
mobileTTLDays = *s.SessionLengthMobileInDays
}
s.SessionLengthMobileInHours = NewInt(mobileTTLDays * 24)
}
if s.SessionLengthSSOInDays == nil {
s.SessionLengthSSOInDays = NewInt(30)
}
if s.SessionLengthSSOInHours == nil {
var ssoTTLDays int
if s.SessionLengthSSOInDays == nil {
ssoTTLDays = 30
} else {
ssoTTLDays = *s.SessionLengthSSOInDays
}
s.SessionLengthSSOInHours = NewInt(ssoTTLDays * 24)
}
if s.SessionCacheInMinutes == nil {
s.SessionCacheInMinutes = NewInt(10)
}
if s.SessionIdleTimeoutInMinutes == nil {
s.SessionIdleTimeoutInMinutes = NewInt(43200)
}
if s.EnableCommands == nil {
s.EnableCommands = NewBool(true)
}
if s.EnablePostUsernameOverride == nil {
s.EnablePostUsernameOverride = NewBool(false)
}
if s.EnablePostIconOverride == nil {
s.EnablePostIconOverride = NewBool(false)
}
if s.WebsocketPort == nil {
s.WebsocketPort = NewInt(80)
}
if s.WebsocketSecurePort == nil {
s.WebsocketSecurePort = NewInt(443)
}
if s.AllowCorsFrom == nil {
s.AllowCorsFrom = NewString(ServiceSettingsDefaultAllowCorsFrom)
}
if s.CorsExposedHeaders == nil {
s.CorsExposedHeaders = NewString("")
}
if s.CorsAllowCredentials == nil {
s.CorsAllowCredentials = NewBool(false)
}
if s.CorsDebug == nil {
s.CorsDebug = NewBool(false)
}
if s.AllowCookiesForSubdomains == nil {
s.AllowCookiesForSubdomains = NewBool(false)
}
if s.WebserverMode == nil {
s.WebserverMode = NewString("gzip")
} else if *s.WebserverMode == "regular" {
*s.WebserverMode = "gzip"
}
if s.EnableCustomEmoji == nil {
s.EnableCustomEmoji = NewBool(true)
}
if s.EnableEmojiPicker == nil {
s.EnableEmojiPicker = NewBool(true)
}
if s.EnableGifPicker == nil {
s.EnableGifPicker = NewBool(true)
}
if s.GfycatAPIKey == nil || *s.GfycatAPIKey == "" {
s.GfycatAPIKey = NewString(ServiceSettingsDefaultGfycatAPIKey)
}
if s.GfycatAPISecret == nil || *s.GfycatAPISecret == "" {
s.GfycatAPISecret = NewString(ServiceSettingsDefaultGfycatAPISecret)
}
if s.ExperimentalEnableAuthenticationTransfer == nil {
s.ExperimentalEnableAuthenticationTransfer = NewBool(true)
}
if s.PostEditTimeLimit == nil {
s.PostEditTimeLimit = NewInt(-1)
}
if s.EnablePreviewFeatures == nil {
s.EnablePreviewFeatures = NewBool(true)
}
if s.ExperimentalEnableDefaultChannelLeaveJoinMessages == nil {
s.ExperimentalEnableDefaultChannelLeaveJoinMessages = NewBool(true)
}
if s.ExperimentalGroupUnreadChannels == nil {
s.ExperimentalGroupUnreadChannels = NewString(GroupUnreadChannelsDisabled)
} else if *s.ExperimentalGroupUnreadChannels == "0" {
s.ExperimentalGroupUnreadChannels = NewString(GroupUnreadChannelsDisabled)
} else if *s.ExperimentalGroupUnreadChannels == "1" {
s.ExperimentalGroupUnreadChannels = NewString(GroupUnreadChannelsDefaultOn)
}
if s.EnableAPITeamDeletion == nil {
s.EnableAPITeamDeletion = NewBool(false)
}
if s.EnableAPITriggerAdminNotifications == nil {
s.EnableAPITriggerAdminNotifications = NewBool(false)
}
if s.EnableAPIUserDeletion == nil {
s.EnableAPIUserDeletion = NewBool(false)
}
if s.EnableAPIChannelDeletion == nil {
s.EnableAPIChannelDeletion = NewBool(false)
}
if s.ExperimentalEnableHardenedMode == nil {
s.ExperimentalEnableHardenedMode = NewBool(false)
}
if s.ExperimentalStrictCSRFEnforcement == nil {
s.ExperimentalStrictCSRFEnforcement = NewBool(false)
}
if s.DisableBotsWhenOwnerIsDeactivated == nil {
s.DisableBotsWhenOwnerIsDeactivated = NewBool(true)
}
if s.EnableBotAccountCreation == nil {
s.EnableBotAccountCreation = NewBool(false)
}
if s.EnableSVGs == nil {
if isUpdate {
s.EnableSVGs = NewBool(true)
} else {
s.EnableSVGs = NewBool(false)
}
}
if s.EnableLatex == nil {
if isUpdate {
s.EnableLatex = NewBool(true)
} else {
s.EnableLatex = NewBool(false)
}
}
if s.EnableInlineLatex == nil {
s.EnableInlineLatex = NewBool(true)
}
if s.EnableLocalMode == nil {
s.EnableLocalMode = NewBool(false)
}
if s.LocalModeSocketLocation == nil {
s.LocalModeSocketLocation = NewString(LocalModeSocketPath)
}
if s.EnableAWSMetering == nil {
s.EnableAWSMetering = NewBool(false)
}
if s.SplitKey == nil {
s.SplitKey = NewString("")
}
if s.FeatureFlagSyncIntervalSeconds == nil {
s.FeatureFlagSyncIntervalSeconds = NewInt(30)
}
if s.DebugSplit == nil {
s.DebugSplit = NewBool(false)
}
if s.ThreadAutoFollow == nil {
s.ThreadAutoFollow = NewBool(true)
}
if s.CollapsedThreads == nil {
s.CollapsedThreads = NewString(CollapsedThreadsAlwaysOn)
}
if s.ManagedResourcePaths == nil {
s.ManagedResourcePaths = NewString("")
}
if s.EnableCustomGroups == nil {
s.EnableCustomGroups = NewBool(true)
}
if s.PostPriority == nil {
s.PostPriority = NewBool(true)
}
if s.AllowSyncedDrafts == nil {
s.AllowSyncedDrafts = NewBool(true)
}
if s.SelfHostedPurchase == nil {
s.SelfHostedPurchase = NewBool(true)
}
if s.SelfHostedExpansion == nil {
s.SelfHostedExpansion = NewBool(false)
}
}
type ClusterSettings struct {
Enable *bool `access:"environment_high_availability,write_restrictable"`
ClusterName *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
OverrideHostname *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
NetworkInterface *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
BindAddress *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
AdvertiseAddress *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
UseIPAddress *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
EnableGossipCompression *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
EnableExperimentalGossipEncryption *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
ReadOnlyConfig *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"`
GossipPort *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
StreamingPort *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
MaxIdleConns *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
MaxIdleConnsPerHost *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
IdleConnTimeoutMilliseconds *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none
}
func (s *ClusterSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.ClusterName == nil {
s.ClusterName = NewString("")
}
if s.OverrideHostname == nil {
s.OverrideHostname = NewString("")
}
if s.NetworkInterface == nil {
s.NetworkInterface = NewString("")
}
if s.BindAddress == nil {
s.BindAddress = NewString("")
}
if s.AdvertiseAddress == nil {
s.AdvertiseAddress = NewString("")
}
if s.UseIPAddress == nil {
s.UseIPAddress = NewBool(true)
}
if s.EnableExperimentalGossipEncryption == nil {
s.EnableExperimentalGossipEncryption = NewBool(false)
}
if s.EnableGossipCompression == nil {
s.EnableGossipCompression = NewBool(true)
}
if s.ReadOnlyConfig == nil {
s.ReadOnlyConfig = NewBool(true)
}
if s.GossipPort == nil {
s.GossipPort = NewInt(8074)
}
if s.StreamingPort == nil {
s.StreamingPort = NewInt(8075)
}
if s.MaxIdleConns == nil {
s.MaxIdleConns = NewInt(100)
}
if s.MaxIdleConnsPerHost == nil {
s.MaxIdleConnsPerHost = NewInt(128)
}
if s.IdleConnTimeoutMilliseconds == nil {
s.IdleConnTimeoutMilliseconds = NewInt(90000)
}
}
type MetricsSettings struct {
Enable *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none
}
func (s *MetricsSettings) SetDefaults() {
if s.ListenAddress == nil {
s.ListenAddress = NewString(":8067")
}
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.BlockProfileRate == nil {
s.BlockProfileRate = NewInt(0)
}
}
type ExperimentalSettings struct {
ClientSideCertEnable *bool `access:"experimental_features,cloud_restrictable"`
ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"`
LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"`
RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"`
UseNewSAMLLibrary *bool `access:"experimental_features,cloud_restrictable"`
EnableSharedChannels *bool `access:"experimental_features"`
EnableRemoteClusterService *bool `access:"experimental_features"`
EnableAppBar *bool `access:"experimental_features"`
PatchPluginsReactDOM *bool `access:"experimental_features"`
}
func (s *ExperimentalSettings) SetDefaults() {
if s.ClientSideCertEnable == nil {
s.ClientSideCertEnable = NewBool(false)
}
if s.ClientSideCertCheck == nil {
s.ClientSideCertCheck = NewString(ClientSideCertCheckSecondaryAuth)
}
if s.LinkMetadataTimeoutMilliseconds == nil {
s.LinkMetadataTimeoutMilliseconds = NewInt64(ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds)
}
if s.RestrictSystemAdmin == nil {
s.RestrictSystemAdmin = NewBool(false)
}
if s.UseNewSAMLLibrary == nil {
s.UseNewSAMLLibrary = NewBool(false)
}
if s.EnableSharedChannels == nil {
s.EnableSharedChannels = NewBool(false)
}
if s.EnableRemoteClusterService == nil {
s.EnableRemoteClusterService = NewBool(false)
}
if s.EnableAppBar == nil {
s.EnableAppBar = NewBool(false)
}
if s.PatchPluginsReactDOM == nil {
s.PatchPluginsReactDOM = NewBool(false)
}
}
type AnalyticsSettings struct {
MaxUsersForStatistics *int `access:"write_restrictable,cloud_restrictable"`
}
func (s *AnalyticsSettings) SetDefaults() {
if s.MaxUsersForStatistics == nil {
s.MaxUsersForStatistics = NewInt(AnalyticsSettingsDefaultMaxUsersForStatistics)
}
}
type SSOSettings struct {
Enable *bool `access:"authentication_openid"`
Secret *string `access:"authentication_openid"` // telemetry: none
Id *string `access:"authentication_openid"` // telemetry: none
Scope *string `access:"authentication_openid"` // telemetry: none
AuthEndpoint *string `access:"authentication_openid"` // telemetry: none
TokenEndpoint *string `access:"authentication_openid"` // telemetry: none
UserAPIEndpoint *string `access:"authentication_openid"` // telemetry: none
DiscoveryEndpoint *string `access:"authentication_openid"` // telemetry: none
ButtonText *string `access:"authentication_openid"` // telemetry: none
ButtonColor *string `access:"authentication_openid"` // telemetry: none
}
func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userAPIEndpoint, buttonColor string) {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.Secret == nil {
s.Secret = NewString("")
}
if s.Id == nil {
s.Id = NewString("")
}
if s.Scope == nil {
s.Scope = NewString(scope)
}
if s.DiscoveryEndpoint == nil {
s.DiscoveryEndpoint = NewString("")
}
if s.AuthEndpoint == nil {
s.AuthEndpoint = NewString(authEndpoint)
}
if s.TokenEndpoint == nil {
s.TokenEndpoint = NewString(tokenEndpoint)
}
if s.UserAPIEndpoint == nil {
s.UserAPIEndpoint = NewString(userAPIEndpoint)
}
if s.ButtonText == nil {
s.ButtonText = NewString("")
}
if s.ButtonColor == nil {
s.ButtonColor = NewString(buttonColor)
}
}
type Office365Settings struct {
Enable *bool `access:"authentication_openid"`
Secret *string `access:"authentication_openid"` // telemetry: none
Id *string `access:"authentication_openid"` // telemetry: none
Scope *string `access:"authentication_openid"`
AuthEndpoint *string `access:"authentication_openid"` // telemetry: none
TokenEndpoint *string `access:"authentication_openid"` // telemetry: none
UserAPIEndpoint *string `access:"authentication_openid"` // telemetry: none
DiscoveryEndpoint *string `access:"authentication_openid"` // telemetry: none
DirectoryId *string `access:"authentication_openid"` // telemetry: none
}
func (s *Office365Settings) setDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.Id == nil {
s.Id = NewString("")
}
if s.Secret == nil {
s.Secret = NewString("")
}
if s.Scope == nil {
s.Scope = NewString(Office365SettingsDefaultScope)
}
if s.DiscoveryEndpoint == nil {
s.DiscoveryEndpoint = NewString("")
}
if s.AuthEndpoint == nil {
s.AuthEndpoint = NewString(Office365SettingsDefaultAuthEndpoint)
}
if s.TokenEndpoint == nil {
s.TokenEndpoint = NewString(Office365SettingsDefaultTokenEndpoint)
}
if s.UserAPIEndpoint == nil {
s.UserAPIEndpoint = NewString(Office365SettingsDefaultUserAPIEndpoint)
}
if s.DirectoryId == nil {
s.DirectoryId = NewString("")
}
}
func (s *Office365Settings) SSOSettings() *SSOSettings {
ssoSettings := SSOSettings{}
ssoSettings.Enable = s.Enable
ssoSettings.Secret = s.Secret
ssoSettings.Id = s.Id
ssoSettings.Scope = s.Scope
ssoSettings.DiscoveryEndpoint = s.DiscoveryEndpoint
ssoSettings.AuthEndpoint = s.AuthEndpoint
ssoSettings.TokenEndpoint = s.TokenEndpoint
ssoSettings.UserAPIEndpoint = s.UserAPIEndpoint
return &ssoSettings
}
type ReplicaLagSettings struct {
DataSource *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
QueryAbsoluteLag *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
QueryTimeLag *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none
}
type SqlSettings struct {
DriverName *string `access:"environment_database,write_restrictable,cloud_restrictable"`
DataSource *string `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
DataSourceReplicas []string `access:"environment_database,write_restrictable,cloud_restrictable"`
DataSourceSearchReplicas []string `access:"environment_database,write_restrictable,cloud_restrictable"`
MaxIdleConns *int `access:"environment_database,write_restrictable,cloud_restrictable"`
ConnMaxLifetimeMilliseconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
ConnMaxIdleTimeMilliseconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
MaxOpenConns *int `access:"environment_database,write_restrictable,cloud_restrictable"`
Trace *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
AtRestEncryptKey *string `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
QueryTimeout *int `access:"environment_database,write_restrictable,cloud_restrictable"`
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
}
func (s *SqlSettings) SetDefaults(isUpdate bool) {
if s.DriverName == nil {
s.DriverName = NewString(DatabaseDriverPostgres)
}
if s.DataSource == nil {
s.DataSource = NewString(SqlSettingsDefaultDataSource)
}
if s.DataSourceReplicas == nil {
s.DataSourceReplicas = []string{}
}
if s.DataSourceSearchReplicas == nil {
s.DataSourceSearchReplicas = []string{}
}
if isUpdate {
// When updating an existing configuration, ensure an encryption key has been specified.
if s.AtRestEncryptKey == nil || *s.AtRestEncryptKey == "" {
s.AtRestEncryptKey = NewString(NewRandomString(32))
}
} else {
// When generating a blank configuration, leave this key empty to be generated on server start.
s.AtRestEncryptKey = NewString("")
}
if s.MaxIdleConns == nil {
s.MaxIdleConns = NewInt(20)
}
if s.MaxOpenConns == nil {
s.MaxOpenConns = NewInt(300)
}
if s.ConnMaxLifetimeMilliseconds == nil {
s.ConnMaxLifetimeMilliseconds = NewInt(3600000)
}
if s.ConnMaxIdleTimeMilliseconds == nil {
s.ConnMaxIdleTimeMilliseconds = NewInt(300000)
}
if s.Trace == nil {
s.Trace = NewBool(false)
}
if s.QueryTimeout == nil {
s.QueryTimeout = NewInt(30)
}
if s.DisableDatabaseSearch == nil {
s.DisableDatabaseSearch = NewBool(false)
}
if s.MigrationsStatementTimeoutSeconds == nil {
s.MigrationsStatementTimeoutSeconds = NewInt(100000)
}
if s.ReplicaLagSettings == nil {
s.ReplicaLagSettings = []*ReplicaLagSettings{}
}
}
type LogSettings struct {
EnableConsole *bool `access:"environment_logging,write_restrictable,cloud_restrictable"`
ConsoleLevel *string `access:"environment_logging,write_restrictable,cloud_restrictable"`
ConsoleJson *bool `access:"environment_logging,write_restrictable,cloud_restrictable"`
EnableColor *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none
EnableFile *bool `access:"environment_logging,write_restrictable,cloud_restrictable"`
FileLevel *string `access:"environment_logging,write_restrictable,cloud_restrictable"`
FileJson *bool `access:"environment_logging,write_restrictable,cloud_restrictable"`
FileLocation *string `access:"environment_logging,write_restrictable,cloud_restrictable"`
EnableWebhookDebugging *bool `access:"environment_logging,write_restrictable,cloud_restrictable"`
EnableDiagnostics *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none
VerboseDiagnostics *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none
EnableSentry *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none
AdvancedLoggingConfig *string `access:"environment_logging,write_restrictable,cloud_restrictable"`
}
func NewLogSettings() *LogSettings {
settings := &LogSettings{}
settings.SetDefaults()
return settings
}
func (s *LogSettings) SetDefaults() {
if s.EnableConsole == nil {
s.EnableConsole = NewBool(true)
}
if s.ConsoleLevel == nil {
s.ConsoleLevel = NewString("DEBUG")
}
if s.EnableColor == nil {
s.EnableColor = NewBool(false)
}
if s.EnableFile == nil {
s.EnableFile = NewBool(true)
}
if s.FileLevel == nil {
s.FileLevel = NewString("INFO")
}
if s.FileLocation == nil {
s.FileLocation = NewString("")
}
if s.EnableWebhookDebugging == nil {
s.EnableWebhookDebugging = NewBool(true)
}
if s.EnableDiagnostics == nil {
s.EnableDiagnostics = NewBool(true)
}
if s.VerboseDiagnostics == nil {
s.VerboseDiagnostics = NewBool(false)
}
if s.EnableSentry == nil {
s.EnableSentry = NewBool(*s.EnableDiagnostics)
}
if s.ConsoleJson == nil {
s.ConsoleJson = NewBool(true)
}
if s.FileJson == nil {
s.FileJson = NewBool(true)
}
if s.AdvancedLoggingConfig == nil {
s.AdvancedLoggingConfig = NewString("")
}
}
type ExperimentalAuditSettings struct {
FileEnabled *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
FileName *string `access:"experimental_features,write_restrictable,cloud_restrictable"` // telemetry: none
FileMaxSizeMB *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
FileMaxAgeDays *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
FileMaxBackups *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
FileCompress *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
FileMaxQueueSize *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
AdvancedLoggingConfig *string `access:"experimental_features,write_restrictable,cloud_restrictable"`
}
func (s *ExperimentalAuditSettings) SetDefaults() {
if s.FileEnabled == nil {
s.FileEnabled = NewBool(false)
}
if s.FileName == nil {
s.FileName = NewString("")
}
if s.FileMaxSizeMB == nil {
s.FileMaxSizeMB = NewInt(100)
}
if s.FileMaxAgeDays == nil {
s.FileMaxAgeDays = NewInt(0) // no limit on age
}
if s.FileMaxBackups == nil { // no limit on number of backups
s.FileMaxBackups = NewInt(0)
}
if s.FileCompress == nil {
s.FileCompress = NewBool(false)
}
if s.FileMaxQueueSize == nil {
s.FileMaxQueueSize = NewInt(1000)
}
if s.AdvancedLoggingConfig == nil {
s.AdvancedLoggingConfig = NewString("")
}
}
type NotificationLogSettings struct {
EnableConsole *bool `access:"write_restrictable,cloud_restrictable"`
ConsoleLevel *string `access:"write_restrictable,cloud_restrictable"`
ConsoleJson *bool `access:"write_restrictable,cloud_restrictable"`
EnableColor *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
EnableFile *bool `access:"write_restrictable,cloud_restrictable"`
FileLevel *string `access:"write_restrictable,cloud_restrictable"`
FileJson *bool `access:"write_restrictable,cloud_restrictable"`
FileLocation *string `access:"write_restrictable,cloud_restrictable"`
AdvancedLoggingConfig *string `access:"write_restrictable,cloud_restrictable"`
}
func (s *NotificationLogSettings) SetDefaults() {
if s.EnableConsole == nil {
s.EnableConsole = NewBool(true)
}
if s.ConsoleLevel == nil {
s.ConsoleLevel = NewString("DEBUG")
}
if s.EnableFile == nil {
s.EnableFile = NewBool(true)
}
if s.FileLevel == nil {
s.FileLevel = NewString("INFO")
}
if s.FileLocation == nil {
s.FileLocation = NewString("")
}
if s.ConsoleJson == nil {
s.ConsoleJson = NewBool(true)
}
if s.EnableColor == nil {
s.EnableColor = NewBool(false)
}
if s.FileJson == nil {
s.FileJson = NewBool(true)
}
if s.AdvancedLoggingConfig == nil {
s.AdvancedLoggingConfig = NewString("")
}
}
type PasswordSettings struct {
MinimumLength *int `access:"authentication_password"`
Lowercase *bool `access:"authentication_password"`
Number *bool `access:"authentication_password"`
Uppercase *bool `access:"authentication_password"`
Symbol *bool `access:"authentication_password"`
}
func (s *PasswordSettings) SetDefaults() {
if s.MinimumLength == nil {
s.MinimumLength = NewInt(8)
}
if s.Lowercase == nil {
s.Lowercase = NewBool(false)
}
if s.Number == nil {
s.Number = NewBool(false)
}
if s.Uppercase == nil {
s.Uppercase = NewBool(false)
}
if s.Symbol == nil {
s.Symbol = NewBool(false)
}
}
type FileSettings struct {
EnableFileAttachments *bool `access:"site_file_sharing_and_downloads"`
EnableMobileUpload *bool `access:"site_file_sharing_and_downloads"`
EnableMobileDownload *bool `access:"site_file_sharing_and_downloads"`
MaxFileSize *int64 `access:"environment_file_storage,cloud_restrictable"`
MaxImageResolution *int64 `access:"environment_file_storage,cloud_restrictable"`
MaxImageDecoderConcurrency *int64 `access:"environment_file_storage,cloud_restrictable"`
DriverName *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
Directory *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
EnablePublicLink *bool `access:"site_public_links,cloud_restrictable"`
ExtractContent *bool `access:"environment_file_storage,write_restrictable"`
ArchiveRecursion *bool `access:"environment_file_storage,write_restrictable"`
PublicLinkSalt *string `access:"site_public_links,cloud_restrictable"` // telemetry: none
InitialFont *string `access:"environment_file_storage,cloud_restrictable"` // telemetry: none
AmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3Bucket *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3Region *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
AmazonS3SSL *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
AmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
AmazonS3SSE *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
AmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
AmazonS3RequestTimeoutMilliseconds *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
}
func (s *FileSettings) SetDefaults(isUpdate bool) {
if s.EnableFileAttachments == nil {
s.EnableFileAttachments = NewBool(true)
}
if s.EnableMobileUpload == nil {
s.EnableMobileUpload = NewBool(true)
}
if s.EnableMobileDownload == nil {
s.EnableMobileDownload = NewBool(true)
}
if s.MaxFileSize == nil {
s.MaxFileSize = NewInt64(100 * 1024 * 1024) // 100MB (IEC)
}
if s.MaxImageResolution == nil {
s.MaxImageResolution = NewInt64(7680 * 4320) // 8K, ~33MPX
}
if s.MaxImageDecoderConcurrency == nil {
s.MaxImageDecoderConcurrency = NewInt64(-1) // Default to NumCPU
}
if s.DriverName == nil {
s.DriverName = NewString(ImageDriverLocal)
}
if s.Directory == nil || *s.Directory == "" {
s.Directory = NewString(FileSettingsDefaultDirectory)
}
if s.EnablePublicLink == nil {
s.EnablePublicLink = NewBool(false)
}
if s.ExtractContent == nil {
s.ExtractContent = NewBool(true)
}
if s.ArchiveRecursion == nil {
s.ArchiveRecursion = NewBool(false)
}
if isUpdate {
// When updating an existing configuration, ensure link salt has been specified.
if s.PublicLinkSalt == nil || *s.PublicLinkSalt == "" {
s.PublicLinkSalt = NewString(NewRandomString(32))
}
} else {
// When generating a blank configuration, leave link salt empty to be generated on server start.
s.PublicLinkSalt = NewString("")
}
if s.InitialFont == nil {
// Defaults to "nunito-bold.ttf"
s.InitialFont = NewString("nunito-bold.ttf")
}
if s.AmazonS3AccessKeyId == nil {
s.AmazonS3AccessKeyId = NewString("")
}
if s.AmazonS3SecretAccessKey == nil {
s.AmazonS3SecretAccessKey = NewString("")
}
if s.AmazonS3Bucket == nil {
s.AmazonS3Bucket = NewString("")
}
if s.AmazonS3PathPrefix == nil {
s.AmazonS3PathPrefix = NewString("")
}
if s.AmazonS3Region == nil {
s.AmazonS3Region = NewString("")
}
if s.AmazonS3Endpoint == nil || *s.AmazonS3Endpoint == "" {
// Defaults to "s3.amazonaws.com"
s.AmazonS3Endpoint = NewString("s3.amazonaws.com")
}
if s.AmazonS3SSL == nil {
s.AmazonS3SSL = NewBool(true) // Secure by default.
}
if s.AmazonS3SignV2 == nil {
s.AmazonS3SignV2 = new(bool)
// Signature v2 is not enabled by default.
}
if s.AmazonS3SSE == nil {
s.AmazonS3SSE = NewBool(false) // Not Encrypted by default.
}
if s.AmazonS3Trace == nil {
s.AmazonS3Trace = NewBool(false)
}
if s.AmazonS3RequestTimeoutMilliseconds == nil {
s.AmazonS3RequestTimeoutMilliseconds = NewInt64(30000)
}
}
func (s *FileSettings) ToFileBackendSettings(enableComplianceFeature bool, skipVerify bool) filestore.FileBackendSettings {
if *s.DriverName == ImageDriverLocal {
return filestore.FileBackendSettings{
DriverName: *s.DriverName,
Directory: *s.Directory,
}
}
return filestore.FileBackendSettings{
DriverName: *s.DriverName,
AmazonS3AccessKeyId: *s.AmazonS3AccessKeyId,
AmazonS3SecretAccessKey: *s.AmazonS3SecretAccessKey,
AmazonS3Bucket: *s.AmazonS3Bucket,
AmazonS3PathPrefix: *s.AmazonS3PathPrefix,
AmazonS3Region: *s.AmazonS3Region,
AmazonS3Endpoint: *s.AmazonS3Endpoint,
AmazonS3SSL: s.AmazonS3SSL == nil || *s.AmazonS3SSL,
AmazonS3SignV2: s.AmazonS3SignV2 != nil && *s.AmazonS3SignV2,
AmazonS3SSE: s.AmazonS3SSE != nil && *s.AmazonS3SSE && enableComplianceFeature,
AmazonS3Trace: s.AmazonS3Trace != nil && *s.AmazonS3Trace,
AmazonS3RequestTimeoutMilliseconds: *s.AmazonS3RequestTimeoutMilliseconds,
SkipVerify: skipVerify,
}
}
type EmailSettings struct {
EnableSignUpWithEmail *bool `access:"authentication_email"`
EnableSignInWithEmail *bool `access:"authentication_email"`
EnableSignInWithUsername *bool `access:"authentication_email"`
SendEmailNotifications *bool `access:"site_notifications"`
UseChannelInEmailNotifications *bool `access:"experimental_features"`
RequireEmailVerification *bool `access:"authentication_email"`
FeedbackName *string `access:"site_notifications"`
FeedbackEmail *string `access:"site_notifications,cloud_restrictable"`
ReplyToAddress *string `access:"site_notifications,cloud_restrictable"`
FeedbackOrganization *string `access:"site_notifications"`
EnableSMTPAuth *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"`
SMTPUsername *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none
SMTPPassword *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none
SMTPServer *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none
SMTPPort *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none
SMTPServerTimeout *int `access:"cloud_restrictable"`
ConnectionSecurity *string `access:"environment_smtp,write_restrictable,cloud_restrictable"`
SendPushNotifications *bool `access:"environment_push_notification_server"`
PushNotificationServer *string `access:"environment_push_notification_server"` // telemetry: none
PushNotificationContents *string `access:"site_notifications"`
PushNotificationBuffer *int // telemetry: none
EnableEmailBatching *bool `access:"site_notifications"`
EmailBatchingBufferSize *int `access:"experimental_features"`
EmailBatchingInterval *int `access:"experimental_features"`
EnablePreviewModeBanner *bool `access:"site_notifications"`
SkipServerCertificateVerification *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"`
EmailNotificationContentsType *string `access:"site_notifications"`
LoginButtonColor *string `access:"experimental_features"`
LoginButtonBorderColor *string `access:"experimental_features"`
LoginButtonTextColor *string `access:"experimental_features"`
}
func (s *EmailSettings) SetDefaults(isUpdate bool) {
if s.EnableSignUpWithEmail == nil {
s.EnableSignUpWithEmail = NewBool(true)
}
if s.EnableSignInWithEmail == nil {
s.EnableSignInWithEmail = NewBool(*s.EnableSignUpWithEmail)
}
if s.EnableSignInWithUsername == nil {
s.EnableSignInWithUsername = NewBool(true)
}
if s.SendEmailNotifications == nil {
s.SendEmailNotifications = NewBool(true)
}
if s.UseChannelInEmailNotifications == nil {
s.UseChannelInEmailNotifications = NewBool(false)
}
if s.RequireEmailVerification == nil {
s.RequireEmailVerification = NewBool(false)
}
if s.FeedbackName == nil {
s.FeedbackName = NewString("")
}
if s.FeedbackEmail == nil {
s.FeedbackEmail = NewString("test@example.com")
}
if s.ReplyToAddress == nil {
s.ReplyToAddress = NewString("test@example.com")
}
if s.FeedbackOrganization == nil {
s.FeedbackOrganization = NewString(EmailSettingsDefaultFeedbackOrganization)
}
if s.EnableSMTPAuth == nil {
if s.ConnectionSecurity == nil || *s.ConnectionSecurity == ConnSecurityNone {
s.EnableSMTPAuth = NewBool(false)
} else {
s.EnableSMTPAuth = NewBool(true)
}
}
if s.SMTPUsername == nil {
s.SMTPUsername = NewString("")
}
if s.SMTPPassword == nil {
s.SMTPPassword = NewString("")
}
if s.SMTPServer == nil || *s.SMTPServer == "" {
s.SMTPServer = NewString(EmailSMTPDefaultServer)
}
if s.SMTPPort == nil || *s.SMTPPort == "" {
s.SMTPPort = NewString(EmailSMTPDefaultPort)
}
if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 {
s.SMTPServerTimeout = NewInt(10)
}
if s.ConnectionSecurity == nil || *s.ConnectionSecurity == ConnSecurityPlain {
s.ConnectionSecurity = NewString(ConnSecurityNone)
}
if s.SendPushNotifications == nil {
s.SendPushNotifications = NewBool(!isUpdate)
}
if s.PushNotificationServer == nil {
if isUpdate {
s.PushNotificationServer = NewString("")
} else {
s.PushNotificationServer = NewString(GenericNotificationServer)
}
}
if s.PushNotificationContents == nil {
s.PushNotificationContents = NewString(FullNotification)
}
if s.PushNotificationBuffer == nil {
s.PushNotificationBuffer = NewInt(1000)
}
if s.EnableEmailBatching == nil {
s.EnableEmailBatching = NewBool(false)
}
if s.EmailBatchingBufferSize == nil {
s.EmailBatchingBufferSize = NewInt(EmailBatchingBufferSize)
}
if s.EmailBatchingInterval == nil {
s.EmailBatchingInterval = NewInt(EmailBatchingInterval)
}
if s.EnablePreviewModeBanner == nil {
s.EnablePreviewModeBanner = NewBool(true)
}
if s.EnableSMTPAuth == nil {
if *s.ConnectionSecurity == ConnSecurityNone {
s.EnableSMTPAuth = NewBool(false)
} else {
s.EnableSMTPAuth = NewBool(true)
}
}
if *s.ConnectionSecurity == ConnSecurityPlain {
*s.ConnectionSecurity = ConnSecurityNone
}
if s.SkipServerCertificateVerification == nil {
s.SkipServerCertificateVerification = NewBool(false)
}
if s.EmailNotificationContentsType == nil {
s.EmailNotificationContentsType = NewString(EmailNotificationContentsFull)
}
if s.LoginButtonColor == nil {
s.LoginButtonColor = NewString("#0000")
}
if s.LoginButtonBorderColor == nil {
s.LoginButtonBorderColor = NewString("#2389D7")
}
if s.LoginButtonTextColor == nil {
s.LoginButtonTextColor = NewString("#2389D7")
}
}
type RateLimitSettings struct {
Enable *bool `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
PerSec *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
MaxBurst *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
MemoryStoreSize *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
VaryByRemoteAddr *bool `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
VaryByUser *bool `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
VaryByHeader string `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"`
}
func (s *RateLimitSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.PerSec == nil {
s.PerSec = NewInt(10)
}
if s.MaxBurst == nil {
s.MaxBurst = NewInt(100)
}
if s.MemoryStoreSize == nil {
s.MemoryStoreSize = NewInt(10000)
}
if s.VaryByRemoteAddr == nil {
s.VaryByRemoteAddr = NewBool(true)
}
if s.VaryByUser == nil {
s.VaryByUser = NewBool(false)
}
}
type PrivacySettings struct {
ShowEmailAddress *bool `access:"site_users_and_teams"`
ShowFullName *bool `access:"site_users_and_teams"`
}
func (s *PrivacySettings) setDefaults() {
if s.ShowEmailAddress == nil {
s.ShowEmailAddress = NewBool(true)
}
if s.ShowFullName == nil {
s.ShowFullName = NewBool(true)
}
}
type SupportSettings struct {
TermsOfServiceLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
PrivacyPolicyLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
AboutLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
HelpLink *string `access:"site_customization"`
ReportAProblemLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
SupportEmail *string `access:"site_notifications"`
CustomTermsOfServiceEnabled *bool `access:"compliance_custom_terms_of_service"`
CustomTermsOfServiceReAcceptancePeriod *int `access:"compliance_custom_terms_of_service"`
EnableAskCommunityLink *bool `access:"site_customization"`
}
func (s *SupportSettings) SetDefaults() {
if !isSafeLink(s.TermsOfServiceLink) {
*s.TermsOfServiceLink = SupportSettingsDefaultTermsOfServiceLink
}
if s.TermsOfServiceLink == nil {
s.TermsOfServiceLink = NewString(SupportSettingsDefaultTermsOfServiceLink)
}
if !isSafeLink(s.PrivacyPolicyLink) {
*s.PrivacyPolicyLink = ""
}
if s.PrivacyPolicyLink == nil {
s.PrivacyPolicyLink = NewString(SupportSettingsDefaultPrivacyPolicyLink)
}
if !isSafeLink(s.AboutLink) {
*s.AboutLink = ""
}
if s.AboutLink == nil {
s.AboutLink = NewString(SupportSettingsDefaultAboutLink)
}
if !isSafeLink(s.HelpLink) {
*s.HelpLink = ""
}
if s.HelpLink == nil {
s.HelpLink = NewString(SupportSettingsDefaultHelpLink)
}
if !isSafeLink(s.ReportAProblemLink) {
*s.ReportAProblemLink = ""
}
if s.ReportAProblemLink == nil {
s.ReportAProblemLink = NewString(SupportSettingsDefaultReportAProblemLink)
}
if s.SupportEmail == nil {
s.SupportEmail = NewString(SupportSettingsDefaultSupportEmail)
}
if s.CustomTermsOfServiceEnabled == nil {
s.CustomTermsOfServiceEnabled = NewBool(false)
}
if s.CustomTermsOfServiceReAcceptancePeriod == nil {
s.CustomTermsOfServiceReAcceptancePeriod = NewInt(SupportSettingsDefaultReAcceptancePeriod)
}
if s.EnableAskCommunityLink == nil {
s.EnableAskCommunityLink = NewBool(true)
}
}
type AnnouncementSettings struct {
EnableBanner *bool `access:"site_announcement_banner"`
BannerText *string `access:"site_announcement_banner"` // telemetry: none
BannerColor *string `access:"site_announcement_banner"`
BannerTextColor *string `access:"site_announcement_banner"`
AllowBannerDismissal *bool `access:"site_announcement_banner"`
AdminNoticesEnabled *bool `access:"site_notices"`
UserNoticesEnabled *bool `access:"site_notices"`
NoticesURL *string `access:"site_notices,write_restrictable"` // telemetry: none
NoticesFetchFrequency *int `access:"site_notices,write_restrictable"` // telemetry: none
NoticesSkipCache *bool `access:"site_notices,write_restrictable"` // telemetry: none
}
func (s *AnnouncementSettings) SetDefaults() {
if s.EnableBanner == nil {
s.EnableBanner = NewBool(false)
}
if s.BannerText == nil {
s.BannerText = NewString("")
}
if s.BannerColor == nil {
s.BannerColor = NewString(AnnouncementSettingsDefaultBannerColor)
}
if s.BannerTextColor == nil {
s.BannerTextColor = NewString(AnnouncementSettingsDefaultBannerTextColor)
}
if s.AllowBannerDismissal == nil {
s.AllowBannerDismissal = NewBool(true)
}
if s.AdminNoticesEnabled == nil {
s.AdminNoticesEnabled = NewBool(true)
}
if s.UserNoticesEnabled == nil {
s.UserNoticesEnabled = NewBool(true)
}
if s.NoticesURL == nil {
s.NoticesURL = NewString(AnnouncementSettingsDefaultNoticesJsonURL)
}
if s.NoticesSkipCache == nil {
s.NoticesSkipCache = NewBool(false)
}
if s.NoticesFetchFrequency == nil {
s.NoticesFetchFrequency = NewInt(AnnouncementSettingsDefaultNoticesFetchFrequencySeconds)
}
}
type ThemeSettings struct {
EnableThemeSelection *bool `access:"experimental_features"`
DefaultTheme *string `access:"experimental_features"`
AllowCustomThemes *bool `access:"experimental_features"`
AllowedThemes []string
}
func (s *ThemeSettings) SetDefaults() {
if s.EnableThemeSelection == nil {
s.EnableThemeSelection = NewBool(true)
}
if s.DefaultTheme == nil {
s.DefaultTheme = NewString(TeamSettingsDefaultTeamText)
}
if s.AllowCustomThemes == nil {
s.AllowCustomThemes = NewBool(true)
}
if s.AllowedThemes == nil {
s.AllowedThemes = []string{}
}
}
type TeamSettings struct {
SiteName *string `access:"site_customization"`
MaxUsersPerTeam *int `access:"site_users_and_teams"`
EnableUserCreation *bool `access:"authentication_signup"`
EnableOpenServer *bool `access:"authentication_signup"`
EnableUserDeactivation *bool `access:"experimental_features"`
RestrictCreationToDomains *string `access:"authentication_signup"` // telemetry: none
EnableCustomUserStatuses *bool `access:"site_users_and_teams"`
EnableCustomBrand *bool `access:"site_customization"`
CustomBrandText *string `access:"site_customization"`
CustomDescriptionText *string `access:"site_customization"`
RestrictDirectMessage *string `access:"site_users_and_teams"`
EnableLastActiveTime *bool `access:"site_users_and_teams"`
// In seconds.
UserStatusAwayTimeout *int64 `access:"experimental_features"`
MaxChannelsPerTeam *int64 `access:"site_users_and_teams"`
MaxNotificationsPerChannel *int64 `access:"environment_push_notification_server"`
EnableConfirmNotificationsToChannel *bool `access:"site_notifications"`
TeammateNameDisplay *string `access:"site_users_and_teams"`
ExperimentalViewArchivedChannels *bool `access:"experimental_features,site_users_and_teams"`
ExperimentalEnableAutomaticReplies *bool `access:"experimental_features"`
LockTeammateNameDisplay *bool `access:"site_users_and_teams"`
ExperimentalPrimaryTeam *string `access:"experimental_features"`
ExperimentalDefaultChannels []string `access:"experimental_features"`
}
func (s *TeamSettings) SetDefaults() {
if s.SiteName == nil || *s.SiteName == "" {
s.SiteName = NewString(TeamSettingsDefaultSiteName)
}
if s.MaxUsersPerTeam == nil {
s.MaxUsersPerTeam = NewInt(TeamSettingsDefaultMaxUsersPerTeam)
}
if s.EnableUserCreation == nil {
s.EnableUserCreation = NewBool(true)
}
if s.EnableOpenServer == nil {
s.EnableOpenServer = NewBool(false)
}
if s.RestrictCreationToDomains == nil {
s.RestrictCreationToDomains = NewString("")
}
if s.EnableCustomUserStatuses == nil {
s.EnableCustomUserStatuses = NewBool(true)
}
if s.EnableLastActiveTime == nil {
s.EnableLastActiveTime = NewBool(true)
}
if s.EnableCustomBrand == nil {
s.EnableCustomBrand = NewBool(false)
}
if s.EnableUserDeactivation == nil {
s.EnableUserDeactivation = NewBool(false)
}
if s.CustomBrandText == nil {
s.CustomBrandText = NewString(TeamSettingsDefaultCustomBrandText)
}
if s.CustomDescriptionText == nil {
s.CustomDescriptionText = NewString(TeamSettingsDefaultCustomDescriptionText)
}
if s.RestrictDirectMessage == nil {
s.RestrictDirectMessage = NewString(DirectMessageAny)
}
if s.UserStatusAwayTimeout == nil {
s.UserStatusAwayTimeout = NewInt64(TeamSettingsDefaultUserStatusAwayTimeout)
}
if s.MaxChannelsPerTeam == nil {
s.MaxChannelsPerTeam = NewInt64(2000)
}
if s.MaxNotificationsPerChannel == nil {
s.MaxNotificationsPerChannel = NewInt64(1000)
}
if s.EnableConfirmNotificationsToChannel == nil {
s.EnableConfirmNotificationsToChannel = NewBool(true)
}
if s.ExperimentalEnableAutomaticReplies == nil {
s.ExperimentalEnableAutomaticReplies = NewBool(false)
}
if s.ExperimentalPrimaryTeam == nil {
s.ExperimentalPrimaryTeam = NewString("")
}
if s.ExperimentalDefaultChannels == nil {
s.ExperimentalDefaultChannels = []string{}
}
if s.EnableUserCreation == nil {
s.EnableUserCreation = NewBool(true)
}
if s.ExperimentalViewArchivedChannels == nil {
s.ExperimentalViewArchivedChannels = NewBool(true)
}
if s.LockTeammateNameDisplay == nil {
s.LockTeammateNameDisplay = NewBool(false)
}
}
type ClientRequirements struct {
AndroidLatestVersion string `access:"write_restrictable,cloud_restrictable"`
AndroidMinVersion string `access:"write_restrictable,cloud_restrictable"`
IosLatestVersion string `access:"write_restrictable,cloud_restrictable"`
IosMinVersion string `access:"write_restrictable,cloud_restrictable"`
}
type LdapSettings struct {
// Basic
Enable *bool `access:"authentication_ldap"`
EnableSync *bool `access:"authentication_ldap"`
LdapServer *string `access:"authentication_ldap"` // telemetry: none
LdapPort *int `access:"authentication_ldap"` // telemetry: none
ConnectionSecurity *string `access:"authentication_ldap"`
BaseDN *string `access:"authentication_ldap"` // telemetry: none
BindUsername *string `access:"authentication_ldap"` // telemetry: none
BindPassword *string `access:"authentication_ldap"` // telemetry: none
// Filtering
UserFilter *string `access:"authentication_ldap"` // telemetry: none
GroupFilter *string `access:"authentication_ldap"`
GuestFilter *string `access:"authentication_ldap"`
EnableAdminFilter *bool
AdminFilter *string
// Group Mapping
GroupDisplayNameAttribute *string `access:"authentication_ldap"`
GroupIdAttribute *string `access:"authentication_ldap"`
// User Mapping
FirstNameAttribute *string `access:"authentication_ldap"`
LastNameAttribute *string `access:"authentication_ldap"`
EmailAttribute *string `access:"authentication_ldap"`
UsernameAttribute *string `access:"authentication_ldap"`
NicknameAttribute *string `access:"authentication_ldap"`
IdAttribute *string `access:"authentication_ldap"`
PositionAttribute *string `access:"authentication_ldap"`
LoginIdAttribute *string `access:"authentication_ldap"`
PictureAttribute *string `access:"authentication_ldap"`
// Synchronization
SyncIntervalMinutes *int `access:"authentication_ldap"`
// Advanced
SkipCertificateVerification *bool `access:"authentication_ldap"`
PublicCertificateFile *string `access:"authentication_ldap"`
PrivateKeyFile *string `access:"authentication_ldap"`
QueryTimeout *int `access:"authentication_ldap"`
MaxPageSize *int `access:"authentication_ldap"`
// Customization
LoginFieldName *string `access:"authentication_ldap"`
LoginButtonColor *string `access:"experimental_features"`
LoginButtonBorderColor *string `access:"experimental_features"`
LoginButtonTextColor *string `access:"experimental_features"`
Trace *bool `access:"authentication_ldap"` // telemetry: none
}
func (s *LdapSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
// When unset should default to LDAP Enabled
if s.EnableSync == nil {
s.EnableSync = NewBool(*s.Enable)
}
if s.EnableAdminFilter == nil {
s.EnableAdminFilter = NewBool(false)
}
if s.LdapServer == nil {
s.LdapServer = NewString("")
}
if s.LdapPort == nil {
s.LdapPort = NewInt(389)
}
if s.ConnectionSecurity == nil {
s.ConnectionSecurity = NewString("")
}
if s.PublicCertificateFile == nil {
s.PublicCertificateFile = NewString("")
}
if s.PrivateKeyFile == nil {
s.PrivateKeyFile = NewString("")
}
if s.BaseDN == nil {
s.BaseDN = NewString("")
}
if s.BindUsername == nil {
s.BindUsername = NewString("")
}
if s.BindPassword == nil {
s.BindPassword = NewString("")
}
if s.UserFilter == nil {
s.UserFilter = NewString("")
}
if s.GuestFilter == nil {
s.GuestFilter = NewString("")
}
if s.AdminFilter == nil {
s.AdminFilter = NewString("")
}
if s.GroupFilter == nil {
s.GroupFilter = NewString("")
}
if s.GroupDisplayNameAttribute == nil {
s.GroupDisplayNameAttribute = NewString(LdapSettingsDefaultGroupDisplayNameAttribute)
}
if s.GroupIdAttribute == nil {
s.GroupIdAttribute = NewString(LdapSettingsDefaultGroupIdAttribute)
}
if s.FirstNameAttribute == nil {
s.FirstNameAttribute = NewString(LdapSettingsDefaultFirstNameAttribute)
}
if s.LastNameAttribute == nil {
s.LastNameAttribute = NewString(LdapSettingsDefaultLastNameAttribute)
}
if s.EmailAttribute == nil {
s.EmailAttribute = NewString(LdapSettingsDefaultEmailAttribute)
}
if s.UsernameAttribute == nil {
s.UsernameAttribute = NewString(LdapSettingsDefaultUsernameAttribute)
}
if s.NicknameAttribute == nil {
s.NicknameAttribute = NewString(LdapSettingsDefaultNicknameAttribute)
}
if s.IdAttribute == nil {
s.IdAttribute = NewString(LdapSettingsDefaultIdAttribute)
}
if s.PositionAttribute == nil {
s.PositionAttribute = NewString(LdapSettingsDefaultPositionAttribute)
}
if s.PictureAttribute == nil {
s.PictureAttribute = NewString(LdapSettingsDefaultPictureAttribute)
}
// For those upgrading to the version when LoginIdAttribute was added
// they need IdAttribute == LoginIdAttribute not to break
if s.LoginIdAttribute == nil {
s.LoginIdAttribute = s.IdAttribute
}
if s.SyncIntervalMinutes == nil {
s.SyncIntervalMinutes = NewInt(60)
}
if s.SkipCertificateVerification == nil {
s.SkipCertificateVerification = NewBool(false)
}
if s.QueryTimeout == nil {
s.QueryTimeout = NewInt(60)
}
if s.MaxPageSize == nil {
s.MaxPageSize = NewInt(0)
}
if s.LoginFieldName == nil {
s.LoginFieldName = NewString(LdapSettingsDefaultLoginFieldName)
}
if s.LoginButtonColor == nil {
s.LoginButtonColor = NewString("#0000")
}
if s.LoginButtonBorderColor == nil {
s.LoginButtonBorderColor = NewString("#2389D7")
}
if s.LoginButtonTextColor == nil {
s.LoginButtonTextColor = NewString("#2389D7")
}
if s.Trace == nil {
s.Trace = NewBool(false)
}
}
type ComplianceSettings struct {
Enable *bool `access:"compliance_compliance_monitoring"`
Directory *string `access:"compliance_compliance_monitoring"` // telemetry: none
EnableDaily *bool `access:"compliance_compliance_monitoring"`
BatchSize *int `access:"compliance_compliance_monitoring"` // telemetry: none
}
func (s *ComplianceSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.Directory == nil {
s.Directory = NewString("./data/")
}
if s.EnableDaily == nil {
s.EnableDaily = NewBool(false)
}
if s.BatchSize == nil {
s.BatchSize = NewInt(30000)
}
}
type LocalizationSettings struct {
DefaultServerLocale *string `access:"site_localization"`
DefaultClientLocale *string `access:"site_localization"`
AvailableLocales *string `access:"site_localization"`
}
func (s *LocalizationSettings) SetDefaults() {
if s.DefaultServerLocale == nil {
s.DefaultServerLocale = NewString(DefaultLocale)
}
if s.DefaultClientLocale == nil {
s.DefaultClientLocale = NewString(DefaultLocale)
}
if s.AvailableLocales == nil {
s.AvailableLocales = NewString("")
}
}
type SamlSettings struct {
// Basic
Enable *bool `access:"authentication_saml"`
EnableSyncWithLdap *bool `access:"authentication_saml"`
EnableSyncWithLdapIncludeAuth *bool `access:"authentication_saml"`
IgnoreGuestsLdapSync *bool `access:"authentication_saml"`
Verify *bool `access:"authentication_saml"`
Encrypt *bool `access:"authentication_saml"`
SignRequest *bool `access:"authentication_saml"`
IdpURL *string `access:"authentication_saml"` // telemetry: none
IdpDescriptorURL *string `access:"authentication_saml"` // telemetry: none
IdpMetadataURL *string `access:"authentication_saml"` // telemetry: none
ServiceProviderIdentifier *string `access:"authentication_saml"` // telemetry: none
AssertionConsumerServiceURL *string `access:"authentication_saml"` // telemetry: none
SignatureAlgorithm *string `access:"authentication_saml"`
CanonicalAlgorithm *string `access:"authentication_saml"`
ScopingIDPProviderId *string `access:"authentication_saml"`
ScopingIDPName *string `access:"authentication_saml"`
IdpCertificateFile *string `access:"authentication_saml"` // telemetry: none
PublicCertificateFile *string `access:"authentication_saml"` // telemetry: none
PrivateKeyFile *string `access:"authentication_saml"` // telemetry: none
// User Mapping
IdAttribute *string `access:"authentication_saml"`
GuestAttribute *string `access:"authentication_saml"`
EnableAdminAttribute *bool
AdminAttribute *string
FirstNameAttribute *string `access:"authentication_saml"`
LastNameAttribute *string `access:"authentication_saml"`
EmailAttribute *string `access:"authentication_saml"`
UsernameAttribute *string `access:"authentication_saml"`
NicknameAttribute *string `access:"authentication_saml"`
LocaleAttribute *string `access:"authentication_saml"`
PositionAttribute *string `access:"authentication_saml"`
LoginButtonText *string `access:"authentication_saml"`
LoginButtonColor *string `access:"experimental_features"`
LoginButtonBorderColor *string `access:"experimental_features"`
LoginButtonTextColor *string `access:"experimental_features"`
}
func (s *SamlSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.EnableSyncWithLdap == nil {
s.EnableSyncWithLdap = NewBool(false)
}
if s.EnableSyncWithLdapIncludeAuth == nil {
s.EnableSyncWithLdapIncludeAuth = NewBool(false)
}
if s.IgnoreGuestsLdapSync == nil {
s.IgnoreGuestsLdapSync = NewBool(false)
}
if s.EnableAdminAttribute == nil {
s.EnableAdminAttribute = NewBool(false)
}
if s.Verify == nil {
s.Verify = NewBool(true)
}
if s.Encrypt == nil {
s.Encrypt = NewBool(true)
}
if s.SignRequest == nil {
s.SignRequest = NewBool(false)
}
if s.SignatureAlgorithm == nil {
s.SignatureAlgorithm = NewString(SamlSettingsDefaultSignatureAlgorithm)
}
if s.CanonicalAlgorithm == nil {
s.CanonicalAlgorithm = NewString(SamlSettingsDefaultCanonicalAlgorithm)
}
if s.IdpURL == nil {
s.IdpURL = NewString("")
}
if s.IdpDescriptorURL == nil {
s.IdpDescriptorURL = NewString("")
}
if s.ServiceProviderIdentifier == nil {
if s.IdpDescriptorURL != nil {
s.ServiceProviderIdentifier = NewString(*s.IdpDescriptorURL)
} else {
s.ServiceProviderIdentifier = NewString("")
}
}
if s.IdpMetadataURL == nil {
s.IdpMetadataURL = NewString("")
}
if s.IdpCertificateFile == nil {
s.IdpCertificateFile = NewString("")
}
if s.PublicCertificateFile == nil {
s.PublicCertificateFile = NewString("")
}
if s.PrivateKeyFile == nil {
s.PrivateKeyFile = NewString("")
}
if s.AssertionConsumerServiceURL == nil {
s.AssertionConsumerServiceURL = NewString("")
}
if s.ScopingIDPProviderId == nil {
s.ScopingIDPProviderId = NewString("")
}
if s.ScopingIDPName == nil {
s.ScopingIDPName = NewString("")
}
if s.LoginButtonText == nil || *s.LoginButtonText == "" {
s.LoginButtonText = NewString(UserAuthServiceSamlText)
}
if s.IdAttribute == nil {
s.IdAttribute = NewString(SamlSettingsDefaultIdAttribute)
}
if s.GuestAttribute == nil {
s.GuestAttribute = NewString(SamlSettingsDefaultGuestAttribute)
}
if s.AdminAttribute == nil {
s.AdminAttribute = NewString(SamlSettingsDefaultAdminAttribute)
}
if s.FirstNameAttribute == nil {
s.FirstNameAttribute = NewString(SamlSettingsDefaultFirstNameAttribute)
}
if s.LastNameAttribute == nil {
s.LastNameAttribute = NewString(SamlSettingsDefaultLastNameAttribute)
}
if s.EmailAttribute == nil {
s.EmailAttribute = NewString(SamlSettingsDefaultEmailAttribute)
}
if s.UsernameAttribute == nil {
s.UsernameAttribute = NewString(SamlSettingsDefaultUsernameAttribute)
}
if s.NicknameAttribute == nil {
s.NicknameAttribute = NewString(SamlSettingsDefaultNicknameAttribute)
}
if s.PositionAttribute == nil {
s.PositionAttribute = NewString(SamlSettingsDefaultPositionAttribute)
}
if s.LocaleAttribute == nil {
s.LocaleAttribute = NewString(SamlSettingsDefaultLocaleAttribute)
}
if s.LoginButtonColor == nil {
s.LoginButtonColor = NewString("#34a28b")
}
if s.LoginButtonBorderColor == nil {
s.LoginButtonBorderColor = NewString("#2389D7")
}
if s.LoginButtonTextColor == nil {
s.LoginButtonTextColor = NewString("#ffffff")
}
}
type NativeAppSettings struct {
AppCustomURLSchemes []string `access:"site_customization,write_restrictable,cloud_restrictable"` // telemetry: none
AppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
AndroidAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
IosAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
}
func (s *NativeAppSettings) SetDefaults() {
if s.AppDownloadLink == nil {
s.AppDownloadLink = NewString(NativeappSettingsDefaultAppDownloadLink)
}
if s.AndroidAppDownloadLink == nil {
s.AndroidAppDownloadLink = NewString(NativeappSettingsDefaultAndroidAppDownloadLink)
}
if s.IosAppDownloadLink == nil {
s.IosAppDownloadLink = NewString(NativeappSettingsDefaultIosAppDownloadLink)
}
if s.AppCustomURLSchemes == nil {
s.AppCustomURLSchemes = GetDefaultAppCustomURLSchemes()
}
}
type ElasticsearchSettings struct {
ConnectionURL *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
Username *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
Password *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
EnableIndexing *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
EnableSearching *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
EnableAutocomplete *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
Sniff *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
PostIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
PostIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
ChannelIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
ChannelIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
UserIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
UserIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
AggregatePostsAfterDays *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none
PostsAggregatorJobStartTime *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none
IndexPrefix *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
LiveIndexingBatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
BulkIndexingTimeWindowSeconds *int `json:",omitempty"` // telemetry: none
BatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
RequestTimeoutSeconds *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
SkipTLSVerification *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
CA *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
ClientCert *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
ClientKey *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
Trace *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"`
}
func (s *ElasticsearchSettings) SetDefaults() {
if s.ConnectionURL == nil {
s.ConnectionURL = NewString(ElasticsearchSettingsDefaultConnectionURL)
}
if s.Username == nil {
s.Username = NewString(ElasticsearchSettingsDefaultUsername)
}
if s.Password == nil {
s.Password = NewString(ElasticsearchSettingsDefaultPassword)
}
if s.CA == nil {
s.CA = NewString("")
}
if s.ClientCert == nil {
s.ClientCert = NewString("")
}
if s.ClientKey == nil {
s.ClientKey = NewString("")
}
if s.EnableIndexing == nil {
s.EnableIndexing = NewBool(false)
}
if s.EnableSearching == nil {
s.EnableSearching = NewBool(false)
}
if s.EnableAutocomplete == nil {
s.EnableAutocomplete = NewBool(false)
}
if s.Sniff == nil {
s.Sniff = NewBool(true)
}
if s.PostIndexReplicas == nil {
s.PostIndexReplicas = NewInt(ElasticsearchSettingsDefaultPostIndexReplicas)
}
if s.PostIndexShards == nil {
s.PostIndexShards = NewInt(ElasticsearchSettingsDefaultPostIndexShards)
}
if s.ChannelIndexReplicas == nil {
s.ChannelIndexReplicas = NewInt(ElasticsearchSettingsDefaultChannelIndexReplicas)
}
if s.ChannelIndexShards == nil {
s.ChannelIndexShards = NewInt(ElasticsearchSettingsDefaultChannelIndexShards)
}
if s.UserIndexReplicas == nil {
s.UserIndexReplicas = NewInt(ElasticsearchSettingsDefaultUserIndexReplicas)
}
if s.UserIndexShards == nil {
s.UserIndexShards = NewInt(ElasticsearchSettingsDefaultUserIndexShards)
}
if s.AggregatePostsAfterDays == nil {
s.AggregatePostsAfterDays = NewInt(ElasticsearchSettingsDefaultAggregatePostsAfterDays)
}
if s.PostsAggregatorJobStartTime == nil {
s.PostsAggregatorJobStartTime = NewString(ElasticsearchSettingsDefaultPostsAggregatorJobStartTime)
}
if s.IndexPrefix == nil {
s.IndexPrefix = NewString(ElasticsearchSettingsDefaultIndexPrefix)
}
if s.LiveIndexingBatchSize == nil {
s.LiveIndexingBatchSize = NewInt(ElasticsearchSettingsDefaultLiveIndexingBatchSize)
}
if s.BatchSize == nil {
s.BatchSize = NewInt(ElasticsearchSettingsDefaultBatchSize)
}
if s.RequestTimeoutSeconds == nil {
s.RequestTimeoutSeconds = NewInt(ElasticsearchSettingsDefaultRequestTimeoutSeconds)
}
if s.SkipTLSVerification == nil {
s.SkipTLSVerification = NewBool(false)
}
if s.Trace == nil {
s.Trace = NewString("")
}
}
type BleveSettings struct {
IndexDir *string `access:"experimental_bleve"` // telemetry: none
EnableIndexing *bool `access:"experimental_bleve"`
EnableSearching *bool `access:"experimental_bleve"`
EnableAutocomplete *bool `access:"experimental_bleve"`
BulkIndexingTimeWindowSeconds *int `json:",omitempty"` // telemetry: none
BatchSize *int `access:"experimental_bleve"`
}
func (bs *BleveSettings) SetDefaults() {
if bs.IndexDir == nil {
bs.IndexDir = NewString(BleveSettingsDefaultIndexDir)
}
if bs.EnableIndexing == nil {
bs.EnableIndexing = NewBool(false)
}
if bs.EnableSearching == nil {
bs.EnableSearching = NewBool(false)
}
if bs.EnableAutocomplete == nil {
bs.EnableAutocomplete = NewBool(false)
}
if bs.BatchSize == nil {
bs.BatchSize = NewInt(BleveSettingsDefaultBatchSize)
}
}
type DataRetentionSettings struct {
EnableMessageDeletion *bool `access:"compliance_data_retention_policy"`
EnableFileDeletion *bool `access:"compliance_data_retention_policy"`
EnableBoardsDeletion *bool `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"`
FileRetentionDays *int `access:"compliance_data_retention_policy"`
BoardsRetentionDays *int `access:"compliance_data_retention_policy"`
DeletionJobStartTime *string `access:"compliance_data_retention_policy"`
BatchSize *int `access:"compliance_data_retention_policy"`
}
func (s *DataRetentionSettings) SetDefaults() {
if s.EnableMessageDeletion == nil {
s.EnableMessageDeletion = NewBool(false)
}
if s.EnableFileDeletion == nil {
s.EnableFileDeletion = NewBool(false)
}
if s.EnableBoardsDeletion == nil {
s.EnableBoardsDeletion = NewBool(false)
}
if s.MessageRetentionDays == nil {
s.MessageRetentionDays = NewInt(DataRetentionSettingsDefaultMessageRetentionDays)
}
if s.FileRetentionDays == nil {
s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays)
}
if s.BoardsRetentionDays == nil {
s.BoardsRetentionDays = NewInt(DataRetentionSettingsDefaultBoardsRetentionDays)
}
if s.DeletionJobStartTime == nil {
s.DeletionJobStartTime = NewString(DataRetentionSettingsDefaultDeletionJobStartTime)
}
if s.BatchSize == nil {
s.BatchSize = NewInt(DataRetentionSettingsDefaultBatchSize)
}
}
type JobSettings struct {
RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
CleanupJobsThresholdDays *int `access:"write_restrictable,cloud_restrictable"`
CleanupConfigThresholdDays *int `access:"write_restrictable,cloud_restrictable"`
}
func (s *JobSettings) SetDefaults() {
if s.RunJobs == nil {
s.RunJobs = NewBool(true)
}
if s.RunScheduler == nil {
s.RunScheduler = NewBool(true)
}
if s.CleanupJobsThresholdDays == nil {
s.CleanupJobsThresholdDays = NewInt(-1)
}
if s.CleanupConfigThresholdDays == nil {
s.CleanupConfigThresholdDays = NewInt(-1)
}
}
type CloudSettings struct {
CWSURL *string `access:"write_restrictable"`
CWSAPIURL *string `access:"write_restrictable"`
}
func (s *CloudSettings) SetDefaults() {
if s.CWSURL == nil {
s.CWSURL = NewString(CloudSettingsDefaultCwsURL)
if !isProdLicensePublicKey {
s.CWSURL = NewString(CloudSettingsDefaultCwsURLTest)
}
}
if s.CWSAPIURL == nil {
s.CWSAPIURL = NewString(CloudSettingsDefaultCwsAPIURL)
if !isProdLicensePublicKey {
s.CWSAPIURL = NewString(CloudSettingsDefaultCwsAPIURLTest)
}
}
}
type ProductSettings struct {
EnablePublicSharedBoards *bool
}
func (s *ProductSettings) SetDefaults(plugins map[string]map[string]any) {
if s.EnablePublicSharedBoards == nil {
if p, ok := plugins[PluginIdFocalboard]; ok {
s.EnablePublicSharedBoards = NewBool(p["enablepublicsharedboards"].(bool))
} else {
s.EnablePublicSharedBoards = NewBool(false)
}
}
}
type PluginState struct {
Enable bool
}
type PluginSettings struct {
Enable *bool `access:"plugins,write_restrictable"`
EnableUploads *bool `access:"plugins,write_restrictable,cloud_restrictable"`
AllowInsecureDownloadURL *bool `access:"plugins,write_restrictable,cloud_restrictable"`
EnableHealthCheck *bool `access:"plugins,write_restrictable,cloud_restrictable"`
Directory *string `access:"plugins,write_restrictable,cloud_restrictable"` // telemetry: none
ClientDirectory *string `access:"plugins,write_restrictable,cloud_restrictable"` // telemetry: none
Plugins map[string]map[string]any `access:"plugins"` // telemetry: none
PluginStates map[string]*PluginState `access:"plugins"` // telemetry: none
EnableMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"`
EnableRemoteMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"`
AutomaticPrepackagedPlugins *bool `access:"plugins,write_restrictable,cloud_restrictable"`
RequirePluginSignature *bool `access:"plugins,write_restrictable,cloud_restrictable"`
MarketplaceURL *string `access:"plugins,write_restrictable,cloud_restrictable"`
SignaturePublicKeyFiles []string `access:"plugins,write_restrictable,cloud_restrictable"`
ChimeraOAuthProxyURL *string `access:"plugins,write_restrictable,cloud_restrictable"`
}
func (s *PluginSettings) SetDefaults(ls LogSettings) {
if s.Enable == nil {
s.Enable = NewBool(true)
}
if s.EnableUploads == nil {
s.EnableUploads = NewBool(false)
}
if s.AllowInsecureDownloadURL == nil {
s.AllowInsecureDownloadURL = NewBool(false)
}
if s.EnableHealthCheck == nil {
s.EnableHealthCheck = NewBool(true)
}
if s.Directory == nil || *s.Directory == "" {
s.Directory = NewString(PluginSettingsDefaultDirectory)
}
if s.ClientDirectory == nil || *s.ClientDirectory == "" {
s.ClientDirectory = NewString(PluginSettingsDefaultClientDirectory)
}
if s.Plugins == nil {
s.Plugins = make(map[string]map[string]any)
}
if s.PluginStates == nil {
s.PluginStates = make(map[string]*PluginState)
}
if s.PluginStates[PluginIdNPS] == nil {
// Enable the NPS plugin by default if diagnostics are enabled
s.PluginStates[PluginIdNPS] = &PluginState{Enable: ls.EnableDiagnostics == nil || *ls.EnableDiagnostics}
}
if s.PluginStates[PluginIdChannelExport] == nil && BuildEnterpriseReady == "true" {
// Enable the channel export plugin by default
s.PluginStates[PluginIdChannelExport] = &PluginState{Enable: true}
}
if s.PluginStates[PluginIdApps] == nil {
// Enable the Apps plugin by default
s.PluginStates[PluginIdApps] = &PluginState{Enable: true}
}
if s.PluginStates[PluginIdCalls] == nil {
// Enable the calls plugin by default
s.PluginStates[PluginIdCalls] = &PluginState{Enable: true}
}
if s.EnableMarketplace == nil {
s.EnableMarketplace = NewBool(PluginSettingsDefaultEnableMarketplace)
}
if s.EnableRemoteMarketplace == nil {
s.EnableRemoteMarketplace = NewBool(true)
}
if s.AutomaticPrepackagedPlugins == nil {
s.AutomaticPrepackagedPlugins = NewBool(true)
}
if s.MarketplaceURL == nil || *s.MarketplaceURL == "" || *s.MarketplaceURL == PluginSettingsOldMarketplaceURL {
s.MarketplaceURL = NewString(PluginSettingsDefaultMarketplaceURL)
}
if s.RequirePluginSignature == nil {
s.RequirePluginSignature = NewBool(false)
}
if s.SignaturePublicKeyFiles == nil {
s.SignaturePublicKeyFiles = []string{}
}
if s.ChimeraOAuthProxyURL == nil {
s.ChimeraOAuthProxyURL = NewString("")
}
}
type GlobalRelayMessageExportSettings struct {
CustomerType *string `access:"compliance_compliance_export"` // must be either A9 or A10, dictates SMTP server url
SMTPUsername *string `access:"compliance_compliance_export"`
SMTPPassword *string `access:"compliance_compliance_export"`
EmailAddress *string `access:"compliance_compliance_export"` // the address to send messages to
SMTPServerTimeout *int `access:"compliance_compliance_export"`
}
func (s *GlobalRelayMessageExportSettings) SetDefaults() {
if s.CustomerType == nil {
s.CustomerType = NewString(GlobalrelayCustomerTypeA9)
}
if s.SMTPUsername == nil {
s.SMTPUsername = NewString("")
}
if s.SMTPPassword == nil {
s.SMTPPassword = NewString("")
}
if s.EmailAddress == nil {
s.EmailAddress = NewString("")
}
if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 {
s.SMTPServerTimeout = NewInt(1800)
}
}
type MessageExportSettings struct {
EnableExport *bool `access:"compliance_compliance_export"`
ExportFormat *string `access:"compliance_compliance_export"`
DailyRunTime *string `access:"compliance_compliance_export"`
ExportFromTimestamp *int64 `access:"compliance_compliance_export"`
BatchSize *int `access:"compliance_compliance_export"`
DownloadExportResults *bool `access:"compliance_compliance_export"`
// formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format
GlobalRelaySettings *GlobalRelayMessageExportSettings `access:"compliance_compliance_export"`
}
func (s *MessageExportSettings) SetDefaults() {
if s.EnableExport == nil {
s.EnableExport = NewBool(false)
}
if s.DownloadExportResults == nil {
s.DownloadExportResults = NewBool(false)
}
if s.ExportFormat == nil {
s.ExportFormat = NewString(ComplianceExportTypeActiance)
}
if s.DailyRunTime == nil {
s.DailyRunTime = NewString("01:00")
}
if s.ExportFromTimestamp == nil {
s.ExportFromTimestamp = NewInt64(0)
}
if s.BatchSize == nil {
s.BatchSize = NewInt(10000)
}
if s.GlobalRelaySettings == nil {
s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{}
}
s.GlobalRelaySettings.SetDefaults()
}
type DisplaySettings struct {
CustomURLSchemes []string `access:"site_customization"`
ExperimentalTimezone *bool `access:"experimental_features"`
}
func (s *DisplaySettings) SetDefaults() {
if s.CustomURLSchemes == nil {
customURLSchemes := []string{}
s.CustomURLSchemes = customURLSchemes
}
if s.ExperimentalTimezone == nil {
s.ExperimentalTimezone = NewBool(true)
}
}
type GuestAccountsSettings struct {
Enable *bool `access:"authentication_guest_access"`
AllowEmailAccounts *bool `access:"authentication_guest_access"`
EnforceMultifactorAuthentication *bool `access:"authentication_guest_access"`
RestrictCreationToDomains *string `access:"authentication_guest_access"`
}
func (s *GuestAccountsSettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.AllowEmailAccounts == nil {
s.AllowEmailAccounts = NewBool(true)
}
if s.EnforceMultifactorAuthentication == nil {
s.EnforceMultifactorAuthentication = NewBool(false)
}
if s.RestrictCreationToDomains == nil {
s.RestrictCreationToDomains = NewString("")
}
}
type ImageProxySettings struct {
Enable *bool `access:"environment_image_proxy"`
ImageProxyType *string `access:"environment_image_proxy"`
RemoteImageProxyURL *string `access:"environment_image_proxy"`
RemoteImageProxyOptions *string `access:"environment_image_proxy"`
}
func (s *ImageProxySettings) SetDefaults() {
if s.Enable == nil {
s.Enable = NewBool(false)
}
if s.ImageProxyType == nil {
s.ImageProxyType = NewString(ImageProxyTypeLocal)
}
if s.RemoteImageProxyURL == nil {
s.RemoteImageProxyURL = NewString("")
}
if s.RemoteImageProxyOptions == nil {
s.RemoteImageProxyOptions = NewString("")
}
}
// ImportSettings defines configuration settings for file imports.
type ImportSettings struct {
// The directory where to store the imported files.
Directory *string
// The number of days to retain the imported files before deleting them.
RetentionDays *int
}
func (s *ImportSettings) isValid() *AppError {
if *s.Directory == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.import.directory.app_error", nil, "", http.StatusBadRequest)
}
if *s.RetentionDays <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.import.retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// SetDefaults applies the default settings to the struct.
func (s *ImportSettings) SetDefaults() {
if s.Directory == nil || *s.Directory == "" {
s.Directory = NewString(ImportSettingsDefaultDirectory)
}
if s.RetentionDays == nil {
s.RetentionDays = NewInt(ImportSettingsDefaultRetentionDays)
}
}
// ExportSettings defines configuration settings for file exports.
type ExportSettings struct {
// The directory where to store the exported files.
Directory *string // telemetry: none
// The number of days to retain the exported files before deleting them.
RetentionDays *int
}
func (s *ExportSettings) isValid() *AppError {
if *s.Directory == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.export.directory.app_error", nil, "", http.StatusBadRequest)
}
if *s.RetentionDays <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.export.retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// SetDefaults applies the default settings to the struct.
func (s *ExportSettings) SetDefaults() {
if s.Directory == nil || *s.Directory == "" {
s.Directory = NewString(ExportSettingsDefaultDirectory)
}
if s.RetentionDays == nil {
s.RetentionDays = NewInt(ExportSettingsDefaultRetentionDays)
}
}
type ConfigFunc func() *Config
const ConfigAccessTagType = "access"
const ConfigAccessTagWriteRestrictable = "write_restrictable"
const ConfigAccessTagCloudRestrictable = "cloud_restrictable"
// Allows read access if any PermissionSysconsoleRead* is allowed
const ConfigAccessTagAnySysConsoleRead = "*_read"
// Config fields support the 'access' tag with the following values corresponding to the suffix of the associated
// PermissionSysconsole* permission Id: 'about', 'reporting', 'user_management_users',
// 'user_management_groups', 'user_management_teams', 'user_management_channels',
// 'user_management_permissions', 'environment_web_server', 'environment_database', 'environment_elasticsearch',
// 'environment_file_storage', 'environment_image_proxy', 'environment_smtp', 'environment_push_notification_server',
// 'environment_high_availability', 'environment_rate_limiting', 'environment_logging', 'environment_session_lengths',
// 'environment_performance_monitoring', 'environment_developer', 'site', 'authentication', 'plugins',
// 'integrations', 'compliance', 'plugins', and 'experimental'. They grant read and/or write access to the config field
// to roles without PermissionManageSystem.
//
// The 'access' tag '*_read' checks for any Sysconsole read permission and grants access if any read permission is allowed.
//
// By default config values can be written with PermissionManageSystem, but if ExperimentalSettings.RestrictSystemAdmin is true
// and the access tag contains the value 'write_restrictable', then even PermissionManageSystem, does not grant write access.
//
// PermissionManageSystem always grants read access.
//
// Config values with the access tag 'cloud_restrictable' mean that are marked to be filtered when it's used in a cloud licensed
// environment with ExperimentalSettings.RestrictedSystemAdmin set to true.
//
// Example:
//
// type HairSettings struct {
// // Colour is writeable with either PermissionSysconsoleWriteReporting or PermissionSysconsoleWriteUserManagementGroups.
// // It is readable by PermissionSysconsoleReadReporting and PermissionSysconsoleReadUserManagementGroups permissions.
// // PermissionManageSystem grants read and write access.
// Colour string `access:"reporting,user_management_groups"`
//
// // Length is only readable and writable via PermissionManageSystem.
// Length string
//
// // Product is only writeable by PermissionManageSystem if ExperimentalSettings.RestrictSystemAdmin is false.
// // PermissionManageSystem can always read the value.
// Product bool `access:write_restrictable`
// }
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
ClientRequirements ClientRequirements
SqlSettings SqlSettings
LogSettings LogSettings
ExperimentalAuditSettings ExperimentalAuditSettings
NotificationLogSettings NotificationLogSettings
PasswordSettings PasswordSettings
FileSettings FileSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
SupportSettings SupportSettings
AnnouncementSettings AnnouncementSettings
ThemeSettings ThemeSettings
GitLabSettings SSOSettings
GoogleSettings SSOSettings
Office365Settings Office365Settings
OpenIdSettings SSOSettings
LdapSettings LdapSettings
ComplianceSettings ComplianceSettings
LocalizationSettings LocalizationSettings
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
MetricsSettings MetricsSettings
ExperimentalSettings ExperimentalSettings
AnalyticsSettings AnalyticsSettings
ElasticsearchSettings ElasticsearchSettings
BleveSettings BleveSettings
DataRetentionSettings DataRetentionSettings
MessageExportSettings MessageExportSettings
JobSettings JobSettings
ProductSettings ProductSettings
PluginSettings PluginSettings
DisplaySettings DisplaySettings
GuestAccountsSettings GuestAccountsSettings
ImageProxySettings ImageProxySettings
CloudSettings CloudSettings // telemetry: none
FeatureFlags *FeatureFlags `access:"*_read" json:",omitempty"`
ImportSettings ImportSettings // telemetry: none
ExportSettings ExportSettings
}
func (o *Config) Auditable() map[string]interface{} {
return map[string]interface{}{
// TODO
}
}
func (o *Config) Clone() *Config {
buf, err := json.Marshal(o)
if err != nil {
panic(err)
}
var ret Config
err = json.Unmarshal(buf, &ret)
if err != nil {
panic(err)
}
return &ret
}
func (o *Config) ToJSONFiltered(tagType, tagValue string) ([]byte, error) {
filteredConfigMap := structToMapFilteredByTag(*o, tagType, tagValue)
for key, value := range filteredConfigMap {
v, ok := value.(map[string]any)
if ok && len(v) == 0 {
delete(filteredConfigMap, key)
}
}
return json.Marshal(filteredConfigMap)
}
func (o *Config) GetSSOService(service string) *SSOSettings {
switch service {
case ServiceGitlab:
return &o.GitLabSettings
case ServiceGoogle:
return &o.GoogleSettings
case ServiceOffice365:
return o.Office365Settings.SSOSettings()
case ServiceOpenid:
return &o.OpenIdSettings
}
return nil
}
func ConfigFromJSON(data io.Reader) *Config {
var o *Config
json.NewDecoder(data).Decode(&o)
return o
}
// isUpdate detects a pre-existing config based on whether SiteURL has been changed
func (o *Config) isUpdate() bool {
return o.ServiceSettings.SiteURL != nil
}
func (o *Config) SetDefaults() {
isUpdate := o.isUpdate()
o.LdapSettings.SetDefaults()
o.SamlSettings.SetDefaults()
if o.TeamSettings.TeammateNameDisplay == nil {
o.TeamSettings.TeammateNameDisplay = NewString(ShowUsername)
if *o.SamlSettings.Enable || *o.LdapSettings.Enable {
*o.TeamSettings.TeammateNameDisplay = ShowFullName
}
}
o.SqlSettings.SetDefaults(isUpdate)
o.FileSettings.SetDefaults(isUpdate)
o.EmailSettings.SetDefaults(isUpdate)
o.PrivacySettings.setDefaults()
o.Office365Settings.setDefaults()
o.Office365Settings.setDefaults()
o.GitLabSettings.setDefaults("", "", "", "", "")
o.GoogleSettings.setDefaults(GoogleSettingsDefaultScope, GoogleSettingsDefaultAuthEndpoint, GoogleSettingsDefaultTokenEndpoint, GoogleSettingsDefaultUserAPIEndpoint, "")
o.OpenIdSettings.setDefaults(OpenidSettingsDefaultScope, "", "", "", "#145DBF")
o.ServiceSettings.SetDefaults(isUpdate)
o.PasswordSettings.SetDefaults()
o.TeamSettings.SetDefaults()
o.MetricsSettings.SetDefaults()
o.ExperimentalSettings.SetDefaults()
o.SupportSettings.SetDefaults()
o.AnnouncementSettings.SetDefaults()
o.ThemeSettings.SetDefaults()
o.ClusterSettings.SetDefaults()
o.PluginSettings.SetDefaults(o.LogSettings)
o.ProductSettings.SetDefaults(o.PluginSettings.Plugins)
o.AnalyticsSettings.SetDefaults()
o.ComplianceSettings.SetDefaults()
o.LocalizationSettings.SetDefaults()
o.ElasticsearchSettings.SetDefaults()
o.BleveSettings.SetDefaults()
o.NativeAppSettings.SetDefaults()
o.DataRetentionSettings.SetDefaults()
o.RateLimitSettings.SetDefaults()
o.LogSettings.SetDefaults()
o.ExperimentalAuditSettings.SetDefaults()
o.NotificationLogSettings.SetDefaults()
o.JobSettings.SetDefaults()
o.MessageExportSettings.SetDefaults()
o.DisplaySettings.SetDefaults()
o.GuestAccountsSettings.SetDefaults()
o.ImageProxySettings.SetDefaults()
o.CloudSettings.SetDefaults()
if o.FeatureFlags == nil {
o.FeatureFlags = &FeatureFlags{}
o.FeatureFlags.SetDefaults()
}
o.ImportSettings.SetDefaults()
o.ExportSettings.SetDefaults()
}
func (o *Config) IsValid() *AppError {
if *o.ServiceSettings.SiteURL == "" && *o.EmailSettings.EnableEmailBatching {
return NewAppError("Config.IsValid", "model.config.is_valid.site_url_email_batching.app_error", nil, "", http.StatusBadRequest)
}
if *o.ClusterSettings.Enable && *o.EmailSettings.EnableEmailBatching {
return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest)
}
if *o.ServiceSettings.SiteURL == "" && *o.ServiceSettings.AllowCookiesForSubdomains {
return NewAppError("Config.IsValid", "model.config.is_valid.allow_cookies_for_subdomains.app_error", nil, "", http.StatusBadRequest)
}
if appErr := o.TeamSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.SqlSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.FileSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.EmailSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.LdapSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.SamlSettings.isValid(); appErr != nil {
return appErr
}
if *o.PasswordSettings.MinimumLength < PasswordMinimumLength || *o.PasswordSettings.MinimumLength > PasswordMaximumLength {
return NewAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]any{"MinLength": PasswordMinimumLength, "MaxLength": PasswordMaximumLength}, "", http.StatusBadRequest)
}
if appErr := o.RateLimitSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.ServiceSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.ElasticsearchSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.BleveSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.DataRetentionSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.LocalizationSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.MessageExportSettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.DisplaySettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.ImageProxySettings.isValid(); appErr != nil {
return appErr
}
if appErr := o.ImportSettings.isValid(); appErr != nil {
return appErr
}
return nil
}
func (s *TeamSettings) isValid() *AppError {
if *s.MaxUsersPerTeam <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxChannelsPerTeam <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxNotificationsPerChannel <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.max_notify_per_channel.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.RestrictDirectMessage == DirectMessageAny || *s.RestrictDirectMessage == DirectMessageTeam) {
return NewAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.TeammateNameDisplay == ShowFullName || *s.TeammateNameDisplay == ShowNicknameFullName || *s.TeammateNameDisplay == ShowUsername) {
return NewAppError("Config.IsValid", "model.config.is_valid.teammate_name_display.app_error", nil, "", http.StatusBadRequest)
}
if len(*s.SiteName) > SitenameMaxLength {
return NewAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]any{"MaxLength": SitenameMaxLength}, "", http.StatusBadRequest)
}
return nil
}
func (s *SqlSettings) isValid() *AppError {
if *s.AtRestEncryptKey != "" && len(*s.AtRestEncryptKey) < 32 {
return NewAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.DriverName == DatabaseDriverMysql || *s.DriverName == DatabaseDriverPostgres) {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_driver.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxIdleConns <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "", http.StatusBadRequest)
}
if *s.ConnMaxLifetimeMilliseconds < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", nil, "", http.StatusBadRequest)
}
if *s.ConnMaxIdleTimeMilliseconds < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_idle_time_milliseconds.app_error", nil, "", http.StatusBadRequest)
}
if *s.QueryTimeout <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest)
}
if *s.DataSource == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxOpenConns <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.sql_max_conn.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (s *FileSettings) isValid() *AppError {
if *s.MaxFileSize <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.max_file_size.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.DriverName == ImageDriverLocal || *s.DriverName == ImageDriverS3) {
return NewAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "", http.StatusBadRequest)
}
if *s.PublicLinkSalt != "" && len(*s.PublicLinkSalt) < 32 {
return NewAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "", http.StatusBadRequest)
}
if *s.Directory == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.directory.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxImageDecoderConcurrency < -1 || *s.MaxImageDecoderConcurrency == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.image_decoder_concurrency.app_error", map[string]any{"Value": *s.MaxImageDecoderConcurrency}, "", http.StatusBadRequest)
}
if *s.AmazonS3RequestTimeoutMilliseconds <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.amazons3_timeout.app_error", map[string]any{"Value": *s.MaxImageDecoderConcurrency}, "", http.StatusBadRequest)
}
return nil
}
func (s *EmailSettings) isValid() *AppError {
if !(*s.ConnectionSecurity == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS || *s.ConnectionSecurity == ConnSecurityStarttls || *s.ConnectionSecurity == ConnSecurityPlain) {
return NewAppError("Config.IsValid", "model.config.is_valid.email_security.app_error", nil, "", http.StatusBadRequest)
}
if *s.EmailBatchingBufferSize <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.email_batching_buffer_size.app_error", nil, "", http.StatusBadRequest)
}
if *s.EmailBatchingInterval < 30 {
return NewAppError("Config.IsValid", "model.config.is_valid.email_batching_interval.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.EmailNotificationContentsType == EmailNotificationContentsFull || *s.EmailNotificationContentsType == EmailNotificationContentsGeneric) {
return NewAppError("Config.IsValid", "model.config.is_valid.email_notification_contents_type.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (s *RateLimitSettings) isValid() *AppError {
if *s.MemoryStoreSize <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "", http.StatusBadRequest)
}
if *s.PerSec <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.rate_sec.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxBurst <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.max_burst.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (s *LdapSettings) isValid() *AppError {
if !(*s.ConnectionSecurity == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS || *s.ConnectionSecurity == ConnSecurityStarttls) {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_security.app_error", nil, "", http.StatusBadRequest)
}
if *s.SyncIntervalMinutes <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_sync_interval.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaxPageSize < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_max_page_size.app_error", nil, "", http.StatusBadRequest)
}
if *s.Enable {
if *s.LdapServer == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_server", nil, "", http.StatusBadRequest)
}
if *s.BaseDN == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_basedn", nil, "", http.StatusBadRequest)
}
if *s.EmailAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_email", nil, "", http.StatusBadRequest)
}
if *s.UsernameAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_username", nil, "", http.StatusBadRequest)
}
if *s.IdAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "", http.StatusBadRequest)
}
if *s.LoginIdAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_login_id", nil, "", http.StatusBadRequest)
}
if *s.UserFilter != "" {
if _, err := ldap.CompileFilter(*s.UserFilter); err != nil {
return NewAppError("ValidateFilter", "ent.ldap.validate_filter.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if *s.GuestFilter != "" {
if _, err := ldap.CompileFilter(*s.GuestFilter); err != nil {
return NewAppError("LdapSettings.isValid", "ent.ldap.validate_guest_filter.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if *s.AdminFilter != "" {
if _, err := ldap.CompileFilter(*s.AdminFilter); err != nil {
return NewAppError("LdapSettings.isValid", "ent.ldap.validate_admin_filter.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
}
return nil
}
func (s *SamlSettings) isValid() *AppError {
if *s.Enable {
if *s.IdpURL == "" || !IsValidHTTPURL(*s.IdpURL) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "", http.StatusBadRequest)
}
if *s.IdpDescriptorURL == "" || !IsValidHTTPURL(*s.IdpDescriptorURL) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "", http.StatusBadRequest)
}
if *s.IdpCertificateFile == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "", http.StatusBadRequest)
}
if *s.EmailAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest)
}
if *s.UsernameAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "", http.StatusBadRequest)
}
if *s.ServiceProviderIdentifier == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_spidentifier_attribute.app_error", nil, "", http.StatusBadRequest)
}
if *s.Verify {
if *s.AssertionConsumerServiceURL == "" || !IsValidHTTPURL(*s.AssertionConsumerServiceURL) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_assertion_consumer_service_url.app_error", nil, "", http.StatusBadRequest)
}
}
if *s.Encrypt {
if *s.PrivateKeyFile == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "", http.StatusBadRequest)
}
if *s.PublicCertificateFile == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "", http.StatusBadRequest)
}
}
if *s.EmailAttribute == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha1 || *s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha256 || *s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha512) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_signature_algorithm.app_error", nil, "", http.StatusBadRequest)
}
if !(*s.CanonicalAlgorithm == SamlSettingsCanonicalAlgorithmC14n || *s.CanonicalAlgorithm == SamlSettingsCanonicalAlgorithmC14n11) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_canonical_algorithm.app_error", nil, "", http.StatusBadRequest)
}
if *s.GuestAttribute != "" {
if !(strings.Contains(*s.GuestAttribute, "=")) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_guest_attribute.app_error", nil, "", http.StatusBadRequest)
}
if len(strings.Split(*s.GuestAttribute, "=")) != 2 {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_guest_attribute.app_error", nil, "", http.StatusBadRequest)
}
}
if *s.AdminAttribute != "" {
if !(strings.Contains(*s.AdminAttribute, "=")) {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_admin_attribute.app_error", nil, "", http.StatusBadRequest)
}
if len(strings.Split(*s.AdminAttribute, "=")) != 2 {
return NewAppError("Config.IsValid", "model.config.is_valid.saml_admin_attribute.app_error", nil, "", http.StatusBadRequest)
}
}
}
return nil
}
func (s *ServiceSettings) isValid() *AppError {
if !(*s.ConnectionSecurity == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS) {
return NewAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "", http.StatusBadRequest)
}
if *s.ConnectionSecurity == ConnSecurityTLS && !*s.UseLetsEncrypt {
appErr := NewAppError("Config.IsValid", "model.config.is_valid.tls_cert_file_missing.app_error", nil, "", http.StatusBadRequest)
if *s.TLSCertFile == "" {
return appErr
} else if _, err := os.Stat(*s.TLSCertFile); os.IsNotExist(err) {
return appErr
}
appErr = NewAppError("Config.IsValid", "model.config.is_valid.tls_key_file_missing.app_error", nil, "", http.StatusBadRequest)
if *s.TLSKeyFile == "" {
return appErr
} else if _, err := os.Stat(*s.TLSKeyFile); os.IsNotExist(err) {
return appErr
}
}
if len(s.TLSOverwriteCiphers) > 0 {
for _, cipher := range s.TLSOverwriteCiphers {
if _, ok := ServerTLSSupportedCiphers[cipher]; !ok {
return NewAppError("Config.IsValid", "model.config.is_valid.tls_overwrite_cipher.app_error", map[string]any{"name": cipher}, "", http.StatusBadRequest)
}
}
}
if *s.ReadTimeout <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.read_timeout.app_error", nil, "", http.StatusBadRequest)
}
if *s.WriteTimeout <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.write_timeout.app_error", nil, "", http.StatusBadRequest)
}
if *s.TimeBetweenUserTypingUpdatesMilliseconds < 1000 {
return NewAppError("Config.IsValid", "model.config.is_valid.time_between_user_typing.app_error", nil, "", http.StatusBadRequest)
}
if *s.MaximumLoginAttempts <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.login_attempts.app_error", nil, "", http.StatusBadRequest)
}
if *s.SiteURL != "" {
if _, err := url.ParseRequestURI(*s.SiteURL); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.site_url.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if *s.WebsocketURL != "" {
if _, err := url.ParseRequestURI(*s.WebsocketURL); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.websocket_url.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
host, port, _ := net.SplitHostPort(*s.ListenAddress)
var isValidHost bool
if host == "" {
isValidHost = true
} else {
isValidHost = (net.ParseIP(host) != nil) || isDomainName(host)
}
portInt, err := strconv.Atoi(port)
if err != nil || !isValidHost || portInt < 0 || portInt > math.MaxUint16 {
return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest)
}
if *s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDisabled &&
*s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOn &&
*s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOff {
return NewAppError("Config.IsValid", "model.config.is_valid.group_unread_channels.app_error", nil, "", http.StatusBadRequest)
}
if *s.CollapsedThreads != CollapsedThreadsDisabled && !*s.ThreadAutoFollow {
return NewAppError("Config.IsValid", "model.config.is_valid.collapsed_threads.autofollow.app_error", nil, "", http.StatusBadRequest)
}
if *s.CollapsedThreads != CollapsedThreadsDisabled &&
*s.CollapsedThreads != CollapsedThreadsDefaultOn &&
*s.CollapsedThreads != CollapsedThreadsAlwaysOn &&
*s.CollapsedThreads != CollapsedThreadsDefaultOff {
return NewAppError("Config.IsValid", "model.config.is_valid.collapsed_threads.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (s *ElasticsearchSettings) isValid() *AppError {
if *s.EnableIndexing {
if *s.ConnectionURL == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.connection_url.app_error", nil, "", http.StatusBadRequest)
}
}
if *s.EnableSearching && !*s.EnableIndexing {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_searching.app_error", nil, "", http.StatusBadRequest)
}
if *s.EnableAutocomplete && !*s.EnableIndexing {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_autocomplete.app_error", nil, "", http.StatusBadRequest)
}
if *s.AggregatePostsAfterDays < 1 {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", nil, "", http.StatusBadRequest)
}
if _, err := time.Parse("15:04", *s.PostsAggregatorJobStartTime); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if *s.LiveIndexingBatchSize < 1 {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", nil, "", http.StatusBadRequest)
}
minBatchSize := 1
if *s.BatchSize < minBatchSize {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.bulk_indexing_batch_size.app_error", map[string]any{"BatchSize": minBatchSize}, "", http.StatusBadRequest)
}
if *s.RequestTimeoutSeconds < 1 {
return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (bs *BleveSettings) isValid() *AppError {
if *bs.EnableIndexing {
if *bs.IndexDir == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.filename.app_error", nil, "", http.StatusBadRequest)
}
} else {
if *bs.EnableSearching {
return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.enable_searching.app_error", nil, "", http.StatusBadRequest)
}
if *bs.EnableAutocomplete {
return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.enable_autocomplete.app_error", nil, "", http.StatusBadRequest)
}
}
minBatchSize := 1
if *bs.BatchSize < minBatchSize {
return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.bulk_indexing_batch_size.app_error", map[string]any{"BatchSize": minBatchSize}, "", http.StatusBadRequest)
}
return nil
}
func (s *DataRetentionSettings) isValid() *AppError {
if *s.MessageRetentionDays <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
if *s.FileRetentionDays <= 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
if _, err := time.Parse("15:04", *s.DeletionJobStartTime); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.deletion_job_start_time.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
func (s *LocalizationSettings) isValid() *AppError {
if *s.AvailableLocales != "" {
if !strings.Contains(*s.AvailableLocales, *s.DefaultClientLocale) {
return NewAppError("Config.IsValid", "model.config.is_valid.localization.available_locales.app_error", nil, "", http.StatusBadRequest)
}
}
return nil
}
func (s *MessageExportSettings) isValid() *AppError {
if s.EnableExport == nil {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.enable.app_error", nil, "", http.StatusBadRequest)
}
if *s.EnableExport {
if s.ExportFromTimestamp == nil || *s.ExportFromTimestamp < 0 || *s.ExportFromTimestamp > GetMillis() {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_from.app_error", nil, "", http.StatusBadRequest)
} else if s.DailyRunTime == nil {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, "", http.StatusBadRequest)
} else if _, err := time.Parse("15:04", *s.DailyRunTime); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, "", http.StatusBadRequest).Wrap(err)
} else if s.BatchSize == nil || *s.BatchSize < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest)
} else if s.ExportFormat == nil || (*s.ExportFormat != ComplianceExportTypeActiance && *s.ExportFormat != ComplianceExportTypeGlobalrelay && *s.ExportFormat != ComplianceExportTypeCsv) {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest)
}
if *s.ExportFormat == ComplianceExportTypeGlobalrelay {
if s.GlobalRelaySettings == nil {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.config_missing.app_error", nil, "", http.StatusBadRequest)
} else if s.GlobalRelaySettings.CustomerType == nil || (*s.GlobalRelaySettings.CustomerType != GlobalrelayCustomerTypeA9 && *s.GlobalRelaySettings.CustomerType != GlobalrelayCustomerTypeA10) {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.customer_type.app_error", nil, "", http.StatusBadRequest)
} else if s.GlobalRelaySettings.EmailAddress == nil || !strings.Contains(*s.GlobalRelaySettings.EmailAddress, "@") {
// validating email addresses is hard - just make sure it contains an '@' sign
// see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.email_address.app_error", nil, "", http.StatusBadRequest)
} else if s.GlobalRelaySettings.SMTPUsername == nil || *s.GlobalRelaySettings.SMTPUsername == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_username.app_error", nil, "", http.StatusBadRequest)
} else if s.GlobalRelaySettings.SMTPPassword == nil || *s.GlobalRelaySettings.SMTPPassword == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_password.app_error", nil, "", http.StatusBadRequest)
}
}
}
return nil
}
func (s *DisplaySettings) isValid() *AppError {
if len(s.CustomURLSchemes) != 0 {
validProtocolPattern := regexp.MustCompile(`(?i)^\s*[A-Za-z][A-Za-z0-9.+-]*\s*$`)
for _, scheme := range s.CustomURLSchemes {
if !validProtocolPattern.MatchString(scheme) {
return NewAppError(
"Config.IsValid",
"model.config.is_valid.display.custom_url_schemes.app_error",
map[string]any{"Scheme": scheme},
"",
http.StatusBadRequest,
)
}
}
}
return nil
}
func (s *ImageProxySettings) isValid() *AppError {
if *s.Enable {
switch *s.ImageProxyType {
case ImageProxyTypeLocal:
// No other settings to validate
case ImageProxyTypeAtmosCamo:
if *s.RemoteImageProxyURL == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_url.app_error", nil, "", http.StatusBadRequest)
}
if *s.RemoteImageProxyOptions == "" {
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options.app_error", nil, "", http.StatusBadRequest)
}
default:
return NewAppError("Config.IsValid", "model.config.is_valid.image_proxy_type.app_error", nil, "", http.StatusBadRequest)
}
}
return nil
}
func (o *Config) GetSanitizeOptions() map[string]bool {
options := map[string]bool{}
options["fullname"] = *o.PrivacySettings.ShowFullName
options["email"] = *o.PrivacySettings.ShowEmailAddress
return options
}
func (o *Config) Sanitize() {
if o.LdapSettings.BindPassword != nil && *o.LdapSettings.BindPassword != "" {
*o.LdapSettings.BindPassword = FakeSetting
}
if o.FileSettings.PublicLinkSalt != nil {
*o.FileSettings.PublicLinkSalt = FakeSetting
}
if o.FileSettings.AmazonS3SecretAccessKey != nil && *o.FileSettings.AmazonS3SecretAccessKey != "" {
*o.FileSettings.AmazonS3SecretAccessKey = FakeSetting
}
if o.EmailSettings.SMTPPassword != nil && *o.EmailSettings.SMTPPassword != "" {
*o.EmailSettings.SMTPPassword = FakeSetting
}
if o.GitLabSettings.Secret != nil && *o.GitLabSettings.Secret != "" {
*o.GitLabSettings.Secret = FakeSetting
}
if o.GoogleSettings.Secret != nil && *o.GoogleSettings.Secret != "" {
*o.GoogleSettings.Secret = FakeSetting
}
if o.Office365Settings.Secret != nil && *o.Office365Settings.Secret != "" {
*o.Office365Settings.Secret = FakeSetting
}
if o.OpenIdSettings.Secret != nil && *o.OpenIdSettings.Secret != "" {
*o.OpenIdSettings.Secret = FakeSetting
}
if o.SqlSettings.DataSource != nil {
*o.SqlSettings.DataSource = FakeSetting
}
if o.SqlSettings.AtRestEncryptKey != nil {
*o.SqlSettings.AtRestEncryptKey = FakeSetting
}
if o.ElasticsearchSettings.Password != nil {
*o.ElasticsearchSettings.Password = FakeSetting
}
for i := range o.SqlSettings.DataSourceReplicas {
o.SqlSettings.DataSourceReplicas[i] = FakeSetting
}
for i := range o.SqlSettings.DataSourceSearchReplicas {
o.SqlSettings.DataSourceSearchReplicas[i] = FakeSetting
}
if o.MessageExportSettings.GlobalRelaySettings != nil &&
o.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil &&
*o.MessageExportSettings.GlobalRelaySettings.SMTPPassword != "" {
*o.MessageExportSettings.GlobalRelaySettings.SMTPPassword = FakeSetting
}
if o.ServiceSettings.GfycatAPISecret != nil && *o.ServiceSettings.GfycatAPISecret != "" {
*o.ServiceSettings.GfycatAPISecret = FakeSetting
}
if o.ServiceSettings.SplitKey != nil {
*o.ServiceSettings.SplitKey = FakeSetting
}
}
// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed
// as argument
func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any {
defer func() {
if r := recover(); r != nil {
mlog.Warn("Panicked in structToMapFilteredByTag. This should never happen.", mlog.Any("recover", r))
}
}()
val := reflect.ValueOf(t)
elemField := reflect.TypeOf(t)
if val.Kind() != reflect.Struct {
return nil
}
out := map[string]any{}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
structField := elemField.Field(i)
tagPermissions := strings.Split(structField.Tag.Get(typeOfTag), ",")
if isTagPresent(filterTag, tagPermissions) {
continue
}
var value any
switch field.Kind() {
case reflect.Struct:
value = structToMapFilteredByTag(field.Interface(), typeOfTag, filterTag)
case reflect.Ptr:
indirectType := field.Elem()
if indirectType.Kind() == reflect.Struct {
value = structToMapFilteredByTag(indirectType.Interface(), typeOfTag, filterTag)
} else if indirectType.Kind() != reflect.Invalid {
value = indirectType.Interface()
}
default:
value = field.Interface()
}
out[val.Type().Field(i).Name] = value
}
return out
}
func isTagPresent(tag string, tags []string) bool {
for _, val := range tags {
tagValue := strings.TrimSpace(val)
if tagValue != "" && tagValue == tag {
return true
}
}
return false
}
// Copied from https://golang.org/src/net/dnsclient.go#L119
func isDomainName(s string) bool {
// See RFC 1035, RFC 3696.
// Presentation format has dots before every label except the first, and the
// terminal empty label is optional here because we assume fully-qualified
// (absolute) input. We must therefore reserve space for the first and last
// labels' length octets in wire format, where they are necessary and the
// maximum total length is 255.
// So our _effective_ maximum is 253, but 254 is not rejected if the last
// character is a dot.
l := len(s)
if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
return false
}
last := byte('.')
ok := false // Ok once we've seen a letter.
partlen := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch {
default:
return false
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
ok = true
partlen++
case '0' <= c && c <= '9':
// fine
partlen++
case c == '-':
// Byte before dash cannot be dot.
if last == '.' {
return false
}
partlen++
case c == '.':
// Byte before dot cannot be dot, dash.
if last == '.' || last == '-' {
return false
}
if partlen > 63 || partlen == 0 {
return false
}
partlen = 0
}
last = c
}
if last == '-' || partlen > 63 {
return false
}
return ok
}
func isSafeLink(link *string) bool {
if link != nil {
if IsValidHTTPURL(*link) {
return true
} else if strings.HasPrefix(*link, "/") {
return true
} else {
return false
}
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"bytes"
"encoding/json"
"fmt"
"time"
"github.com/graph-gophers/graphql-go"
)
const (
UserPropsKeyCustomStatus = "customStatus"
CustomStatusTextMaxRunes = 100
MaxRecentCustomStatuses = 5
DefaultCustomStatusEmoji = "speech_balloon"
)
var validCustomStatusDuration = map[string]bool{
"thirty_minutes": true,
"one_hour": true,
"four_hours": true,
"today": true,
"this_week": true,
"date_and_time": true,
}
type CustomStatus struct {
Emoji string `json:"emoji"`
Text string `json:"text"`
Duration string `json:"duration"`
ExpiresAt time.Time `json:"expires_at"`
}
func (cs *CustomStatus) PreSave() {
if cs.Emoji == "" {
cs.Emoji = DefaultCustomStatusEmoji
}
if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) {
cs.Duration = "date_and_time"
}
runes := []rune(cs.Text)
if len(runes) > CustomStatusTextMaxRunes {
cs.Text = string(runes[:CustomStatusTextMaxRunes])
}
}
func (cs *CustomStatus) AreDurationAndExpirationTimeValid() bool {
if cs.Duration == "" && (cs.ExpiresAt.IsZero() || !cs.ExpiresAt.Before(time.Now())) {
return true
}
if validCustomStatusDuration[cs.Duration] && !cs.ExpiresAt.Before(time.Now()) {
return true
}
return false
}
// ExpiresAt_ returns the time in a type that has the marshal/unmarshal methods
// attached to it.
func (cs *CustomStatus) ExpiresAt_() graphql.Time {
return graphql.Time{Time: cs.ExpiresAt}
}
func RuneToHexadecimalString(r rune) string {
return fmt.Sprintf("%04x", r)
}
type RecentCustomStatuses []CustomStatus
func (rcs RecentCustomStatuses) Contains(cs *CustomStatus) (bool, error) {
if cs == nil {
return false, nil
}
csJSON, jsonErr := json.Marshal(cs)
if jsonErr != nil {
return false, jsonErr
}
// status is empty
if len(csJSON) == 0 || (cs.Emoji == "" && cs.Text == "") {
return false, nil
}
for _, status := range rcs {
js, jsonErr := json.Marshal(status)
if jsonErr != nil {
return false, jsonErr
}
if bytes.Equal(js, csJSON) {
return true, nil
}
}
return false, nil
}
func (rcs RecentCustomStatuses) Add(cs *CustomStatus) RecentCustomStatuses {
newRCS := rcs[:0]
// if same `text` exists in existing recent custom statuses, modify existing status
for _, status := range rcs {
if status.Text != cs.Text {
newRCS = append(newRCS, status)
}
}
newRCS = append(RecentCustomStatuses{*cs}, newRCS...)
if len(newRCS) > MaxRecentCustomStatuses {
newRCS = newRCS[:MaxRecentCustomStatuses]
}
return newRCS
}
func (rcs RecentCustomStatuses) Remove(cs *CustomStatus) (RecentCustomStatuses, error) {
if cs == nil {
return rcs, nil
}
csJSON, jsonErr := json.Marshal(cs)
if jsonErr != nil {
return rcs, jsonErr
}
if len(csJSON) == 0 || (cs.Emoji == "" && cs.Text == "") {
return rcs, nil
}
newRCS := rcs[:0]
for _, status := range rcs {
js, jsonErr := json.Marshal(status)
if jsonErr != nil {
return rcs, jsonErr
}
if !bytes.Equal(js, csJSON) {
newRCS = append(newRCS, status)
}
}
return newRCS, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type GlobalRetentionPolicy struct {
MessageDeletionEnabled bool `json:"message_deletion_enabled"`
FileDeletionEnabled bool `json:"file_deletion_enabled"`
BoardsDeletionEnabled bool `json:"boards_deletion_enabled"`
MessageRetentionCutoff int64 `json:"message_retention_cutoff"`
FileRetentionCutoff int64 `json:"file_retention_cutoff"`
BoardsRetentionCutoff int64 `json:"boards_retention_cutoff"`
}
type RetentionPolicy struct {
ID string `db:"Id" json:"id"`
DisplayName string `json:"display_name"`
PostDurationDays *int64 `db:"PostDuration" json:"post_duration"`
}
type RetentionPolicyWithTeamAndChannelIDs struct {
RetentionPolicy
TeamIDs []string `json:"team_ids"`
ChannelIDs []string `json:"channel_ids"`
}
func (o *RetentionPolicyWithTeamAndChannelIDs) Auditable() map[string]interface{} {
return map[string]interface{}{
"retention_policy": o.RetentionPolicy,
"team_ids": o.TeamIDs,
"channel_ids": o.ChannelIDs,
}
}
type RetentionPolicyWithTeamAndChannelCounts struct {
RetentionPolicy
ChannelCount int64 `json:"channel_count"`
TeamCount int64 `json:"team_count"`
}
func (o *RetentionPolicyWithTeamAndChannelCounts) Auditable() map[string]interface{} {
return map[string]interface{}{
"retention_policy": o.RetentionPolicy,
"channel_count": o.ChannelCount,
"team_count": o.TeamCount,
}
}
type RetentionPolicyChannel struct {
PolicyID string `db:"PolicyId"`
ChannelID string `db:"ChannelId"`
}
type RetentionPolicyTeam struct {
PolicyID string `db:"PolicyId"`
TeamID string `db:"TeamId"`
}
type RetentionPolicyWithTeamAndChannelCountsList struct {
Policies []*RetentionPolicyWithTeamAndChannelCounts `json:"policies"`
TotalCount int64 `json:"total_count"`
}
type RetentionPolicyForTeam struct {
TeamID string `db:"Id" json:"team_id"`
PostDurationDays int64 `db:"PostDuration" json:"post_duration"`
}
type RetentionPolicyForTeamList struct {
Policies []*RetentionPolicyForTeam `json:"policies"`
TotalCount int64 `json:"total_count"`
}
type RetentionPolicyForChannel struct {
ChannelID string `db:"Id" json:"channel_id"`
PostDurationDays int64 `db:"PostDuration" json:"post_duration"`
}
type RetentionPolicyForChannelList struct {
Policies []*RetentionPolicyForChannel `json:"policies"`
TotalCount int64 `json:"total_count"`
}
type RetentionPolicyCursor struct {
ChannelPoliciesDone bool
TeamPoliciesDone bool
GlobalPoliciesDone bool
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"sync"
"unicode/utf8"
)
type Draft struct {
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
RootId string `json:"root_id"`
Message string `json:"message"`
propsMu sync.RWMutex `db:"-"` // Unexported mutex used to guard Draft.Props.
Props StringInterface `json:"props"` // Deprecated: use GetProps()
FileIds StringArray `json:"file_ids,omitempty"`
Metadata *PostMetadata `json:"metadata,omitempty"`
Priority StringInterface `json:"priority,omitempty"`
}
func (o *Draft) IsValid(maxDraftSize int) *AppError {
if o.CreateAt == 0 {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.create_at.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.update_at.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !(IsValidId(o.RootId) || o.RootId == "") {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Message) > maxDraftSize {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.msg.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(ArrayToJSON(o.FileIds)) > PostFileidsMaxRunes {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.file_ids.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(StringInterfaceToJSON(o.GetProps())) > PostPropsMaxRunes {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.props.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(StringInterfaceToJSON(o.Priority)) > PostPropsMaxRunes {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.priority.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
return nil
}
func (o *Draft) SetProps(props StringInterface) {
o.propsMu.Lock()
defer o.propsMu.Unlock()
o.Props = props
}
func (o *Draft) GetProps() StringInterface {
o.propsMu.RLock()
defer o.propsMu.RUnlock()
return o.Props
}
func (o *Draft) PreSave() {
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt
o.DeleteAt = 0
o.PreCommit()
}
func (o *Draft) PreCommit() {
if o.GetProps() == nil {
o.SetProps(make(map[string]interface{}))
}
if o.FileIds == nil {
o.FileIds = []string{}
}
// There's a rare bug where the client sends up duplicate FileIds so protect against that
o.FileIds = RemoveDuplicateStrings(o.FileIds)
}
func (o *Draft) PreUpdate() {
o.UpdateAt = GetMillis()
o.PreCommit()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"regexp"
"sort"
)
const (
EmojiNameMaxLength = 64
EmojiSortByName = "name"
)
var EmojiPattern = regexp.MustCompile(`:[a-zA-Z0-9_+-]+:`)
type Emoji struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
CreatorId string `json:"creator_id"`
Name string `json:"name"`
}
func (emoji *Emoji) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": emoji.Id,
"create_at": emoji.CreateAt,
"update_at": emoji.UpdateAt,
"delete_at": emoji.CreateAt,
"creator_id": emoji.CreatorId,
"name": emoji.Name,
}
}
func inSystemEmoji(emojiName string) bool {
_, ok := SystemEmojis[emojiName]
return ok
}
func GetSystemEmojiId(emojiName string) (string, bool) {
id, found := SystemEmojis[emojiName]
return id, found
}
func makeReverseEmojiMap() map[string][]string {
reverseEmojiMap := make(map[string][]string)
for key, value := range SystemEmojis {
emojiNames := reverseEmojiMap[value]
emojiNames = append(emojiNames, key)
sort.Strings(emojiNames)
reverseEmojiMap[value] = emojiNames
}
return reverseEmojiMap
}
var reverseSystemEmojisMap = makeReverseEmojiMap()
func GetEmojiNameFromUnicode(unicode string) (emojiName string, count int) {
if emojiNames, found := reverseSystemEmojisMap[unicode]; found {
return emojiNames[0], len(emojiNames)
}
return "", 0
}
func (emoji *Emoji) IsValid() *AppError {
if !IsValidId(emoji.Id) {
return NewAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "", http.StatusBadRequest)
}
if emoji.CreateAt == 0 {
return NewAppError("Emoji.IsValid", "model.emoji.create_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest)
}
if emoji.UpdateAt == 0 {
return NewAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest)
}
if len(emoji.CreatorId) > 26 {
return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest)
}
return IsValidEmojiName(emoji.Name)
}
func IsValidEmojiName(name string) *AppError {
if name == "" || len(name) > EmojiNameMaxLength || !IsValidAlphaNumHyphenUnderscorePlus(name) {
return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest)
}
if inSystemEmoji(name) {
return NewAppError("Emoji.IsValid", "model.emoji.system_emoji_name.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (emoji *Emoji) PreSave() {
if emoji.Id == "" {
emoji.Id = NewId()
}
emoji.CreateAt = GetMillis()
emoji.UpdateAt = emoji.CreateAt
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"reflect"
"strconv"
)
type FeatureFlags struct {
// Exists only for unit and manual testing.
// When set to a value, will be returned by the ping endpoint.
TestFeature string
// Exists only for testing bool functionality. Boolean feature flags interpret "on" or "true" as true and
// all other values as false.
TestBoolFeature bool
// Enable the remote cluster service for shared channels.
EnableRemoteClusterService bool
// AppsEnabled toggles the Apps framework functionalities both in server and client side
AppsEnabled bool
// Feature flags to control plugin versions
PluginPlaybooks string `plugin_id:"playbooks"`
PluginApps string `plugin_id:"com.mattermost.apps"`
PluginFocalboard string `plugin_id:"focalboard"`
PluginCalls string `plugin_id:"com.mattermost.calls"`
PermalinkPreviews bool
// CallsEnabled controls whether or not the Calls plugin should be enabled
CallsEnabled bool
// A dash separated list for feature flags to turn on for Boards
BoardsFeatureFlags string
// Enable DataRetention for Boards
BoardsDataRetention bool
NormalizeLdapDNs bool
// Enable special onboarding flow for first admin
UseCaseOnboarding bool
// Enable GraphQL feature
GraphQL bool
InsightsEnabled bool
CommandPalette bool
// Enable Boards as a product (multi-product architecture)
BoardsProduct bool
// A/B Test on posting a welcome message
SendWelcomePost bool
WorkTemplate bool
PostPriority bool
// Enable WYSIWYG text editor
WysiwygEditor bool
PeopleProduct bool
// A/B Test on reduced onboarding task list item
ReduceOnBoardingTaskList bool
// A/B Test to control when to show onboarding linked board
OnboardingAutoShowLinkedBoard bool
ThreadsEverywhere bool
GlobalDrafts bool
OnboardingTourTips bool
}
func (f *FeatureFlags) SetDefaults() {
f.TestFeature = "off"
f.TestBoolFeature = false
f.EnableRemoteClusterService = false
f.AppsEnabled = true
f.PluginApps = ""
f.PluginFocalboard = ""
f.PermalinkPreviews = true
f.BoardsFeatureFlags = ""
f.BoardsDataRetention = false
f.NormalizeLdapDNs = false
f.UseCaseOnboarding = true
f.GraphQL = false
f.InsightsEnabled = true
f.CommandPalette = false
f.CallsEnabled = true
f.BoardsProduct = false
f.SendWelcomePost = true
f.PostPriority = true
f.PeopleProduct = false
f.WorkTemplate = false
f.ReduceOnBoardingTaskList = false
f.ThreadsEverywhere = false
f.GlobalDrafts = true
f.WysiwygEditor = false
f.OnboardingAutoShowLinkedBoard = true
f.OnboardingTourTips = true
}
func (f *FeatureFlags) Plugins() map[string]string {
rFFVal := reflect.ValueOf(f).Elem()
rFFType := reflect.TypeOf(f).Elem()
pluginVersions := make(map[string]string)
for i := 0; i < rFFVal.NumField(); i++ {
rFieldVal := rFFVal.Field(i)
rFieldType := rFFType.Field(i)
pluginId, hasPluginId := rFieldType.Tag.Lookup("plugin_id")
if !hasPluginId {
continue
}
pluginVersions[pluginId] = rFieldVal.String()
}
return pluginVersions
}
// ToMap returns the feature flags as a map[string]string
// Supports boolean and string feature flags.
func (f *FeatureFlags) ToMap() map[string]string {
refStructVal := reflect.ValueOf(*f)
refStructType := reflect.TypeOf(*f)
ret := make(map[string]string)
for i := 0; i < refStructVal.NumField(); i++ {
refFieldVal := refStructVal.Field(i)
if !refFieldVal.IsValid() {
continue
}
refFieldType := refStructType.Field(i)
switch refFieldType.Type.Kind() {
case reflect.Bool:
ret[refFieldType.Name] = strconv.FormatBool(refFieldVal.Bool())
default:
ret[refFieldType.Name] = refFieldVal.String()
}
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"image"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/imgutils"
)
const (
FileinfoSortByCreated = "CreateAt"
FileinfoSortBySize = "Size"
)
// GetFileInfosOptions contains options for getting FileInfos
type GetFileInfosOptions struct {
// UserIds optionally limits the FileInfos to those created by the given users.
UserIds []string `json:"user_ids"`
// ChannelIds optionally limits the FileInfos to those created in the given channels.
ChannelIds []string `json:"channel_ids"`
// Since optionally limits FileInfos to those created at or after the given time, specified as Unix time in milliseconds.
Since int64 `json:"since"`
// IncludeDeleted if set includes deleted FileInfos.
IncludeDeleted bool `json:"include_deleted"`
// SortBy sorts the FileInfos by this field. The default is to sort by date created.
SortBy string `json:"sort_by"`
// SortDescending changes the sort direction to descending order when true.
SortDescending bool `json:"sort_descending"`
}
type FileInfo struct {
Id string `json:"id"`
CreatorId string `json:"user_id"`
PostId string `json:"post_id,omitempty"`
// ChannelId is the denormalized value from the corresponding post. Note that this value is
// potentially distinct from the ChannelId provided when the file is first uploaded and
// used to organize the directories in the file store, since in theory that same file
// could be attached to a post from a different channel (or not attached to a post at all).
ChannelId string `json:"channel_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Path string `json:"-"` // not sent back to the client
ThumbnailPath string `json:"-"` // not sent back to the client
PreviewPath string `json:"-"` // not sent back to the client
Name string `json:"name"`
Extension string `json:"extension"`
Size int64 `json:"size"`
MimeType string `json:"mime_type"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
HasPreviewImage bool `json:"has_preview_image,omitempty"`
MiniPreview *[]byte `json:"mini_preview"` // declared as *[]byte to avoid postgres/mysql differences in deserialization
Content string `json:"-"`
RemoteId *string `json:"remote_id"`
Archived bool `json:"archived"`
}
func (fi *FileInfo) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": fi.Id,
"creator_id": fi.CreatorId,
"post_id": fi.PostId,
"channel_id": fi.ChannelId,
"create_at": fi.CreateAt,
"update_at": fi.UpdateAt,
"delete_at": fi.DeleteAt,
"name": fi.Name,
"extension": fi.Extension,
"size": fi.Size,
}
}
func (fi *FileInfo) PreSave() {
if fi.Id == "" {
fi.Id = NewId()
}
if fi.CreateAt == 0 {
fi.CreateAt = GetMillis()
}
if fi.UpdateAt < fi.CreateAt {
fi.UpdateAt = fi.CreateAt
}
if fi.RemoteId == nil {
fi.RemoteId = NewString("")
}
}
func (fi *FileInfo) IsValid() *AppError {
if !IsValidId(fi.Id) {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(fi.CreatorId) && fi.CreatorId != "nouser" {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}
if fi.PostId != "" && !IsValidId(fi.PostId) {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}
if fi.CreateAt == 0 {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}
if fi.UpdateAt == 0 {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}
if fi.Path == "" {
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
}
return nil
}
func (fi *FileInfo) IsImage() bool {
return strings.HasPrefix(fi.MimeType, "image")
}
func (fi *FileInfo) IsSvg() bool {
return fi.MimeType == "image/svg+xml"
}
func NewInfo(name string) *FileInfo {
info := &FileInfo{
Name: name,
}
extension := strings.ToLower(filepath.Ext(name))
info.MimeType = mime.TypeByExtension(extension)
if extension != "" && extension[0] == '.' {
// The client expects a file extension without the leading period
info.Extension = extension[1:]
} else {
info.Extension = extension
}
return info
}
func GetInfoForBytes(name string, data io.ReadSeeker, size int) (*FileInfo, *AppError) {
info := &FileInfo{
Name: name,
Size: int64(size),
}
var err *AppError
extension := strings.ToLower(filepath.Ext(name))
info.MimeType = mime.TypeByExtension(extension)
if extension != "" {
// The client expects a file extension without the leading period
info.Extension = extension[1:]
} else {
info.Extension = extension
}
if info.IsImage() {
// Only set the width and height if it's actually an image that we can understand
if config, _, err := image.DecodeConfig(data); err == nil {
info.Width = config.Width
info.Height = config.Height
if info.MimeType == "image/gif" {
// Just show the gif itself instead of a preview image for animated gifs
data.Seek(0, io.SeekStart)
frameCount, err := imgutils.CountGIFFrames(data)
if err != nil {
// Still return the rest of the info even though it doesn't appear to be an actual gif
info.HasPreviewImage = true
return info, NewAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
info.HasPreviewImage = frameCount == 1
} else {
info.HasPreviewImage = true
}
}
}
return info, err
}
func GetEtagForFileInfos(infos []*FileInfo) string {
if len(infos) == 0 {
return Etag()
}
var maxUpdateAt int64
for _, info := range infos {
if info.UpdateAt > maxUpdateAt {
maxUpdateAt = info.UpdateAt
}
}
return Etag(infos[0].PostId, maxUpdateAt)
}
func (fi *FileInfo) MakeContentInaccessible() {
if fi == nil {
return
}
fi.Archived = true
fi.Content = ""
fi.HasPreviewImage = false
fi.MiniPreview = nil
fi.Path = ""
fi.PreviewPath = ""
fi.ThumbnailPath = ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"sort"
)
type FileInfoList struct {
Order []string `json:"order"`
FileInfos map[string]*FileInfo `json:"file_infos"`
NextFileInfoId string `json:"next_file_info_id"`
PrevFileInfoId string `json:"prev_file_info_id"`
// If there are inaccessible files, FirstInaccessibleFileTime is the time of the latest inaccessible file
FirstInaccessibleFileTime int64 `json:"first_inaccessible_file_time"`
}
func NewFileInfoList() *FileInfoList {
return &FileInfoList{
Order: make([]string, 0),
FileInfos: make(map[string]*FileInfo),
NextFileInfoId: "",
PrevFileInfoId: "",
}
}
func (o *FileInfoList) ToSlice() []*FileInfo {
var fileInfos []*FileInfo
for _, id := range o.Order {
fileInfos = append(fileInfos, o.FileInfos[id])
}
return fileInfos
}
func (o *FileInfoList) MakeNonNil() {
if o.Order == nil {
o.Order = make([]string, 0)
}
if o.FileInfos == nil {
o.FileInfos = make(map[string]*FileInfo)
}
}
func (o *FileInfoList) AddOrder(id string) {
if o.Order == nil {
o.Order = make([]string, 0, 128)
}
o.Order = append(o.Order, id)
}
func (o *FileInfoList) AddFileInfo(fileInfo *FileInfo) {
if o.FileInfos == nil {
o.FileInfos = make(map[string]*FileInfo)
}
o.FileInfos[fileInfo.Id] = fileInfo
}
func (o *FileInfoList) UniqueOrder() {
keys := make(map[string]bool)
order := []string{}
for _, fileInfoId := range o.Order {
if _, value := keys[fileInfoId]; !value {
keys[fileInfoId] = true
order = append(order, fileInfoId)
}
}
o.Order = order
}
func (o *FileInfoList) Extend(other *FileInfoList) {
for fileInfoId := range other.FileInfos {
o.AddFileInfo(other.FileInfos[fileInfoId])
}
for _, fileInfoId := range other.Order {
o.AddOrder(fileInfoId)
}
o.UniqueOrder()
}
func (o *FileInfoList) SortByCreateAt() {
sort.Slice(o.Order, func(i, j int) bool {
return o.FileInfos[o.Order[i]].CreateAt > o.FileInfos[o.Order[j]].CreateAt
})
}
func (o *FileInfoList) Etag() string {
id := "0"
var t int64 = 0
for _, v := range o.FileInfos {
if v.UpdateAt > t {
t = v.UpdateAt
id = v.Id
} else if v.UpdateAt == t && v.Id > id {
t = v.UpdateAt
id = v.Id
}
}
orderId := ""
if len(o.Order) > 0 {
orderId = o.Order[0]
}
return Etag(orderId, id, t)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type FileInfoSearchMatches map[string][]string
type FileInfoSearchResults struct {
*FileInfoList
Matches FileInfoSearchMatches `json:"matches"`
}
func MakeFileInfoSearchResults(fileInfos *FileInfoList, matches FileInfoSearchMatches) *FileInfoSearchResults {
return &FileInfoSearchResults{
fileInfos,
matches,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
type GithubReleaseInfo struct {
Id int `json:"id"`
TagName string `json:"tag_name"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
PublishedAt string `json:"published_at"`
Body string `json:"body"`
Url string `json:"html_url"`
}
func (g *GithubReleaseInfo) IsValid() *AppError {
if g.Id == 0 {
return NewAppError("GithubReleaseInfo.IsValid", NoTranslation, nil, "empty ID", http.StatusInternalServerError)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"regexp"
)
const (
GroupSourceLdap GroupSource = "ldap"
GroupSourceCustom GroupSource = "custom"
GroupNameMaxLength = 64
GroupSourceMaxLength = 64
GroupDisplayNameMaxLength = 128
GroupDescriptionMaxLength = 1024
GroupRemoteIDMaxLength = 48
)
type GroupSource string
var allGroupSources = []GroupSource{
GroupSourceLdap,
GroupSourceCustom,
}
var groupSourcesRequiringRemoteID = []GroupSource{
GroupSourceLdap,
}
type Group struct {
Id string `json:"id"`
Name *string `json:"name,omitempty"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Source GroupSource `json:"source"`
RemoteId *string `json:"remote_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
HasSyncables bool `db:"-" json:"has_syncables"`
MemberCount *int `db:"-" json:"member_count,omitempty"`
AllowReference bool `json:"allow_reference"`
ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"`
ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"`
}
func (group *Group) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": group.Id,
"source": group.Source,
"remote_id": group.RemoteId,
"create_at": group.CreateAt,
"update_at": group.UpdateAt,
"delete_at": group.DeleteAt,
"has_syncables": group.HasSyncables,
"member_count": group.MemberCount,
"allow_reference": group.AllowReference,
}
}
type GroupWithUserIds struct {
Group
UserIds []string `json:"user_ids"`
}
func (group *GroupWithUserIds) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": group.Id,
"source": group.Source,
"remote_id": group.RemoteId,
"create_at": group.CreateAt,
"update_at": group.UpdateAt,
"delete_at": group.DeleteAt,
"has_syncables": group.HasSyncables,
"member_count": group.MemberCount,
"allow_reference": group.AllowReference,
"user_ids": group.UserIds,
}
}
type GroupWithSchemeAdmin struct {
Group
SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"`
}
type GroupsAssociatedToChannelWithSchemeAdmin struct {
ChannelId string `json:"channel_id"`
Group
SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"`
}
type GroupsAssociatedToChannel struct {
ChannelId string `json:"channel_id"`
Groups []*GroupWithSchemeAdmin `json:"groups"`
}
type GroupPatch struct {
Name *string `json:"name"`
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
AllowReference *bool `json:"allow_reference"`
// For security reasons (including preventing unintended LDAP group synchronization) do no allow a Group's RemoteId or Source field to be
// included in patches.
}
type LdapGroupSearchOpts struct {
Q string
IsLinked *bool
IsConfigured *bool
}
type GroupSearchOpts struct {
Q string
NotAssociatedToTeam string
NotAssociatedToChannel string
IncludeMemberCount bool
FilterAllowReference bool
PageOpts *PageOpts
Since int64
Source GroupSource
// FilterParentTeamPermitted filters the groups to the intersect of the
// set associated to the parent team and those returned by the query.
// If the parent team is not group-constrained or if NotAssociatedToChannel
// is not set then this option is ignored.
FilterParentTeamPermitted bool
// FilterHasMember filters the groups to the intersect of the
// set returned by the query and those that have the given user as a member.
FilterHasMember string
IncludeChannelMemberCount string
IncludeTimezones bool
}
type GetGroupOpts struct {
IncludeMemberCount bool
}
type PageOpts struct {
Page int
PerPage int
}
type GroupStats struct {
GroupID string `json:"group_id"`
TotalMemberCount int64 `json:"total_member_count"`
}
type GroupModifyMembers struct {
UserIds []string `json:"user_ids"`
}
func (group *GroupModifyMembers) Auditable() map[string]interface{} {
return map[string]interface{}{
"user_ids": group.UserIds,
}
}
func (group *Group) Patch(patch *GroupPatch) {
if patch.Name != nil {
group.Name = patch.Name
}
if patch.DisplayName != nil {
group.DisplayName = *patch.DisplayName
}
if patch.Description != nil {
group.Description = *patch.Description
}
if patch.AllowReference != nil {
group.AllowReference = *patch.AllowReference
}
}
func (group *Group) IsValidForCreate() *AppError {
appErr := group.IsValidName()
if appErr != nil {
return appErr
}
if l := len(group.DisplayName); l == 0 || l > GroupDisplayNameMaxLength {
return NewAppError("Group.IsValidForCreate", "model.group.display_name.app_error", map[string]any{"GroupDisplayNameMaxLength": GroupDisplayNameMaxLength}, "", http.StatusBadRequest)
}
if len(group.Description) > GroupDescriptionMaxLength {
return NewAppError("Group.IsValidForCreate", "model.group.description.app_error", map[string]any{"GroupDescriptionMaxLength": GroupDescriptionMaxLength}, "", http.StatusBadRequest)
}
isValidSource := false
for _, groupSource := range allGroupSources {
if group.Source == groupSource {
isValidSource = true
break
}
}
if !isValidSource {
return NewAppError("Group.IsValidForCreate", "model.group.source.app_error", nil, "", http.StatusBadRequest)
}
if (group.GetRemoteId() == "" && group.requiresRemoteId()) || len(group.GetRemoteId()) > GroupRemoteIDMaxLength {
return NewAppError("Group.IsValidForCreate", "model.group.remote_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (group *Group) requiresRemoteId() bool {
for _, groupSource := range groupSourcesRequiringRemoteID {
if groupSource == group.Source {
return true
}
}
return false
}
func (group *Group) IsValidForUpdate() *AppError {
if !IsValidId(group.Id) {
return NewAppError("Group.IsValidForUpdate", "app.group.id.app_error", nil, "", http.StatusBadRequest)
}
if group.CreateAt == 0 {
return NewAppError("Group.IsValidForUpdate", "model.group.create_at.app_error", nil, "", http.StatusBadRequest)
}
if group.UpdateAt == 0 {
return NewAppError("Group.IsValidForUpdate", "model.group.update_at.app_error", nil, "", http.StatusBadRequest)
}
if appErr := group.IsValidForCreate(); appErr != nil {
return appErr
}
return nil
}
var validGroupnameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
func (group *Group) IsValidName() *AppError {
if group.Name == nil {
if group.AllowReference {
return NewAppError("Group.IsValidName", "model.group.name.app_error", map[string]any{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest)
}
} else {
if l := len(*group.Name); l == 0 || l > GroupNameMaxLength {
return NewAppError("Group.IsValidName", "model.group.name.invalid_length.app_error", map[string]any{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest)
}
if *group.Name == UserNotifyAll || *group.Name == ChannelMentionsNotifyProp || *group.Name == UserNotifyHere {
return NewAppError("IsValidName", "model.group.name.reserved_name.app_error", nil, "", http.StatusBadRequest)
}
if !validGroupnameChars.MatchString(*group.Name) {
return NewAppError("Group.IsValidName", "model.group.name.invalid_chars.app_error", nil, "", http.StatusBadRequest)
}
}
return nil
}
func (group *Group) GetName() string {
if group.Name == nil {
return ""
}
return *group.Name
}
func (group *Group) GetRemoteId() string {
if group.RemoteId == nil {
return ""
}
return *group.RemoteId
}
type GroupsWithCount struct {
Groups []*Group `json:"groups"`
TotalCount int64 `json:"total_count"`
}
type CreateDefaultMembershipParams struct {
Since int64
ReAddRemovedMembers bool
ScopedUserID *string
ScopedTeamID *string
ScopedChannelID *string
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "net/http"
type GroupMember struct {
GroupId string `json:"group_id"`
UserId string `json:"user_id"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
}
func (gm *GroupMember) IsValid() *AppError {
if !IsValidId(gm.GroupId) {
return NewAppError("GroupMember.IsValid", "model.group_member.group_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(gm.UserId) {
return NewAppError("GroupMember.IsValid", "model.group_member.user_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"net/http"
)
type GroupSyncableType string
const (
GroupSyncableTypeTeam GroupSyncableType = "Team"
GroupSyncableTypeChannel GroupSyncableType = "Channel"
)
func (gst GroupSyncableType) String() string {
return string(gst)
}
type GroupSyncable struct {
GroupId string `json:"group_id"`
// SyncableId represents the Id of the model that is being synced with the group, for example a ChannelId or
// TeamId.
SyncableId string `db:"-" json:"-"`
AutoAdd bool `json:"auto_add"`
SchemeAdmin bool `json:"scheme_admin"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
UpdateAt int64 `json:"update_at"`
Type GroupSyncableType `db:"-" json:"-"`
// Values joined in from the associated team and/or channel
ChannelDisplayName string `db:"-" json:"-"`
TeamDisplayName string `db:"-" json:"-"`
TeamType string `db:"-" json:"-"`
ChannelType string `db:"-" json:"-"`
TeamID string `db:"-" json:"-"`
}
func (syncable *GroupSyncable) Auditable() map[string]interface{} {
return map[string]interface{}{
"group_id": syncable.GroupId,
"syncable_id": syncable.SyncableId,
"auto_add": syncable.AutoAdd,
"scheme_admin": syncable.SchemeAdmin,
"create_at": syncable.CreateAt,
"delete_at": syncable.DeleteAt,
"update_at": syncable.UpdateAt,
"type": syncable.Type,
"channel_display_name": syncable.ChannelDisplayName,
"team_display_name": syncable.TeamDisplayName,
"team_type": syncable.TeamType,
"channel_type": syncable.ChannelType,
"team_id": syncable.TeamID,
}
}
func (syncable *GroupSyncable) IsValid() *AppError {
if !IsValidId(syncable.GroupId) {
return NewAppError("GroupSyncable.SyncableIsValid", "model.group_syncable.group_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(syncable.SyncableId) {
return NewAppError("GroupSyncable.SyncableIsValid", "model.group_syncable.syncable_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (syncable *GroupSyncable) UnmarshalJSON(b []byte) error {
var kvp map[string]any
err := json.Unmarshal(b, &kvp)
if err != nil {
return err
}
var channelId string
var teamId string
for key, value := range kvp {
switch key {
case "team_id":
teamId = value.(string)
case "channel_id":
channelId = value.(string)
case "group_id":
syncable.GroupId = value.(string)
case "auto_add":
syncable.AutoAdd = value.(bool)
default:
}
}
if channelId != "" {
syncable.TeamID = teamId
syncable.SyncableId = channelId
syncable.Type = GroupSyncableTypeChannel
} else {
syncable.SyncableId = teamId
syncable.Type = GroupSyncableTypeTeam
}
return nil
}
func (syncable *GroupSyncable) MarshalJSON() ([]byte, error) {
type Alias GroupSyncable
switch syncable.Type {
case GroupSyncableTypeTeam:
return json.Marshal(&struct {
TeamID string `json:"team_id"`
TeamDisplayName string `json:"team_display_name,omitempty"`
TeamType string `json:"team_type,omitempty"`
Type GroupSyncableType `json:"type,omitempty"`
*Alias
}{
TeamDisplayName: syncable.TeamDisplayName,
TeamType: syncable.TeamType,
TeamID: syncable.SyncableId,
Type: syncable.Type,
Alias: (*Alias)(syncable),
})
case GroupSyncableTypeChannel:
return json.Marshal(&struct {
ChannelID string `json:"channel_id"`
ChannelDisplayName string `json:"channel_display_name,omitempty"`
ChannelType string `json:"channel_type,omitempty"`
Type GroupSyncableType `json:"type,omitempty"`
TeamID string `json:"team_id,omitempty"`
TeamDisplayName string `json:"team_display_name,omitempty"`
TeamType string `json:"team_type,omitempty"`
*Alias
}{
ChannelID: syncable.SyncableId,
ChannelDisplayName: syncable.ChannelDisplayName,
ChannelType: syncable.ChannelType,
Type: syncable.Type,
TeamID: syncable.TeamID,
TeamDisplayName: syncable.TeamDisplayName,
TeamType: syncable.TeamType,
Alias: (*Alias)(syncable),
})
default:
return nil, fmt.Errorf("unknown syncable type: %s", syncable.Type)
}
}
type GroupSyncablePatch struct {
AutoAdd *bool `json:"auto_add"`
SchemeAdmin *bool `json:"scheme_admin"`
}
func (syncable *GroupSyncablePatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"auto_add": syncable.AutoAdd,
"scheme_admin": syncable.SchemeAdmin,
}
}
func (syncable *GroupSyncable) Patch(patch *GroupSyncablePatch) {
if patch.AutoAdd != nil {
syncable.AutoAdd = *patch.AutoAdd
}
if patch.SchemeAdmin != nil {
syncable.SchemeAdmin = *patch.SchemeAdmin
}
}
type UserTeamIDPair struct {
UserID string
TeamID string
}
type UserChannelIDPair struct {
UserID string
ChannelID string
}
func NewGroupTeam(groupID, teamID string, autoAdd bool) *GroupSyncable {
return &GroupSyncable{
GroupId: groupID,
SyncableId: teamID,
Type: GroupSyncableTypeTeam,
AutoAdd: autoAdd,
}
}
func NewGroupChannel(groupID, channelID string, autoAdd bool) *GroupSyncable {
return &GroupSyncable{
GroupId: groupID,
SyncableId: channelID,
Type: GroupSyncableTypeChannel,
AutoAdd: autoAdd,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
type GuestsInvite struct {
Emails []string `json:"emails"`
Channels []string `json:"channels"`
Message string `json:"message"`
}
func (i *GuestsInvite) Auditable() map[string]interface{} {
return map[string]interface{}{
"emails": i.Emails,
"channels": i.Channels,
}
}
// IsValid validates the user and returns an error if it isn't configured
// correctly.
func (i *GuestsInvite) IsValid() *AppError {
if len(i.Emails) == 0 {
return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.emails.app_error", nil, "", http.StatusBadRequest)
}
for _, email := range i.Emails {
if len(email) > UserEmailMaxLength || email == "" || !IsValidEmail(email) {
return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.email.app_error", nil, "email="+email, http.StatusBadRequest)
}
}
if len(i.Channels) == 0 {
return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channels.app_error", nil, "", http.StatusBadRequest)
}
for _, channel := range i.Channels {
if len(channel) != 26 {
return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channel.app_error", nil, "channel="+channel, http.StatusBadRequest)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"bytes"
"encoding/json"
"io"
"net/http"
"regexp"
)
const (
DefaultWebhookUsername = "webhook"
)
type IncomingWebhook struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
ChannelLocked bool `json:"channel_locked"`
}
func (o *IncomingWebhook) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
"user_id": o.UserId,
"channel_id": o.ChannelId,
"team_id": o.TeamId,
"display_name": o.DisplayName,
"description": o.Description,
"username": o.Username,
"icon_url:": o.IconURL,
"channel_locked": o.ChannelLocked,
}
}
type IncomingWebhookRequest struct {
Text string `json:"text"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
ChannelName string `json:"channel"`
Props StringInterface `json:"props"`
Attachments []*SlackAttachment `json:"attachments"`
Type string `json:"type"`
IconEmoji string `json:"icon_emoji"`
}
func (o *IncomingWebhook) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.TeamId) {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.DisplayName) > 64 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Description) > 500 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Username) > 64 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.username.app_error", nil, "", http.StatusBadRequest)
}
if len(o.IconURL) > 1024 {
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.icon_url.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (o *IncomingWebhook) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
}
func (o *IncomingWebhook) PreUpdate() {
o.UpdateAt = GetMillis()
}
// escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice.
// Context:
// JSON strings are not supposed to contain control characters such as \n, \t,
// ... but some incoming webhooks might still send invalid JSON and we want to
// try to handle that. An example invalid JSON string from an incoming webhook
// might look like this (strings for both "text" and "fallback" attributes are
// invalid JSON strings because they contain unescaped newlines and tabs):
//
// `{
// "text": "this is a test
// that contains a newline and tabs",
// "attachments": [
// {
// "fallback": "Required plain-text summary of the attachment
// that contains a newline and tabs",
// "color": "#36a64f",
// ...
// "text": "Optional text that appears within the attachment
// that contains a newline and tabs",
// ...
// "thumb_url": "http://example.com/path/to/thumb.png"
// }
// ]
// }`
//
// This function will search for `"key": "value"` pairs, and escape \n, \t
// from the value.
func escapeControlCharsFromPayload(by []byte) []byte {
// we'll search for `"text": "..."` or `"fallback": "..."`, ...
keys := "text|fallback|pretext|author_name|title|value"
// the regexp reads like this:
// (?s): this flag let . match \n (default is false)
// "(keys)": we search for the keys defined above
// \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs
// ": a double-quote
// (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote
// ": a double-quote
r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"`
re := regexp.MustCompile(r)
// the function that will escape \n and \t on the regexp matches
repl := func(b []byte) []byte {
if bytes.Contains(b, []byte("\n")) {
b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1)
}
if bytes.Contains(b, []byte("\t")) {
b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1)
}
return b
}
return re.ReplaceAllFunc(by, repl)
}
func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
decoder := json.NewDecoder(bytes.NewReader(by))
var o IncomingWebhookRequest
err := decoder.Decode(&o)
if err == nil {
return &o, nil
}
return nil, err
}
func IncomingWebhookRequestFromJSON(data io.Reader) (*IncomingWebhookRequest, *AppError) {
buf := new(bytes.Buffer)
buf.ReadFrom(data)
by := buf.Bytes()
// Try to decode the JSON data. Only if it fails, try to escape control
// characters from the strings contained in the JSON data.
o, err := decodeIncomingWebhookRequest(by)
if err != nil {
o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by))
if err != nil {
return nil, NewAppError("IncomingWebhookRequestFromJSON", "model.incoming_hook.parse_data.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
o.Attachments = StringifySlackFieldValue(o.Attachments)
return o, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"time"
)
type PostCountGrouping string
const (
TimeRangeToday string = "today"
TimeRange7Day string = "7_day"
TimeRange28Day string = "28_day"
PostsByHour PostCountGrouping = "hour"
PostsByDay PostCountGrouping = "day"
)
type InsightsOpts struct {
StartUnixMilli int64
Page int
PerPage int
}
type InsightsListData struct {
HasNext bool `json:"has_next"`
}
// Top Reactions
type TopReactionList struct {
InsightsListData
Items []*TopReaction `json:"items"`
}
type TopReaction struct {
EmojiName string `json:"emoji_name"`
Count int64 `json:"count"`
}
// Top Channels
type TopChannelList struct {
InsightsListData
Items []*TopChannel `json:"items"`
PostCountByDuration ChannelPostCountByDuration `json:"channel_post_counts_by_duration"`
}
func (t *TopChannelList) ChannelIDs() []string {
var ids []string
for _, item := range t.Items {
ids = append(ids, item.ID)
}
return ids
}
type TopChannel struct {
ID string `json:"id"`
Type ChannelType `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
TeamID string `json:"team_id"`
MessageCount int64 `json:"message_count"`
}
// Top Channels
type TopInactiveChannelList struct {
InsightsListData
Items []*TopInactiveChannel `json:"items"`
}
type TopInactiveChannel struct {
ID string `json:"id"`
Type ChannelType `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
LastActivityAt int64 `json:"last_activity_at"`
Participants StringArray `json:"participants"`
MessageCount int64 `json:"-"`
}
// Top Threads
type TopThreadList struct {
InsightsListData
Items []*TopThread `json:"items"`
}
type TopThread struct {
PostId string `json:"-"`
ReplyCount int64 `json:"-"`
ChannelId string `json:"channel_id"`
DisplayName string `json:"channel_display_name"`
Name string `json:"channel_name"`
Participants StringArray `json:"participants"`
UserId string `json:"-"`
UserInformation *InsightUserInformation `json:"user_information"`
Post *Post `json:"post"`
}
type InsightUserInformation struct {
Id string `json:"id"`
LastPictureUpdate int64 `json:"last_picture_update"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
NickName string `json:"nickname"`
Username string `json:"username"`
}
type TopDMInsightUserInformation struct {
InsightUserInformation
Position string `json:"position"`
}
type NewTeamMembersList struct {
InsightsListData
Items []*NewTeamMember `json:"items"`
TotalCount int64 `json:"total_count"`
}
type NewTeamMember struct {
Id string `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Nickname string `json:"nickname"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
CreateAt int64 `json:"create_at"`
}
type DurationPostCount struct {
ChannelID string `db:"channelid"`
// Duration is an ISO8601 date string.
Duration string `db:"duration"`
PostCount int `db:"postcount"`
}
// Top DMs
type TopDM struct {
MessageCount int64 `json:"post_count"`
OutgoingMessageCount int64 `json:"outgoing_message_count"`
Participants string `json:"-"`
ChannelId string `json:"-"`
SecondParticipant *TopDMInsightUserInformation `json:"second_participant"`
}
type OutgoingMessageQueryResult struct {
ChannelId string
MessageCount int
}
type TopDMList struct {
InsightsListData
Items []*TopDM `json:"items"`
}
func TimeRangeToNumberDays(timeRange string) int {
var n int
switch timeRange {
case TimeRangeToday:
n = 1
case TimeRange7Day:
n = 7
case TimeRange28Day:
n = 28
}
return n
}
// ChannelPostCountByDuration contains a count of posts by channel id, grouped by ISO8601 date string.
// Example 1 (grouped by day):
//
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-12": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
//
// Example 2 (grouped by hour):
//
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11T01": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-11T02": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
type ChannelPostCountByDuration map[string]map[string]int
func blankChannelCountsMap(channelIDs []string) map[string]int {
blankChannelCounts := map[string]int{}
for _, id := range channelIDs {
blankChannelCounts[id] = 0
}
return blankChannelCounts
}
func ToDailyPostCountViewModel(dpc []*DurationPostCount, startTime *time.Time, numDays int, channelIDs []string) ChannelPostCountByDuration {
viewModel := ChannelPostCountByDuration{}
keyTime := *startTime
nowAtLocation := time.Now().In(startTime.Location())
if numDays == 1 {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format(time.RFC3339)
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(time.Hour)
}
} else {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format("2006-01-02")
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(24 * time.Hour)
}
}
for _, item := range dpc {
var parseFormat string
var keyFormat string
if numDays == 1 {
parseFormat = "2006-01-02T15 "
keyFormat = time.RFC3339
} else {
parseFormat = "2006-01-02"
keyFormat = parseFormat
}
durTime, err := time.ParseInLocation(parseFormat, item.Duration, startTime.Location())
if err != nil {
continue
}
localizedKey := durTime.Format(keyFormat)
_, hasKey := viewModel[localizedKey]
if !hasKey {
viewModel[localizedKey] = map[string]int{}
}
viewModel[localizedKey][item.ChannelID] = item.PostCount
}
return viewModel
}
// Deprecated: This method doesn't perform error checking.
// Use GetStartOfDayForTimeRange instead.
//
// StartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Time {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
}
return &resultTime
}
// GetStartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func GetStartOfDayForTimeRange(timeRange string, location *time.Location) (*time.Time, *AppError) {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRangeToday:
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
default:
return nil, NewAppError("GetStartOfDayForTimeRange", "model.insights.get_start_of_day_for_time_range.time_range.app_error", nil, "", http.StatusBadRequest)
}
return &resultTime, nil
}
// GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopReaction is assumed to be
// sorted by Count. Returns a TopReactionList.
func GetTopReactionListWithPagination(reactions []*TopReaction, limit int) *TopReactionList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(reactions) == limit+1) {
hasNext = true
reactions = reactions[:len(reactions)-1]
}
return &TopReactionList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: reactions}
}
// GetTopChannelListWithPagination adds a rank to each item in the given list of TopChannel and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopChannel is assumed to be
// sorted by Score. Returns a TopChannelList.
func GetTopChannelListWithPagination(channels []*TopChannel, limit int) *TopChannelList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(channels) == limit+1) {
hasNext = true
channels = channels[:len(channels)-1]
}
return &TopChannelList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: channels}
}
// GetTopThreadListWithPagination adds a rank to each item in the given list of TopThread and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopThread is assumed to be
// sorted by ReplyCount(score). Returns a TopThreadList.
func GetTopThreadListWithPagination(threads []*TopThread, limit int) *TopThreadList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(threads) == limit+1) {
hasNext = true
threads = threads[:len(threads)-1]
}
return &TopThreadList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: threads}
}
// GetTopInactiveChannelListWithPagination adds a rank to each item in the given list of TopInactiveChannel and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopInactiveChannel is assumed to be
// sorted by Score. Returns a TopInactiveChannelList.
func GetTopInactiveChannelListWithPagination(channels []*TopInactiveChannel, limit int) *TopInactiveChannelList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(channels) == limit+1) {
hasNext = true
channels = channels[:len(channels)-1]
}
return &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: channels}
}
// GetTopDMListWithPagination adds a rank to each item in the given list of TopDM and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopDM is assumed to be
// sorted by MessageCount(score). Returns a TopDMList.
func GetTopDMListWithPagination(dms []*TopDM, limit int) *TopDMList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(dms) == limit+1) {
hasNext = true
dms = dms[:len(dms)-1]
}
return &TopDMList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: dms}
}
func GetNewTeamMembersListWithPagination(teamMembers []*NewTeamMember, limit int) *NewTeamMembersList {
var hasNext bool
if (limit != 0) && (len(teamMembers) == limit+1) {
hasNext = true
teamMembers = teamMembers[:len(teamMembers)-1]
}
return &NewTeamMembersList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: teamMembers}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"reflect"
"strconv"
"strings"
)
const (
PostActionTypeButton = "button"
PostActionTypeSelect = "select"
InteractiveDialogTriggerTimeoutMilliseconds = 3000
)
var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"}
type DoPostActionRequest struct {
SelectedOption string `json:"selected_option,omitempty"`
Cookie string `json:"cookie,omitempty"`
}
type PostAction struct {
// A unique Action ID. If not set, generated automatically.
Id string `json:"id,omitempty"`
// The type of the interactive element. Currently supported are
// "select" and "button".
Type string `json:"type,omitempty"`
// The text on the button, or in the select placeholder.
Name string `json:"name,omitempty"`
// If the action is disabled.
Disabled bool `json:"disabled,omitempty"`
// Style defines a text and border style.
// Supported values are "default", "primary", "success", "good", "warning", "danger"
// and any hex color.
Style string `json:"style,omitempty"`
// DataSource indicates the data source for the select action. If left
// empty, the select is populated from Options. Other supported values
// are "users" and "channels".
DataSource string `json:"data_source,omitempty"`
// Options contains the values listed in a select dropdown on the post.
Options []*PostActionOptions `json:"options,omitempty"`
// DefaultOption contains the option, if any, that will appear as the
// default selection in a select box. It has no effect when used with
// other types of actions.
DefaultOption string `json:"default_option,omitempty"`
// Defines the interaction with the backend upon a user action.
// Integration contains Context, which is private plugin data;
// Integrations are stripped from Posts when they are sent to the
// client, or are encrypted in a Cookie.
Integration *PostActionIntegration `json:"integration,omitempty"`
Cookie string `json:"cookie,omitempty" db:"-"`
}
func (p *PostAction) Equals(input *PostAction) bool {
if p.Id != input.Id {
return false
}
if p.Type != input.Type {
return false
}
if p.Name != input.Name {
return false
}
if p.DataSource != input.DataSource {
return false
}
if p.DefaultOption != input.DefaultOption {
return false
}
if p.Cookie != input.Cookie {
return false
}
// Compare PostActionOptions
if len(p.Options) != len(input.Options) {
return false
}
for k := range p.Options {
if p.Options[k].Text != input.Options[k].Text {
return false
}
if p.Options[k].Value != input.Options[k].Value {
return false
}
}
// Compare PostActionIntegration
// If input is nil, then return true if original is also nil.
// Else return false.
if input.Integration == nil {
return p.Integration == nil
}
// Both are unequal and not nil.
if p.Integration.URL != input.Integration.URL {
return false
}
if len(p.Integration.Context) != len(input.Integration.Context) {
return false
}
for key, value := range p.Integration.Context {
inputValue, ok := input.Integration.Context[key]
if !ok {
return false
}
switch inputValue.(type) {
case string, bool, int, float64:
if value != inputValue {
return false
}
default:
if !reflect.DeepEqual(value, inputValue) {
return false
}
}
}
return true
}
// PostActionCookie is set by the server, serialized and encrypted into
// PostAction.Cookie. The clients should hold on to it, and include it with
// subsequent DoPostAction requests. This allows the server to access the
// action metadata even when it's not available in the database, for ephemeral
// posts.
type PostActionCookie struct {
Type string `json:"type,omitempty"`
PostId string `json:"post_id,omitempty"`
RootPostId string `json:"root_post_id,omitempty"`
ChannelId string `json:"channel_id,omitempty"`
DataSource string `json:"data_source,omitempty"`
Integration *PostActionIntegration `json:"integration,omitempty"`
RetainProps map[string]any `json:"retain_props,omitempty"`
RemoveProps []string `json:"remove_props,omitempty"`
}
type PostActionOptions struct {
Text string `json:"text"`
Value string `json:"value"`
}
type PostActionIntegration struct {
URL string `json:"url,omitempty"`
Context map[string]any `json:"context,omitempty"`
}
type PostActionIntegrationRequest struct {
UserId string `json:"user_id"`
UserName string `json:"user_name"`
ChannelId string `json:"channel_id"`
ChannelName string `json:"channel_name"`
TeamId string `json:"team_id"`
TeamName string `json:"team_domain"`
PostId string `json:"post_id"`
TriggerId string `json:"trigger_id"`
Type string `json:"type"`
DataSource string `json:"data_source"`
Context map[string]any `json:"context,omitempty"`
}
type PostActionIntegrationResponse struct {
Update *Post `json:"update"`
EphemeralText string `json:"ephemeral_text"`
SkipSlackParsing bool `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text.
}
type PostActionAPIResponse struct {
Status string `json:"status"` // needed to maintain backwards compatibility
TriggerId string `json:"trigger_id"`
}
type Dialog struct {
CallbackId string `json:"callback_id"`
Title string `json:"title"`
IntroductionText string `json:"introduction_text"`
IconURL string `json:"icon_url"`
Elements []DialogElement `json:"elements"`
SubmitLabel string `json:"submit_label"`
NotifyOnCancel bool `json:"notify_on_cancel"`
State string `json:"state"`
}
type DialogElement struct {
DisplayName string `json:"display_name"`
Name string `json:"name"`
Type string `json:"type"`
SubType string `json:"subtype"`
Default string `json:"default"`
Placeholder string `json:"placeholder"`
HelpText string `json:"help_text"`
Optional bool `json:"optional"`
MinLength int `json:"min_length"`
MaxLength int `json:"max_length"`
DataSource string `json:"data_source"`
Options []*PostActionOptions `json:"options"`
}
type OpenDialogRequest struct {
TriggerId string `json:"trigger_id"`
URL string `json:"url"`
Dialog Dialog `json:"dialog"`
}
type SubmitDialogRequest struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
CallbackId string `json:"callback_id"`
State string `json:"state"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
Submission map[string]any `json:"submission"`
Cancelled bool `json:"cancelled"`
}
type SubmitDialogResponse struct {
Error string `json:"error,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
}
func GenerateTriggerId(userId string, s crypto.Signer) (string, string, *AppError) {
clientTriggerId := NewId()
triggerData := strings.Join([]string{clientTriggerId, userId, strconv.FormatInt(GetMillis(), 10)}, ":") + ":"
h := crypto.SHA256
sum := h.New()
sum.Write([]byte(triggerData))
signature, err := s.Sign(rand.Reader, sum.Sum(nil), h)
if err != nil {
return "", "", NewAppError("GenerateTriggerId", "interactive_message.generate_trigger_id.signing_failed", nil, "", http.StatusInternalServerError).Wrap(err)
}
base64Sig := base64.StdEncoding.EncodeToString(signature)
triggerId := base64.StdEncoding.EncodeToString([]byte(triggerData + base64Sig))
return clientTriggerId, triggerId, nil
}
func (r *PostActionIntegrationRequest) GenerateTriggerId(s crypto.Signer) (string, string, *AppError) {
clientTriggerId, triggerId, appErr := GenerateTriggerId(r.UserId, s)
if appErr != nil {
return "", "", appErr
}
r.TriggerId = triggerId
return clientTriggerId, triggerId, nil
}
func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, string, *AppError) {
triggerIdBytes, err := base64.StdEncoding.DecodeString(triggerId)
if err != nil {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
split := strings.Split(string(triggerIdBytes), ":")
if len(split) != 4 {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.missing_data", nil, "", http.StatusBadRequest)
}
clientTriggerId := split[0]
userId := split[1]
timestampStr := split[2]
timestamp, _ := strconv.ParseInt(timestampStr, 10, 64)
now := GetMillis()
if now-timestamp > InteractiveDialogTriggerTimeoutMilliseconds {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]any{"Seconds": InteractiveDialogTriggerTimeoutMilliseconds / 1000}, "", http.StatusBadRequest)
}
signature, err := base64.StdEncoding.DecodeString(split[3])
if err != nil {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed_signature", nil, "", http.StatusBadRequest).Wrap(err)
}
var esig struct {
R, S *big.Int
}
if _, err := asn1.Unmarshal(signature, &esig); err != nil {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.signature_decode_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
triggerData := strings.Join([]string{clientTriggerId, userId, timestampStr}, ":") + ":"
h := crypto.SHA256
sum := h.New()
sum.Write([]byte(triggerData))
if !ecdsa.Verify(&s.PublicKey, sum.Sum(nil), esig.R, esig.S) {
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.verify_signature_failed", nil, "", http.StatusBadRequest)
}
return clientTriggerId, userId, nil
}
func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey) (string, string, *AppError) {
return DecodeAndVerifyTriggerId(r.TriggerId, s)
}
func (o *Post) StripActionIntegrations() {
attachments := o.Attachments()
if o.GetProp("attachments") != nil {
o.AddProp("attachments", attachments)
}
for _, attachment := range attachments {
for _, action := range attachment.Actions {
action.Integration = nil
}
}
}
func (o *Post) GetAction(id string) *PostAction {
for _, attachment := range o.Attachments() {
for _, action := range attachment.Actions {
if action != nil && action.Id == id {
return action
}
}
}
return nil
}
func (o *Post) GenerateActionIds() {
if o.GetProp("attachments") != nil {
o.AddProp("attachments", o.Attachments())
}
if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
for _, attachment := range attachments {
for _, action := range attachment.Actions {
if action != nil && action.Id == "" {
action.Id = NewId()
}
}
}
}
}
func AddPostActionCookies(o *Post, secret []byte) *Post {
p := o.Clone()
// retainedProps carry over their value from the old post, including no value
retainProps := map[string]any{}
removeProps := []string{}
for _, key := range PostActionRetainPropKeys {
value, ok := p.GetProps()[key]
if ok {
retainProps[key] = value
} else {
removeProps = append(removeProps, key)
}
}
attachments := p.Attachments()
for _, attachment := range attachments {
for _, action := range attachment.Actions {
c := &PostActionCookie{
Type: action.Type,
ChannelId: p.ChannelId,
DataSource: action.DataSource,
Integration: action.Integration,
RetainProps: retainProps,
RemoveProps: removeProps,
}
c.PostId = p.Id
if p.RootId == "" {
c.RootPostId = p.Id
} else {
c.RootPostId = p.RootId
}
b, _ := json.Marshal(c)
action.Cookie, _ = encryptPostActionCookie(string(b), secret)
}
}
return p
}
func encryptPostActionCookie(plain string, secret []byte) (string, error) {
if len(secret) == 0 {
return plain, nil
}
block, err := aes.NewCipher(secret)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesgcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return "", err
}
sealed := aesgcm.Seal(nil, nonce, []byte(plain), nil)
combined := append(nonce, sealed...)
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(combined)))
base64.StdEncoding.Encode(encoded, combined)
return string(encoded), nil
}
func DecryptPostActionCookie(encoded string, secret []byte) (string, error) {
if len(secret) == 0 {
return encoded, nil
}
block, err := aes.NewCipher(secret)
if err != nil {
return "", err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(encoded)))
n, err := base64.StdEncoding.Decode(decoded, []byte(encoded))
if err != nil {
return "", err
}
decoded = decoded[:n]
nonceSize := aesgcm.NonceSize()
if len(decoded) < nonceSize {
return "", fmt.Errorf("cookie too short")
}
nonce, decoded := decoded[:nonceSize], decoded[nonceSize:]
plain, err := aesgcm.Open(nil, nonce, decoded, nil)
if err != nil {
return "", err
}
return string(plain), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
)
type OrphanedRecord struct {
ParentId *string `json:"parent_id"`
ChildId *string `json:"child_id"`
}
type RelationalIntegrityCheckData struct {
ParentName string `json:"parent_name"`
ChildName string `json:"child_name"`
ParentIdAttr string `json:"parent_id_attr"`
ChildIdAttr string `json:"child_id_attr"`
Records []OrphanedRecord `json:"records"`
}
type IntegrityCheckResult struct {
Data any `json:"data"`
Err error `json:"err"`
}
func (r *IntegrityCheckResult) UnmarshalJSON(b []byte) error {
var data map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
if d, ok := data["data"]; ok && d != nil {
var rdata RelationalIntegrityCheckData
m := d.(map[string]any)
rdata.ParentName = m["parent_name"].(string)
rdata.ChildName = m["child_name"].(string)
rdata.ParentIdAttr = m["parent_id_attr"].(string)
rdata.ChildIdAttr = m["child_id_attr"].(string)
for _, recData := range m["records"].([]any) {
var record OrphanedRecord
m := recData.(map[string]any)
if val := m["parent_id"]; val != nil {
record.ParentId = NewString(val.(string))
}
if val := m["child_id"]; val != nil {
record.ChildId = NewString(val.(string))
}
rdata.Records = append(rdata.Records, record)
}
r.Data = rdata
}
if err, ok := data["err"]; ok && err != nil {
r.Err = errors.New(data["err"].(string))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"time"
)
const (
JobTypeDataRetention = "data_retention"
JobTypeMessageExport = "message_export"
JobTypeElasticsearchPostIndexing = "elasticsearch_post_indexing"
JobTypeElasticsearchPostAggregation = "elasticsearch_post_aggregation"
JobTypeBlevePostIndexing = "bleve_post_indexing"
JobTypeLdapSync = "ldap_sync"
JobTypeMigrations = "migrations"
JobTypePlugins = "plugins"
JobTypeExpiryNotify = "expiry_notify"
JobTypeProductNotices = "product_notices"
JobTypeActiveUsers = "active_users"
JobTypeImportProcess = "import_process"
JobTypeImportDelete = "import_delete"
JobTypeExportProcess = "export_process"
JobTypeExportDelete = "export_delete"
JobTypeCloud = "cloud"
JobTypeResendInvitationEmail = "resend_invitation_email"
JobTypeExtractContent = "extract_content"
JobTypeLastAccessiblePost = "last_accessible_post"
JobTypeLastAccessibleFile = "last_accessible_file"
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
JobTypeTrialNotifyAdmin = "trial_notify_admin"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
JobStatusPending = "pending"
JobStatusInProgress = "in_progress"
JobStatusSuccess = "success"
JobStatusError = "error"
JobStatusCancelRequested = "cancel_requested"
JobStatusCanceled = "canceled"
JobStatusWarning = "warning"
)
var AllJobTypes = [...]string{
JobTypeDataRetention,
JobTypeMessageExport,
JobTypeElasticsearchPostIndexing,
JobTypeElasticsearchPostAggregation,
JobTypeBlevePostIndexing,
JobTypeLdapSync,
JobTypeMigrations,
JobTypePlugins,
JobTypeExpiryNotify,
JobTypeProductNotices,
JobTypeActiveUsers,
JobTypeImportProcess,
JobTypeImportDelete,
JobTypeExportProcess,
JobTypeExportDelete,
JobTypeCloud,
JobTypeExtractContent,
JobTypeLastAccessiblePost,
JobTypeLastAccessibleFile,
}
type Job struct {
Id string `json:"id"`
Type string `json:"type"`
Priority int64 `json:"priority"`
CreateAt int64 `json:"create_at"`
StartAt int64 `json:"start_at"`
LastActivityAt int64 `json:"last_activity_at"`
Status string `json:"status"`
Progress int64 `json:"progress"`
Data StringMap `json:"data"`
}
func (j *Job) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": j.Id,
"type": j.Type,
"priority": j.Priority,
"create_at": j.CreateAt,
"start_at": j.StartAt,
"last_activity_at": j.LastActivityAt,
"status": j.Status,
"progress": j.Progress,
"data": j.Data, // TODO do we want this here
}
}
func (j *Job) IsValid() *AppError {
if !IsValidId(j.Id) {
return NewAppError("Job.IsValid", "model.job.is_valid.id.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
if j.CreateAt == 0 {
return NewAppError("Job.IsValid", "model.job.is_valid.create_at.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
switch j.Status {
case JobStatusPending,
JobStatusInProgress,
JobStatusSuccess,
JobStatusError,
JobStatusWarning,
JobStatusCancelRequested,
JobStatusCanceled:
default:
return NewAppError("Job.IsValid", "model.job.is_valid.status.app_error", nil, "id="+j.Id, http.StatusBadRequest)
}
return nil
}
type Worker interface {
Run()
Stop()
JobChannel() chan<- Job
IsEnabled(cfg *Config) bool
}
type Scheduler interface {
Enabled(cfg *Config) bool
NextScheduleTime(cfg *Config, now time.Time, pendingJobs bool, lastSuccessfulJob *Job) *time.Time
ScheduleJob(cfg *Config, pendingJobs bool, lastSuccessfulJob *Job) (*Job, *AppError)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
DayInSeconds = 24 * 60 * 60
DayInMilliseconds = DayInSeconds * 1000
ExpiredLicenseError = "api.license.add_license.expired.app_error"
InvalidLicenseError = "api.license.add_license.invalid.app_error"
LicenseGracePeriod = DayInMilliseconds * 10 //10 days
LicenseRenewalLink = "https://mattermost.com/renew/"
LicenseShortSkuE10 = "E10"
LicenseShortSkuE20 = "E20"
LicenseShortSkuProfessional = "professional"
LicenseShortSkuEnterprise = "enterprise"
)
const (
LicenseUpForRenewalEmailSent = "LicenseUpForRenewalEmailSent"
)
var (
trialDuration = 30*(time.Hour*24) + (time.Hour * 8) // 720 hours (30 days) + 8 hours is trial license duration
adminTrialDuration = 30*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 720 hours (30 days) + 23 hours, 59 mins and 59 seconds
// a sanctioned trial's duration is either more than the upper bound,
// or less than the lower bound
sanctionedTrialDurationLowerBound = 31*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 744 hours (31 days) + 23 hours, 59 mins and 59 seconds
sanctionedTrialDurationUpperBound = 29*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 696 hours (29 days) + 23 hours, 59 mins and 59 seconds
)
const (
TrueUpReviewTelemetryName = "true_up_review_sent"
TrueUpReviewAuthFeaturesMfa = "multi_factor_authentication"
TrueUpReviewAuthFeaturesADLdap = "ad_ldap_sign_in"
TrueUpReviewAuthFeaturesSaml = "saml_sign_in"
TrueUpReviewAuthFeatureOpenId = "openid_connect"
TrueUpReviewAuthFeatureGuestAccess = "guest_access"
)
type LicenseRecord struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
Bytes string `json:"-"`
}
type License struct {
Id string `json:"id"`
IssuedAt int64 `json:"issued_at"`
StartsAt int64 `json:"starts_at"`
ExpiresAt int64 `json:"expires_at"`
Customer *Customer `json:"customer"`
Features *Features `json:"features"`
SkuName string `json:"sku_name"`
SkuShortName string `json:"sku_short_name"`
IsTrial bool `json:"is_trial"`
IsGovSku bool `json:"is_gov_sku"`
SignupJWT *string `json:"signup_jwt"`
}
type Customer struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Company string `json:"company"`
}
type TrialLicenseRequest struct {
ServerID string `json:"server_id"`
Email string `json:"email"`
Name string `json:"name"`
SiteURL string `json:"site_url"`
SiteName string `json:"site_name"`
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
}
type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
LDAPGroups *bool `json:"ldap_groups"`
MFA *bool `json:"mfa"`
GoogleOAuth *bool `json:"google_oauth"`
Office365OAuth *bool `json:"office365_oauth"`
OpenId *bool `json:"openid"`
Compliance *bool `json:"compliance"`
Cluster *bool `json:"cluster"`
Metrics *bool `json:"metrics"`
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
Elasticsearch *bool `json:"elastic_search"`
Announcement *bool `json:"announcement"`
ThemeManagement *bool `json:"theme_management"`
EmailNotificationContents *bool `json:"email_notification_contents"`
DataRetention *bool `json:"data_retention"`
MessageExport *bool `json:"message_export"`
CustomPermissionsSchemes *bool `json:"custom_permissions_schemes"`
CustomTermsOfService *bool `json:"custom_terms_of_service"`
GuestAccounts *bool `json:"guest_accounts"`
GuestAccountsPermissions *bool `json:"guest_accounts_permissions"`
IDLoadedPushNotifications *bool `json:"id_loaded"`
LockTeammateNameDisplay *bool `json:"lock_teammate_name_display"`
EnterprisePlugins *bool `json:"enterprise_plugins"`
AdvancedLogging *bool `json:"advanced_logging"`
Cloud *bool `json:"cloud"`
SharedChannels *bool `json:"shared_channels"`
RemoteClusterService *bool `json:"remote_cluster_service"`
// after we enabled more features we'll need to control them with this
FutureFeatures *bool `json:"future_features"`
}
func (f *Features) ToMap() map[string]any {
return map[string]any{
"ldap": *f.LDAP,
"ldap_groups": *f.LDAPGroups,
"mfa": *f.MFA,
"google": *f.GoogleOAuth,
"office365": *f.Office365OAuth,
"openid": *f.OpenId,
"compliance": *f.Compliance,
"cluster": *f.Cluster,
"metrics": *f.Metrics,
"mhpns": *f.MHPNS,
"saml": *f.SAML,
"elastic_search": *f.Elasticsearch,
"email_notification_contents": *f.EmailNotificationContents,
"data_retention": *f.DataRetention,
"message_export": *f.MessageExport,
"custom_permissions_schemes": *f.CustomPermissionsSchemes,
"guest_accounts": *f.GuestAccounts,
"guest_accounts_permissions": *f.GuestAccountsPermissions,
"id_loaded": *f.IDLoadedPushNotifications,
"lock_teammate_name_display": *f.LockTeammateNameDisplay,
"enterprise_plugins": *f.EnterprisePlugins,
"advanced_logging": *f.AdvancedLogging,
"cloud": *f.Cloud,
"shared_channels": *f.SharedChannels,
"remote_cluster_service": *f.RemoteClusterService,
"future": *f.FutureFeatures,
}
}
func (f *Features) SetDefaults() {
if f.FutureFeatures == nil {
f.FutureFeatures = NewBool(true)
}
if f.Users == nil {
f.Users = NewInt(0)
}
if f.LDAP == nil {
f.LDAP = NewBool(*f.FutureFeatures)
}
if f.LDAPGroups == nil {
f.LDAPGroups = NewBool(*f.FutureFeatures)
}
if f.MFA == nil {
f.MFA = NewBool(*f.FutureFeatures)
}
if f.GoogleOAuth == nil {
f.GoogleOAuth = NewBool(*f.FutureFeatures)
}
if f.Office365OAuth == nil {
f.Office365OAuth = NewBool(*f.FutureFeatures)
}
if f.OpenId == nil {
f.OpenId = NewBool(*f.FutureFeatures)
}
if f.Compliance == nil {
f.Compliance = NewBool(*f.FutureFeatures)
}
if f.Cluster == nil {
f.Cluster = NewBool(*f.FutureFeatures)
}
if f.Metrics == nil {
f.Metrics = NewBool(*f.FutureFeatures)
}
if f.MHPNS == nil {
f.MHPNS = NewBool(*f.FutureFeatures)
}
if f.SAML == nil {
f.SAML = NewBool(*f.FutureFeatures)
}
if f.Elasticsearch == nil {
f.Elasticsearch = NewBool(*f.FutureFeatures)
}
if f.Announcement == nil {
f.Announcement = NewBool(true)
}
if f.ThemeManagement == nil {
f.ThemeManagement = NewBool(true)
}
if f.EmailNotificationContents == nil {
f.EmailNotificationContents = NewBool(*f.FutureFeatures)
}
if f.DataRetention == nil {
f.DataRetention = NewBool(*f.FutureFeatures)
}
if f.MessageExport == nil {
f.MessageExport = NewBool(*f.FutureFeatures)
}
if f.CustomPermissionsSchemes == nil {
f.CustomPermissionsSchemes = NewBool(*f.FutureFeatures)
}
if f.GuestAccounts == nil {
f.GuestAccounts = NewBool(*f.FutureFeatures)
}
if f.GuestAccountsPermissions == nil {
f.GuestAccountsPermissions = NewBool(*f.FutureFeatures)
}
if f.CustomTermsOfService == nil {
f.CustomTermsOfService = NewBool(*f.FutureFeatures)
}
if f.IDLoadedPushNotifications == nil {
f.IDLoadedPushNotifications = NewBool(*f.FutureFeatures)
}
if f.LockTeammateNameDisplay == nil {
f.LockTeammateNameDisplay = NewBool(*f.FutureFeatures)
}
if f.EnterprisePlugins == nil {
f.EnterprisePlugins = NewBool(*f.FutureFeatures)
}
if f.AdvancedLogging == nil {
f.AdvancedLogging = NewBool(*f.FutureFeatures)
}
if f.Cloud == nil {
f.Cloud = NewBool(false)
}
if f.SharedChannels == nil {
f.SharedChannels = NewBool(*f.FutureFeatures)
}
if f.RemoteClusterService == nil {
f.RemoteClusterService = NewBool(*f.FutureFeatures)
}
}
func (l *License) IsExpired() bool {
return l.ExpiresAt < GetMillis()
}
func (l *License) IsPastGracePeriod() bool {
timeDiff := GetMillis() - l.ExpiresAt
return timeDiff > LicenseGracePeriod
}
func (l *License) IsWithinExpirationPeriod() bool {
days := l.DaysToExpiration()
return days <= 60 && days >= 58
}
func (l *License) DaysToExpiration() int {
dif := l.ExpiresAt - GetMillis()
d, _ := time.ParseDuration(fmt.Sprint(dif) + "ms")
days := d.Hours() / 24
return int(days)
}
func (l *License) IsStarted() bool {
return l.StartsAt < GetMillis()
}
func (l *License) IsCloud() bool {
return l != nil && l.Features != nil && l.Features.Cloud != nil && *l.Features.Cloud
}
func (l *License) IsTrialLicense() bool {
return l.IsTrial || (l.ExpiresAt-l.StartsAt) == trialDuration.Milliseconds() || (l.ExpiresAt-l.StartsAt) == adminTrialDuration.Milliseconds()
}
func (l *License) IsSanctionedTrial() bool {
duration := l.ExpiresAt - l.StartsAt
return l.IsTrialLicense() &&
(duration >= sanctionedTrialDurationLowerBound.Milliseconds() || duration <= sanctionedTrialDurationUpperBound.Milliseconds())
}
func (l *License) HasEnterpriseMarketplacePlugins() bool {
return *l.Features.EnterprisePlugins ||
l.SkuShortName == LicenseShortSkuE20 ||
l.SkuShortName == LicenseShortSkuProfessional ||
l.SkuShortName == LicenseShortSkuEnterprise
}
func (l *License) HasRemoteClusterService() bool {
if l == nil {
return false
}
// If SharedChannels is enabled then RemoteClusterService must be enabled.
if l.HasSharedChannels() {
return true
}
return (l.Features != nil && l.Features.RemoteClusterService != nil && *l.Features.RemoteClusterService) ||
l.SkuShortName == LicenseShortSkuProfessional ||
l.SkuShortName == LicenseShortSkuEnterprise
}
func (l *License) HasSharedChannels() bool {
if l == nil {
return false
}
return (l.Features != nil && l.Features.SharedChannels != nil && *l.Features.SharedChannels) ||
l.SkuShortName == LicenseShortSkuProfessional ||
l.SkuShortName == LicenseShortSkuEnterprise
}
// NewTestLicense returns a license that expires in the future and has the given features.
func NewTestLicense(features ...string) *License {
ret := &License{
ExpiresAt: GetMillis() + 90*DayInMilliseconds,
Customer: &Customer{},
Features: &Features{},
}
ret.Features.SetDefaults()
featureMap := map[string]bool{}
for _, feature := range features {
featureMap[feature] = true
}
featureJson, _ := json.Marshal(featureMap)
json.Unmarshal(featureJson, &ret.Features)
return ret
}
// NewTestLicense returns a license that expires in the future and set as false the given features.
func NewTestLicenseWithFalseDefaults(features ...string) *License {
ret := &License{
ExpiresAt: GetMillis() + 90*DayInMilliseconds,
Customer: &Customer{},
Features: &Features{},
}
ret.Features.SetDefaults()
featureMap := map[string]bool{}
for _, feature := range features {
featureMap[feature] = false
}
featureJson, _ := json.Marshal(featureMap)
json.Unmarshal(featureJson, &ret.Features)
return ret
}
func NewTestLicenseSKU(skuShortName string, features ...string) *License {
lic := NewTestLicense(features...)
lic.SkuShortName = skuShortName
return lic
}
func (lr *LicenseRecord) IsValid() *AppError {
if !IsValidId(lr.Id) {
return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if lr.CreateAt == 0 {
return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if lr.Bytes == "" || len(lr.Bytes) > 10000 {
return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (lr *LicenseRecord) PreSave() {
lr.CreateAt = GetMillis()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/binary"
"encoding/json"
"fmt"
"hash/fnv"
"net/http"
"time"
"unicode/utf8"
"github.com/dyatlov/go-opengraph/opengraph"
"github.com/dyatlov/go-opengraph/opengraph/types/image"
)
const (
LinkMetadataTypeImage LinkMetadataType = "image"
LinkMetadataTypeNone LinkMetadataType = "none"
LinkMetadataTypeOpengraph LinkMetadataType = "opengraph"
LinkMetadataMaxImages int = 5
)
type LinkMetadataType string
// LinkMetadata stores arbitrary data about a link posted in a message. This includes dimensions of linked images
// and OpenGraph metadata.
type LinkMetadata struct {
// Hash is a value computed from the URL and Timestamp for use as a primary key in the database.
Hash int64
URL string
Timestamp int64
Type LinkMetadataType
// Data is the actual metadata for the link. It should contain data of one of the following types:
// - *model.PostImage if the linked content is an image
// - *opengraph.OpenGraph if the linked content is an HTML document
// - nil if the linked content has no metadata
Data any
}
// truncateText ensure string is 300 chars, truncate and add ellipsis
// if it was bigger.
func truncateText(original string) string {
if utf8.RuneCountInString(original) > 300 {
return fmt.Sprintf("%.300s[...]", original)
}
return original
}
func firstNImages(images []*image.Image, maxImages int) []*image.Image {
if maxImages < 0 { // don't break stuff, if it's weird, go for sane defaults
maxImages = LinkMetadataMaxImages
}
numImages := len(images)
if numImages > maxImages {
return images[0:maxImages]
}
return images
}
// TruncateOpenGraph ensure OpenGraph metadata doesn't grow too big by
// shortening strings, trimming fields and reducing the number of
// images.
func TruncateOpenGraph(ogdata *opengraph.OpenGraph) *opengraph.OpenGraph {
if ogdata != nil {
empty := &opengraph.OpenGraph{}
ogdata.Title = truncateText(ogdata.Title)
ogdata.Description = truncateText(ogdata.Description)
ogdata.SiteName = truncateText(ogdata.SiteName)
ogdata.Article = empty.Article
ogdata.Book = empty.Book
ogdata.Profile = empty.Profile
ogdata.Determiner = empty.Determiner
ogdata.Locale = empty.Locale
ogdata.LocalesAlternate = empty.LocalesAlternate
ogdata.Images = firstNImages(ogdata.Images, LinkMetadataMaxImages)
ogdata.Audios = empty.Audios
ogdata.Videos = empty.Videos
}
return ogdata
}
func (o *LinkMetadata) PreSave() {
o.Hash = GenerateLinkMetadataHash(o.URL, o.Timestamp)
}
func (o *LinkMetadata) IsValid() *AppError {
if o.URL == "" {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.url.app_error", nil, "", http.StatusBadRequest)
}
if o.Timestamp == 0 || !isRoundedToNearestHour(o.Timestamp) {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.timestamp.app_error", nil, "", http.StatusBadRequest)
}
switch o.Type {
case LinkMetadataTypeImage:
if o.Data == nil {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest)
}
if _, ok := o.Data.(*PostImage); !ok {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
}
case LinkMetadataTypeNone:
if o.Data != nil {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
}
case LinkMetadataTypeOpengraph:
if o.Data == nil {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest)
}
if _, ok := o.Data.(*opengraph.OpenGraph); !ok {
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
}
default:
return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.type.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
// DeserializeDataToConcreteType converts o.Data from JSON into properly structured data. This is intended to be used
// after getting a LinkMetadata object that has been stored in the database.
func (o *LinkMetadata) DeserializeDataToConcreteType() error {
var b []byte
switch t := o.Data.(type) {
case []byte:
// MySQL uses a byte slice for JSON
b = t
case string:
// Postgres uses a string for JSON
b = []byte(t)
}
if b == nil {
// Data doesn't need to be fixed
return nil
}
var data any
var err error
switch o.Type {
case LinkMetadataTypeImage:
image := &PostImage{}
err = json.Unmarshal(b, &image)
data = image
case LinkMetadataTypeOpengraph:
og := &opengraph.OpenGraph{}
json.Unmarshal(b, &og)
data = og
}
if err != nil {
return err
}
o.Data = data
return nil
}
// FloorToNearestHour takes a timestamp (in milliseconds) and returns it rounded to the previous hour in UTC.
func FloorToNearestHour(ms int64) int64 {
t := time.Unix(0, ms*int64(1000*1000)).UTC()
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, time.UTC).UnixNano() / int64(time.Millisecond)
}
// isRoundedToNearestHour returns true if the given timestamp (in milliseconds) has been rounded to the nearest hour in UTC.
func isRoundedToNearestHour(ms int64) bool {
return FloorToNearestHour(ms) == ms
}
// GenerateLinkMetadataHash generates a unique hash for a given URL and timestamp for use as a database key.
func GenerateLinkMetadataHash(url string, timestamp int64) int64 {
hash := fnv.New32()
// Note that we ignore write errors here because the Hash interface says that its Write will never return an error
binary.Write(hash, binary.LittleEndian, timestamp)
hash.Write([]byte(url))
return int64(hash.Sum32())
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/blang/semver"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
type PluginOption struct {
// The display name for the option.
DisplayName string `json:"display_name" yaml:"display_name"`
// The string value for the option.
Value string `json:"value" yaml:"value"`
}
type PluginSettingType int
const (
Bool PluginSettingType = iota
Dropdown
Generated
Radio
Text
LongText
Number
Username
Custom
)
type PluginSetting struct {
// The key that the setting will be assigned to in the configuration file.
Key string `json:"key" yaml:"key"`
// The display name for the setting.
DisplayName string `json:"display_name" yaml:"display_name"`
// The type of the setting.
//
// "bool" will result in a boolean true or false setting.
//
// "dropdown" will result in a string setting that allows the user to select from a list of
// pre-defined options.
//
// "generated" will result in a string setting that is set to a random, cryptographically secure
// string.
//
// "radio" will result in a string setting that allows the user to select from a short selection
// of pre-defined options.
//
// "text" will result in a string setting that can be typed in manually.
//
// "longtext" will result in a multi line string that can be typed in manually.
//
// "number" will result in in integer setting that can be typed in manually.
//
// "username" will result in a text setting that will autocomplete to a username.
//
// "custom" will result in a custom defined setting and will load the custom component registered for the Web App System Console.
Type string `json:"type" yaml:"type"`
// The help text to display to the user. Supports Markdown formatting.
HelpText string `json:"help_text" yaml:"help_text"`
// The help text to display alongside the "Regenerate" button for settings of the "generated" type.
RegenerateHelpText string `json:"regenerate_help_text,omitempty" yaml:"regenerate_help_text,omitempty"`
// The placeholder to display for "generated", "text", "longtext", "number" and "username" types when blank.
Placeholder string `json:"placeholder" yaml:"placeholder"`
// The default value of the setting.
Default any `json:"default" yaml:"default"`
// For "radio" or "dropdown" settings, this is the list of pre-defined options that the user can choose
// from.
Options []*PluginOption `json:"options,omitempty" yaml:"options,omitempty"`
// The intended hosting environment for this plugin setting. Can be "cloud" or "on-prem". When this field is set,
// and the opposite environment is running the plugin, the setting will be hidden in the admin console UI.
// Note that this functionality is entirely client-side, so the plugin needs to handle the case of invalid submissions.
Hosting string `json:"hosting"`
}
type PluginSettingsSchema struct {
// Optional text to display above the settings. Supports Markdown formatting.
Header string `json:"header" yaml:"header"`
// Optional text to display below the settings. Supports Markdown formatting.
Footer string `json:"footer" yaml:"footer"`
// A list of setting definitions.
Settings []*PluginSetting `json:"settings" yaml:"settings"`
}
// The plugin manifest defines the metadata required to load and present your plugin. The manifest
// file should be named plugin.json or plugin.yaml and placed in the top of your
// plugin bundle.
//
// Example plugin.json:
//
// {
// "id": "com.mycompany.myplugin",
// "name": "My Plugin",
// "description": "This is my plugin",
// "homepage_url": "https://example.com",
// "support_url": "https://example.com/support",
// "release_notes_url": "https://example.com/releases/v0.0.1",
// "icon_path": "assets/logo.svg",
// "version": "0.1.0",
// "min_server_version": "5.6.0",
// "server": {
// "executables": {
// "linux-amd64": "server/dist/plugin-linux-amd64",
// "darwin-amd64": "server/dist/plugin-darwin-amd64",
// "windows-amd64": "server/dist/plugin-windows-amd64.exe"
// }
// },
// "webapp": {
// "bundle_path": "webapp/dist/main.js"
// },
// "settings_schema": {
// "header": "Some header text",
// "footer": "Some footer text",
// "settings": [{
// "key": "someKey",
// "display_name": "Enable Extra Feature",
// "type": "bool",
// "help_text": "When true, an extra feature will be enabled!",
// "default": "false"
// }]
// },
// "props": {
// "someKey": "someData"
// }
// }
type Manifest struct {
// The id is a globally unique identifier that represents your plugin. Ids must be at least
// 3 characters, at most 190 characters and must match ^[a-zA-Z0-9-_\.]+$.
// Reverse-DNS notation using a name you control is a good option, e.g. "com.mycompany.myplugin".
Id string `json:"id" yaml:"id"`
// The name to be displayed for the plugin.
Name string `json:"name" yaml:"name"`
// A description of what your plugin is and does.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
// HomepageURL is an optional link to learn more about the plugin.
HomepageURL string `json:"homepage_url,omitempty" yaml:"homepage_url,omitempty"`
// SupportURL is an optional URL where plugin issues can be reported.
SupportURL string `json:"support_url,omitempty" yaml:"support_url,omitempty"`
// ReleaseNotesURL is an optional URL where a changelog for the release can be found.
ReleaseNotesURL string `json:"release_notes_url,omitempty" yaml:"release_notes_url,omitempty"`
// A relative file path in the bundle that points to the plugins svg icon for use with the Plugin Marketplace.
// This should be relative to the root of your bundle and the location of the manifest file. Bitmap image formats are not supported.
IconPath string `json:"icon_path,omitempty" yaml:"icon_path,omitempty"`
// A version number for your plugin. Semantic versioning is recommended: http://semver.org
Version string `json:"version" yaml:"version"`
// The minimum Mattermost server version required for your plugin.
//
// Minimum server version: 5.6
MinServerVersion string `json:"min_server_version,omitempty" yaml:"min_server_version,omitempty"`
// Server defines the server-side portion of your plugin.
Server *ManifestServer `json:"server,omitempty" yaml:"server,omitempty"`
// If your plugin extends the web app, you'll need to define webapp.
Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"`
// To allow administrators to configure your plugin via the Mattermost system console, you can
// provide your settings schema.
SettingsSchema *PluginSettingsSchema `json:"settings_schema,omitempty" yaml:"settings_schema,omitempty"`
// Plugins can store any kind of data in Props to allow other plugins to use it.
Props map[string]any `json:"props,omitempty" yaml:"props,omitempty"`
// RequiredConfig defines any required server configuration fields for the plugin to function properly.
//
// Use the pluginapi.Configuration.CheckRequiredServerConfiguration method to enforce this.
RequiredConfig *Config `json:"required_configuration,omitempty" yaml:"required_configuration,omitempty"`
}
type ManifestServer struct {
// Executables are the paths to your executable binaries, specifying multiple entry
// points for different platforms when bundled together in a single plugin.
Executables map[string]string `json:"executables,omitempty" yaml:"executables,omitempty"`
// Executable is the path to your executable binary. This should be relative to the root
// of your bundle and the location of the manifest file.
//
// On Windows, this file must have a ".exe" extension.
//
// If your plugin is compiled for multiple platforms, consider bundling them together
// and using the Executables field instead.
Executable string `json:"executable" yaml:"executable"`
}
// Deprecated: ManifestExecutables is a legacy structure capturing a subset of the known platform executables.
// It will be remove in v7.0: https://mattermost.atlassian.net/browse/MM-40531
type ManifestExecutables struct {
// LinuxAmd64 is the path to your executable binary for the corresponding platform
LinuxAmd64 string `json:"linux-amd64,omitempty" yaml:"linux-amd64,omitempty"`
// DarwinAmd64 is the path to your executable binary for the corresponding platform
DarwinAmd64 string `json:"darwin-amd64,omitempty" yaml:"darwin-amd64,omitempty"`
// WindowsAmd64 is the path to your executable binary for the corresponding platform
// This file must have a ".exe" extension
WindowsAmd64 string `json:"windows-amd64,omitempty" yaml:"windows-amd64,omitempty"`
}
type ManifestWebapp struct {
// The path to your webapp bundle. This should be relative to the root of your bundle and the
// location of the manifest file.
BundlePath string `json:"bundle_path" yaml:"bundle_path"`
// BundleHash is the 64-bit FNV-1a hash of the webapp bundle, computed when the plugin is loaded
BundleHash []byte `json:"-"`
}
func (m *Manifest) HasClient() bool {
return m.Webapp != nil
}
func (m *Manifest) ClientManifest() *Manifest {
cm := new(Manifest)
*cm = *m
cm.Name = ""
cm.Description = ""
cm.Server = nil
if cm.Webapp != nil {
cm.Webapp = new(ManifestWebapp)
*cm.Webapp = *m.Webapp
cm.Webapp.BundlePath = "/static/" + m.Id + "/" + fmt.Sprintf("%s_%x_bundle.js", m.Id, m.Webapp.BundleHash)
}
return cm
}
// GetExecutableForRuntime returns the path to the executable for the given runtime architecture.
//
// If the manifest defines multiple executables, but none match, or if only a single executable
// is defined, the Executable field will be returned. This method does not guarantee that the
// resulting binary can actually execute on the given platform.
func (m *Manifest) GetExecutableForRuntime(goOs, goArch string) string {
server := m.Server
if server == nil {
return ""
}
var executable string
if len(server.Executables) > 0 {
osArch := fmt.Sprintf("%s-%s", goOs, goArch)
executable = server.Executables[osArch]
}
if executable == "" {
executable = server.Executable
}
return executable
}
func (m *Manifest) HasServer() bool {
return m.Server != nil
}
func (m *Manifest) HasWebapp() bool {
return m.Webapp != nil
}
func (m *Manifest) MeetMinServerVersion(serverVersion string) (bool, error) {
minServerVersion, err := semver.Parse(m.MinServerVersion)
if err != nil {
return false, errors.New("failed to parse MinServerVersion")
}
sv := semver.MustParse(serverVersion)
if sv.LT(minServerVersion) {
return false, nil
}
return true, nil
}
func (m *Manifest) IsValid() error {
if !IsValidPluginId(m.Id) {
return errors.New("invalid plugin ID")
}
if strings.TrimSpace(m.Name) == "" {
return errors.New("a plugin name is needed")
}
if m.HomepageURL != "" && !IsValidHTTPURL(m.HomepageURL) {
return errors.New("invalid HomepageURL")
}
if m.SupportURL != "" && !IsValidHTTPURL(m.SupportURL) {
return errors.New("invalid SupportURL")
}
if m.ReleaseNotesURL != "" && !IsValidHTTPURL(m.ReleaseNotesURL) {
return errors.New("invalid ReleaseNotesURL")
}
if m.Version != "" {
_, err := semver.Parse(m.Version)
if err != nil {
return errors.Wrap(err, "failed to parse Version")
}
}
if m.MinServerVersion != "" {
_, err := semver.Parse(m.MinServerVersion)
if err != nil {
return errors.Wrap(err, "failed to parse MinServerVersion")
}
}
if m.SettingsSchema != nil {
err := m.SettingsSchema.isValid()
if err != nil {
return errors.Wrap(err, "invalid settings schema")
}
}
return nil
}
func (s *PluginSettingsSchema) isValid() error {
for _, setting := range s.Settings {
err := setting.isValid()
if err != nil {
return err
}
}
return nil
}
func (s *PluginSetting) isValid() error {
pluginSettingType, err := convertTypeToPluginSettingType(s.Type)
if err != nil {
return err
}
if s.RegenerateHelpText != "" && pluginSettingType != Generated {
return errors.New("should not set RegenerateHelpText for setting type that is not generated")
}
if s.Placeholder != "" && !(pluginSettingType == Generated ||
pluginSettingType == Text ||
pluginSettingType == LongText ||
pluginSettingType == Number ||
pluginSettingType == Username ||
pluginSettingType == Custom) {
return errors.New("should not set Placeholder for setting type not in text, generated, number, username, or custom")
}
if s.Options != nil {
if pluginSettingType != Radio && pluginSettingType != Dropdown {
return errors.New("should not set Options for setting type not in radio or dropdown")
}
for _, option := range s.Options {
if option.DisplayName == "" || option.Value == "" {
return errors.New("should not have empty Displayname or Value for any option")
}
}
}
return nil
}
func convertTypeToPluginSettingType(t string) (PluginSettingType, error) {
var settingType PluginSettingType
switch t {
case "bool":
return Bool, nil
case "dropdown":
return Dropdown, nil
case "generated":
return Generated, nil
case "radio":
return Radio, nil
case "text":
return Text, nil
case "number":
return Number, nil
case "longtext":
return LongText, nil
case "username":
return Username, nil
case "custom":
return Custom, nil
default:
return settingType, errors.New("invalid setting type: " + t)
}
}
// FindManifest will find and parse the manifest in a given directory.
//
// In all cases other than a does-not-exist error, path is set to the path of the manifest file that was
// found.
//
// Manifests are JSON or YAML files named plugin.json, plugin.yaml, or plugin.yml.
func FindManifest(dir string) (manifest *Manifest, path string, err error) {
for _, name := range []string{"plugin.yml", "plugin.yaml"} {
path = filepath.Join(dir, name)
f, ferr := os.Open(path)
if ferr != nil {
if !os.IsNotExist(ferr) {
return nil, "", ferr
}
continue
}
b, ioerr := io.ReadAll(f)
f.Close()
if ioerr != nil {
return nil, path, ioerr
}
var parsed Manifest
err = yaml.Unmarshal(b, &parsed)
if err != nil {
return nil, path, err
}
manifest = &parsed
manifest.Id = strings.ToLower(manifest.Id)
return manifest, path, nil
}
path = filepath.Join(dir, "plugin.json")
f, ferr := os.Open(path)
if ferr != nil {
if os.IsNotExist(ferr) {
path = ""
}
return nil, path, ferr
}
defer f.Close()
var parsed Manifest
err = json.NewDecoder(f).Decode(&parsed)
if err != nil {
return nil, path, err
}
manifest = &parsed
manifest.Id = strings.ToLower(manifest.Id)
return manifest, path, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"net/url"
"strconv"
"github.com/pkg/errors"
)
// BaseMarketplacePlugin is a Mattermost plugin received from the Marketplace server.
type BaseMarketplacePlugin struct {
HomepageURL string `json:"homepage_url"`
IconData string `json:"icon_data"`
DownloadURL string `json:"download_url"`
ReleaseNotesURL string `json:"release_notes_url"`
Labels []MarketplaceLabel `json:"labels,omitempty"`
Hosting string `json:"hosting"` // Indicated if the plugin is limited to a certain hosting type
AuthorType string `json:"author_type"` // The maintainer of the plugin
ReleaseStage string `json:"release_stage"` // The stage in the software release cycle that the plugin is in
Enterprise bool `json:"enterprise"` // Indicated if the plugin is an enterprise plugin
Signature string `json:"signature"` // Signature represents a signature of a plugin saved in base64 encoding.
Manifest *Manifest `json:"manifest"`
}
// MarketplaceLabel represents a label shown in the Marketplace UI.
type MarketplaceLabel struct {
Name string `json:"name"`
Description string `json:"description"`
URL string `json:"url"`
Color string `json:"color"`
}
// MarketplacePlugin is a state aware Marketplace plugin.
type MarketplacePlugin struct {
*BaseMarketplacePlugin
InstalledVersion string `json:"installed_version"`
}
// BaseMarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader.
func BaseMarketplacePluginsFromReader(reader io.Reader) ([]*BaseMarketplacePlugin, error) {
plugins := []*BaseMarketplacePlugin{}
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&plugins); err != nil && err != io.EOF {
return nil, err
}
return plugins, nil
}
// MarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader.
func MarketplacePluginsFromReader(reader io.Reader) ([]*MarketplacePlugin, error) {
plugins := []*MarketplacePlugin{}
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&plugins); err != nil && err != io.EOF {
return nil, err
}
return plugins, nil
}
// DecodeSignature Decodes signature and returns ReadSeeker.
func (plugin *BaseMarketplacePlugin) DecodeSignature() (io.ReadSeeker, error) {
signatureBytes, err := base64.StdEncoding.DecodeString(plugin.Signature)
if err != nil {
return nil, errors.Wrap(err, "Unable to decode base64 signature.")
}
return bytes.NewReader(signatureBytes), nil
}
// MarketplacePluginFilter describes the parameters to request a list of plugins.
type MarketplacePluginFilter struct {
Page int
PerPage int
Filter string
ServerVersion string
BuildEnterpriseReady bool
EnterprisePlugins bool
Cloud bool
LocalOnly bool
Platform string
PluginId string
ReturnAllVersions bool
RemoteOnly bool
}
// ApplyToURL modifies the given url to include query string parameters for the request.
func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) {
q := u.Query()
q.Add("page", strconv.Itoa(filter.Page))
if filter.PerPage > 0 {
q.Add("per_page", strconv.Itoa(filter.PerPage))
}
q.Add("filter", filter.Filter)
q.Add("server_version", filter.ServerVersion)
q.Add("build_enterprise_ready", strconv.FormatBool(filter.BuildEnterpriseReady))
q.Add("enterprise_plugins", strconv.FormatBool(filter.EnterprisePlugins))
q.Add("cloud", strconv.FormatBool(filter.Cloud))
q.Add("local_only", strconv.FormatBool(filter.LocalOnly))
q.Add("remote_only", strconv.FormatBool(filter.RemoteOnly))
q.Add("platform", filter.Platform)
q.Add("plugin_id", filter.PluginId)
q.Add("return_all_versions", strconv.FormatBool(filter.ReturnAllVersions))
u.RawQuery = q.Encode()
}
// InstallMarketplacePluginRequest struct describes parameters of the requested plugin.
type InstallMarketplacePluginRequest struct {
Id string `json:"id"`
Version string `json:"version"`
}
// PluginRequestFromReader decodes a json-encoded plugin request from the given io.Reader.
func PluginRequestFromReader(reader io.Reader) (*InstallMarketplacePluginRequest, error) {
var r *InstallMarketplacePluginRequest
err := json.NewDecoder(reader).Decode(&r)
if err != nil {
return nil, err
}
return r, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"net/http"
)
type MemberInvite struct {
Emails []string `json:"emails"`
ChannelIds []string `json:"channelIds,omitempty"`
Message string `json:"message"`
}
func (i *MemberInvite) Auditable() map[string]interface{} {
return map[string]interface{}{
"emails": i.Emails,
"channel_ids": i.ChannelIds,
}
}
// IsValid validates that the invitation info is loaded correctly and with the correct structure
func (i *MemberInvite) IsValid() *AppError {
if len(i.Emails) == 0 {
return NewAppError("MemberInvite.IsValid", "model.member.is_valid.emails.app_error", nil, "", http.StatusBadRequest)
}
if len(i.ChannelIds) > 0 {
for _, channel := range i.ChannelIds {
if len(channel) != 26 {
return NewAppError("MemberInvite.IsValid", "model.member.is_valid.channel.app_error", nil, "channel="+channel, http.StatusBadRequest)
}
}
}
return nil
}
func (i *MemberInvite) UnmarshalJSON(b []byte) error {
var emails []string
if err := json.Unmarshal(b, &emails); err == nil {
*i = MemberInvite{}
i.Emails = emails
return nil
}
type TempMemberInvite MemberInvite
var o2 TempMemberInvite
if err := json.Unmarshal(b, &o2); err != nil {
return err
}
*i = MemberInvite(o2)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/url"
)
type UserMentionMap map[string]string
type ChannelMentionMap map[string]string
const (
userMentionsKey = "user_mentions"
userMentionsIdsKey = "user_mentions_ids"
channelMentionsKey = "channel_mentions"
channelMentionsIdsKey = "channel_mentions_ids"
)
func UserMentionMapFromURLValues(values url.Values) (UserMentionMap, error) {
return mentionsFromURLValues(values, userMentionsKey, userMentionsIdsKey)
}
func (m UserMentionMap) ToURLValues() url.Values {
return mentionsToURLValues(m, userMentionsKey, userMentionsIdsKey)
}
func ChannelMentionMapFromURLValues(values url.Values) (ChannelMentionMap, error) {
return mentionsFromURLValues(values, channelMentionsKey, channelMentionsIdsKey)
}
func (m ChannelMentionMap) ToURLValues() url.Values {
return mentionsToURLValues(m, channelMentionsKey, channelMentionsIdsKey)
}
func mentionsFromURLValues(values url.Values, mentionKey, idKey string) (map[string]string, error) {
mentions, mentionsOk := values[mentionKey]
ids, idsOk := values[idKey]
if !mentionsOk && !idsOk {
return map[string]string{}, nil
}
if !mentionsOk {
return nil, fmt.Errorf("%s key not found", mentionKey)
}
if !idsOk {
return nil, fmt.Errorf("%s key not found", idKey)
}
if len(mentions) != len(ids) {
return nil, fmt.Errorf("keys %s and %s have different length", mentionKey, idKey)
}
mentionsMap := make(map[string]string)
for i, mention := range mentions {
id := ids[i]
if oldId, ok := mentionsMap[mention]; ok && oldId != id {
return nil, fmt.Errorf("key %s has two different values: %s and %s", mention, oldId, id)
}
mentionsMap[mention] = id
}
return mentionsMap, nil
}
func mentionsToURLValues(mentions map[string]string, mentionKey, idKey string) url.Values {
values := url.Values{}
for mention, id := range mentions {
values.Add(mentionKey, mention)
values.Add(idKey, id)
}
return values
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "encoding/json"
type MessageExport struct {
TeamId *string
TeamName *string
TeamDisplayName *string
ChannelId *string
ChannelName *string
ChannelDisplayName *string
ChannelType *ChannelType
UserId *string
UserEmail *string
Username *string
IsBot bool
PostId *string
PostCreateAt *int64
PostUpdateAt *int64
PostDeleteAt *int64
PostMessage *string
PostType *string
PostRootId *string
PostProps *string
PostOriginalId *string
PostFileIds StringArray
}
type MessageExportCursor struct {
LastPostUpdateAt int64
LastPostId string
}
// PreviewID returns the value of the post's previewed_post prop, if present, or an empty string.
func (m *MessageExport) PreviewID() string {
var previewID string
props := map[string]any{}
if m.PostProps != nil && json.Unmarshal([]byte(*m.PostProps), &props) == nil {
if val, ok := props[PostPropsPreviewedPost]; ok {
previewID = val.(string)
}
}
return previewID
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"database/sql"
"fmt"
"net/http"
"strings"
)
type MattermostFeature string
const (
PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
PluginFeature = MattermostFeature("mattermost.feature.plugin")
)
var validSKUs map[string]struct{} = map[string]struct{}{
LicenseShortSkuProfessional: {},
LicenseShortSkuEnterprise: {},
}
// These are the features a non admin would typically ping an admin about
var paidFeatures map[MattermostFeature]struct{} = map[MattermostFeature]struct{}{
PaidFeatureGuestAccounts: {},
PaidFeatureCustomUsergroups: {},
PaidFeatureCreateMultipleTeams: {},
PaidFeatureStartcall: {},
PaidFeaturePlaybooksRetrospective: {},
PaidFeatureUnlimitedMessages: {},
PaidFeatureUnlimitedFileStorage: {},
PaidFeatureAllProfessionalfeatures: {},
PaidFeatureAllEnterprisefeatures: {},
UpgradeDowngradedWorkspace: {},
}
type NotifyAdminToUpgradeRequest struct {
TrialNotification bool `json:"trial_notification"`
RequiredPlan string `json:"required_plan"`
RequiredFeature MattermostFeature `json:"required_feature"`
}
type NotifyAdminData struct {
CreateAt int64 `json:"create_at,omitempty"`
UserId string `json:"user_id"`
RequiredPlan string `json:"required_plan"`
RequiredFeature MattermostFeature `json:"required_feature"`
Trial bool `json:"trial"`
SentAt sql.NullInt64 `json:"sent_at"`
}
func (nad *NotifyAdminData) IsValid() *AppError {
if strings.HasPrefix(string(nad.RequiredFeature), string(PluginFeature)) {
return nil
}
if _, planOk := validSKUs[nad.RequiredPlan]; !planOk {
return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid plan, %s provided", nad.RequiredPlan), nil, "", http.StatusBadRequest)
}
if _, featureOk := paidFeatures[nad.RequiredFeature]; !featureOk {
return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid feature, %s provided", nad.RequiredFeature), nil, "", http.StatusBadRequest)
}
return nil
}
func (nad *NotifyAdminData) PreSave() {
nad.CreateAt = GetMillis()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"unicode/utf8"
)
const (
OAuthActionSignup = "signup"
OAuthActionLogin = "login"
OAuthActionEmailToSSO = "email_to_sso"
OAuthActionSSOToEmail = "sso_to_email"
OAuthActionMobile = "mobile"
)
type OAuthApp struct {
Id string `json:"id"`
CreatorId string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
ClientSecret string `json:"client_secret"`
Name string `json:"name"`
Description string `json:"description"`
IconURL string `json:"icon_url"`
CallbackUrls StringArray `json:"callback_urls"`
Homepage string `json:"homepage"`
IsTrusted bool `json:"is_trusted"`
MattermostAppID string `json:"mattermost_app_id"`
}
func (a *OAuthApp) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": a.Id,
"creator_id": a.CreatorId,
"create_at": a.CreateAt,
"update_at": a.UpdateAt,
"name": a.Name,
"description": a.Description,
"icon_url": a.IconURL,
"callback_urls:": a.CallbackUrls,
"homepage": a.Homepage,
"is_trusted": a.IsTrusted,
"mattermost_app_id": a.MattermostAppID,
}
}
// IsValid validates the app and returns an error if it isn't configured
// correctly.
func (a *OAuthApp) IsValid() *AppError {
if !IsValidId(a.Id) {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.app_id.app_error", nil, "", http.StatusBadRequest)
}
if a.CreateAt == 0 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.create_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if a.UpdateAt == 0 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.update_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if !IsValidId(a.CreatorId) {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.creator_id.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if a.ClientSecret == "" || len(a.ClientSecret) > 128 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if a.Name == "" || len(a.Name) > 64 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.name.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
for _, callback := range a.CallbackUrls {
if !IsValidHTTPURL(callback) {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "", http.StatusBadRequest)
}
}
if a.Homepage == "" || len(a.Homepage) > 256 || !IsValidHTTPURL(a.Homepage) {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(a.Description) > 512 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
if a.IconURL != "" {
if len(a.IconURL) > 512 || !IsValidHTTPURL(a.IconURL) {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
}
if len(a.MattermostAppID) > 32 {
return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.mattermost_app_id.app_error", nil, "app_id="+a.Id, http.StatusBadRequest)
}
return nil
}
// PreSave will set the Id and ClientSecret if missing. It will also fill
// in the CreateAt, UpdateAt times. It should be run before saving the app to the db.
func (a *OAuthApp) PreSave() {
if a.Id == "" {
a.Id = NewId()
}
if a.ClientSecret == "" {
a.ClientSecret = NewId()
}
a.CreateAt = GetMillis()
a.UpdateAt = a.CreateAt
}
// PreUpdate should be run before updating the app in the db.
func (a *OAuthApp) PreUpdate() {
a.UpdateAt = GetMillis()
}
// Generate a valid strong etag so the browser can cache the results
func (a *OAuthApp) Etag() string {
return Etag(a.Id, a.UpdateAt)
}
// Remove any private data from the app object
func (a *OAuthApp) Sanitize() {
a.ClientSecret = ""
}
func (a *OAuthApp) IsValidRedirectURL(url string) bool {
for _, u := range a.CallbackUrls {
if u == url {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package oauthgitlab
import (
"encoding/json"
"errors"
"io"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
type GitLabProvider struct {
}
type GitLabUser struct {
Id int64 `json:"id"`
Username string `json:"username"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
}
func init() {
provider := &GitLabProvider{}
einterfaces.RegisterOAuthProvider(model.UserAuthServiceGitlab, provider)
}
func userFromGitLabUser(glu *GitLabUser) *model.User {
user := &model.User{}
username := glu.Username
if username == "" {
username = glu.Login
}
user.Username = model.CleanUsername(username)
splitName := strings.Split(glu.Name, " ")
if len(splitName) == 2 {
user.FirstName = splitName[0]
user.LastName = splitName[1]
} else if len(splitName) >= 2 {
user.FirstName = splitName[0]
user.LastName = strings.Join(splitName[1:], " ")
} else {
user.FirstName = glu.Name
}
user.Email = glu.Email
user.Email = strings.ToLower(user.Email)
userId := glu.getAuthData()
user.AuthData = &userId
user.AuthService = model.UserAuthServiceGitlab
return user
}
func gitLabUserFromJSON(data io.Reader) (*GitLabUser, error) {
decoder := json.NewDecoder(data)
var glu GitLabUser
err := decoder.Decode(&glu)
if err != nil {
return nil, err
}
return &glu, nil
}
func (glu *GitLabUser) IsValid() error {
if glu.Id == 0 {
return errors.New("user id can't be 0")
}
if glu.Email == "" {
return errors.New("user e-mail should not be empty")
}
return nil
}
func (glu *GitLabUser) getAuthData() string {
return strconv.FormatInt(glu.Id, 10)
}
func (m *GitLabProvider) GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error) {
glu, err := gitLabUserFromJSON(data)
if err != nil {
return nil, err
}
if err = glu.IsValid(); err != nil {
return nil, err
}
return userFromGitLabUser(glu), nil
}
func (m *GitLabProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) {
return &config.GitLabSettings, nil
}
func (m *GitLabProvider) GetUserFromIdToken(idToken string) (*model.User, error) {
return nil, nil
}
func (m *GitLabProvider) IsSameUser(dbUser, oauthUser *model.User) bool {
return dbUser.AuthData == oauthUser.AuthData
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
// CompleteOnboardingRequest describes parameters of the requested plugin.
type CompleteOnboardingRequest struct {
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
}
func (r *CompleteOnboardingRequest) Auditable() map[string]interface{} {
return map[string]interface{}{
"install_plugins": r.InstallPlugins,
}
}
// CompleteOnboardingRequest decodes a json-encoded request from the given io.Reader.
func CompleteOnboardingRequestFromReader(reader io.Reader) (*CompleteOnboardingRequest, error) {
var r *CompleteOnboardingRequest
err := json.NewDecoder(reader).Decode(&r)
if err != nil {
return nil, err
}
return r, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
)
type OutgoingWebhook struct {
Id string `json:"id"`
Token string `json:"token"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
CreatorId string `json:"creator_id"`
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
TriggerWords StringArray `json:"trigger_words"`
TriggerWhen int `json:"trigger_when"`
CallbackURLs StringArray `json:"callback_urls"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ContentType string `json:"content_type"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
}
func (o *OutgoingWebhook) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
"creator_id": o.CreatorId,
"channel_id": o.ChannelId,
"team_id": o.TeamId,
"trigger_words": o.TriggerWords,
"trigger_when": o.TriggerWhen,
"callback_urls": o.CallbackURLs,
"display_name": o.DisplayName,
"description": o.Description,
"content_type": o.ContentType,
"username": o.Username,
"icon_url": o.IconURL,
}
}
type OutgoingWebhookPayload struct {
Token string `json:"token"`
TeamId string `json:"team_id"`
TeamDomain string `json:"team_domain"`
ChannelId string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Timestamp int64 `json:"timestamp"`
UserId string `json:"user_id"`
UserName string `json:"user_name"`
PostId string `json:"post_id"`
Text string `json:"text"`
TriggerWord string `json:"trigger_word"`
FileIds string `json:"file_ids"`
}
type OutgoingWebhookResponse struct {
Text *string `json:"text"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
Props StringInterface `json:"props"`
Attachments []*SlackAttachment `json:"attachments"`
Type string `json:"type"`
ResponseType string `json:"response_type"`
}
const OutgoingHookResponseTypeComment = "comment"
func (o *OutgoingWebhookPayload) ToFormValues() string {
v := url.Values{}
v.Set("token", o.Token)
v.Set("team_id", o.TeamId)
v.Set("team_domain", o.TeamDomain)
v.Set("channel_id", o.ChannelId)
v.Set("channel_name", o.ChannelName)
v.Set("timestamp", strconv.FormatInt(o.Timestamp/1000, 10))
v.Set("user_id", o.UserId)
v.Set("user_name", o.UserName)
v.Set("post_id", o.PostId)
v.Set("text", o.Text)
v.Set("trigger_word", o.TriggerWord)
v.Set("file_ids", o.FileIds)
return v.Encode()
}
func (o *OutgoingWebhook) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Token) != 26 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.CreatorId) {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if o.ChannelId != "" && !IsValidId(o.ChannelId) {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.TeamId) {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "", http.StatusBadRequest)
}
if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "", http.StatusBadRequest)
}
if len(o.TriggerWords) != 0 {
for _, triggerWord := range o.TriggerWords {
if triggerWord == "" {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "", http.StatusBadRequest)
}
}
}
if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "", http.StatusBadRequest)
}
for _, callback := range o.CallbackURLs {
if !IsValidHTTPURL(callback) {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "", http.StatusBadRequest)
}
}
if len(o.DisplayName) > 64 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Description) > 500 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest)
}
if len(o.ContentType) > 128 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest)
}
if o.TriggerWhen > 1 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Username) > 64 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.username.app_error", nil, "", http.StatusBadRequest)
}
if len(o.IconURL) > 1024 {
return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.icon_url.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (o *OutgoingWebhook) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
if o.Token == "" {
o.Token = NewId()
}
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
}
func (o *OutgoingWebhook) PreUpdate() {
o.UpdateAt = GetMillis()
}
func (o *OutgoingWebhook) TriggerWordExactMatch(word string) bool {
if word == "" {
return false
}
for _, trigger := range o.TriggerWords {
if trigger == word {
return true
}
}
return false
}
func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
if word == "" {
return false
}
for _, trigger := range o.TriggerWords {
if strings.HasPrefix(word, trigger) {
return true
}
}
return false
}
func (o *OutgoingWebhook) GetTriggerWord(word string, isExactMatch bool) (triggerWord string) {
if word == "" {
return
}
if isExactMatch {
for _, trigger := range o.TriggerWords {
if trigger == word {
triggerWord = trigger
break
}
}
} else {
for _, trigger := range o.TriggerWords {
if strings.HasPrefix(word, trigger) {
triggerWord = trigger
break
}
}
}
return triggerWord
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type Permalink struct {
PreviewPost *PreviewPost `json:"preview_post"`
}
type PreviewPost struct {
PostID string `json:"post_id"`
Post *Post `json:"post"`
TeamName string `json:"team_name"`
ChannelDisplayName string `json:"channel_display_name"`
ChannelType ChannelType `json:"channel_type"`
ChannelID string `json:"channel_id"`
}
func NewPreviewPost(post *Post, team *Team, channel *Channel) *PreviewPost {
if post == nil {
return nil
}
return &PreviewPost{
PostID: post.Id,
Post: post,
TeamName: team.Name,
ChannelDisplayName: channel.DisplayName,
ChannelType: channel.Type,
ChannelID: channel.Id,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
PermissionScopeSystem = "system_scope"
PermissionScopeTeam = "team_scope"
PermissionScopeChannel = "channel_scope"
PermissionScopeGroup = "group_scope"
PermissionScopePlaybook = "playbook_scope"
PermissionScopeRun = "run_scope"
)
type Permission struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Scope string `json:"scope"`
}
var PermissionInviteUser *Permission
var PermissionAddUserToTeam *Permission
var PermissionUseSlashCommands *Permission
var PermissionManageSlashCommands *Permission
var PermissionManageOthersSlashCommands *Permission
var PermissionCreatePublicChannel *Permission
var PermissionCreatePrivateChannel *Permission
var PermissionManagePublicChannelMembers *Permission
var PermissionManagePrivateChannelMembers *Permission
var PermissionConvertPublicChannelToPrivate *Permission
var PermissionConvertPrivateChannelToPublic *Permission
var PermissionAssignSystemAdminRole *Permission
var PermissionManageRoles *Permission
var PermissionManageTeamRoles *Permission
var PermissionManageChannelRoles *Permission
var PermissionCreateDirectChannel *Permission
var PermissionCreateGroupChannel *Permission
var PermissionManagePublicChannelProperties *Permission
var PermissionManagePrivateChannelProperties *Permission
var PermissionListPublicTeams *Permission
var PermissionJoinPublicTeams *Permission
var PermissionListPrivateTeams *Permission
var PermissionJoinPrivateTeams *Permission
var PermissionListTeamChannels *Permission
var PermissionJoinPublicChannels *Permission
var PermissionDeletePublicChannel *Permission
var PermissionDeletePrivateChannel *Permission
var PermissionEditOtherUsers *Permission
var PermissionReadChannel *Permission
var PermissionReadPublicChannelGroups *Permission
var PermissionReadPrivateChannelGroups *Permission
var PermissionReadPublicChannel *Permission
var PermissionAddReaction *Permission
var PermissionRemoveReaction *Permission
var PermissionRemoveOthersReactions *Permission
var PermissionPermanentDeleteUser *Permission
var PermissionUploadFile *Permission
var PermissionGetPublicLink *Permission
var PermissionManageWebhooks *Permission
var PermissionManageOthersWebhooks *Permission
var PermissionManageIncomingWebhooks *Permission
var PermissionManageOutgoingWebhooks *Permission
var PermissionManageOthersIncomingWebhooks *Permission
var PermissionManageOthersOutgoingWebhooks *Permission
var PermissionManageOAuth *Permission
var PermissionManageSystemWideOAuth *Permission
var PermissionManageEmojis *Permission
var PermissionManageOthersEmojis *Permission
var PermissionCreateEmojis *Permission
var PermissionDeleteEmojis *Permission
var PermissionDeleteOthersEmojis *Permission
var PermissionCreatePost *Permission
var PermissionCreatePostPublic *Permission
var PermissionCreatePostEphemeral *Permission
var PermissionReadDeletedPosts *Permission
var PermissionEditPost *Permission
var PermissionEditOthersPosts *Permission
var PermissionDeletePost *Permission
var PermissionDeleteOthersPosts *Permission
var PermissionRemoveUserFromTeam *Permission
var PermissionCreateTeam *Permission
var PermissionManageTeam *Permission
var PermissionImportTeam *Permission
var PermissionViewTeam *Permission
var PermissionListUsersWithoutTeam *Permission
var PermissionReadJobs *Permission
var PermissionManageJobs *Permission
var PermissionCreateUserAccessToken *Permission
var PermissionReadUserAccessToken *Permission
var PermissionRevokeUserAccessToken *Permission
var PermissionCreateBot *Permission
var PermissionAssignBot *Permission
var PermissionReadBots *Permission
var PermissionReadOthersBots *Permission
var PermissionManageBots *Permission
var PermissionManageOthersBots *Permission
var PermissionViewMembers *Permission
var PermissionInviteGuest *Permission
var PermissionPromoteGuest *Permission
var PermissionDemoteToGuest *Permission
var PermissionUseChannelMentions *Permission
var PermissionUseGroupMentions *Permission
var PermissionReadOtherUsersTeams *Permission
var PermissionEditBrand *Permission
var PermissionManageSharedChannels *Permission
var PermissionManageSecureConnections *Permission
var PermissionDownloadComplianceExportResult *Permission
var PermissionCreateDataRetentionJob *Permission
var PermissionReadDataRetentionJob *Permission
var PermissionCreateComplianceExportJob *Permission
var PermissionReadComplianceExportJob *Permission
var PermissionReadAudits *Permission
var PermissionTestElasticsearch *Permission
var PermissionTestSiteURL *Permission
var PermissionTestS3 *Permission
var PermissionReloadConfig *Permission
var PermissionInvalidateCaches *Permission
var PermissionRecycleDatabaseConnections *Permission
var PermissionPurgeElasticsearchIndexes *Permission
var PermissionTestEmail *Permission
var PermissionCreateElasticsearchPostIndexingJob *Permission
var PermissionCreateElasticsearchPostAggregationJob *Permission
var PermissionReadElasticsearchPostIndexingJob *Permission
var PermissionReadElasticsearchPostAggregationJob *Permission
var PermissionPurgeBleveIndexes *Permission
var PermissionCreatePostBleveIndexesJob *Permission
var PermissionCreateLdapSyncJob *Permission
var PermissionReadLdapSyncJob *Permission
var PermissionTestLdap *Permission
var PermissionInvalidateEmailInvite *Permission
var PermissionGetSamlMetadataFromIdp *Permission
var PermissionAddSamlPublicCert *Permission
var PermissionAddSamlPrivateCert *Permission
var PermissionAddSamlIdpCert *Permission
var PermissionRemoveSamlPublicCert *Permission
var PermissionRemoveSamlPrivateCert *Permission
var PermissionRemoveSamlIdpCert *Permission
var PermissionGetSamlCertStatus *Permission
var PermissionAddLdapPublicCert *Permission
var PermissionAddLdapPrivateCert *Permission
var PermissionRemoveLdapPublicCert *Permission
var PermissionRemoveLdapPrivateCert *Permission
var PermissionGetLogs *Permission
var PermissionGetAnalytics *Permission
var PermissionReadLicenseInformation *Permission
var PermissionManageLicenseInformation *Permission
var PermissionSysconsoleReadAbout *Permission
var PermissionSysconsoleWriteAbout *Permission
var PermissionSysconsoleReadAboutEditionAndLicense *Permission
var PermissionSysconsoleWriteAboutEditionAndLicense *Permission
var PermissionSysconsoleReadBilling *Permission
var PermissionSysconsoleWriteBilling *Permission
var PermissionSysconsoleReadReporting *Permission
var PermissionSysconsoleWriteReporting *Permission
var PermissionSysconsoleReadReportingSiteStatistics *Permission
var PermissionSysconsoleWriteReportingSiteStatistics *Permission
var PermissionSysconsoleReadReportingTeamStatistics *Permission
var PermissionSysconsoleWriteReportingTeamStatistics *Permission
var PermissionSysconsoleReadReportingServerLogs *Permission
var PermissionSysconsoleWriteReportingServerLogs *Permission
var PermissionSysconsoleReadUserManagementUsers *Permission
var PermissionSysconsoleWriteUserManagementUsers *Permission
var PermissionSysconsoleReadUserManagementGroups *Permission
var PermissionSysconsoleWriteUserManagementGroups *Permission
var PermissionSysconsoleReadUserManagementTeams *Permission
var PermissionSysconsoleWriteUserManagementTeams *Permission
var PermissionSysconsoleReadUserManagementChannels *Permission
var PermissionSysconsoleWriteUserManagementChannels *Permission
var PermissionSysconsoleReadUserManagementPermissions *Permission
var PermissionSysconsoleWriteUserManagementPermissions *Permission
var PermissionSysconsoleReadUserManagementSystemRoles *Permission
var PermissionSysconsoleWriteUserManagementSystemRoles *Permission
// DEPRECATED
var PermissionSysconsoleReadEnvironment *Permission
// DEPRECATED
var PermissionSysconsoleWriteEnvironment *Permission
var PermissionSysconsoleReadEnvironmentWebServer *Permission
var PermissionSysconsoleWriteEnvironmentWebServer *Permission
var PermissionSysconsoleReadEnvironmentDatabase *Permission
var PermissionSysconsoleWriteEnvironmentDatabase *Permission
var PermissionSysconsoleReadEnvironmentElasticsearch *Permission
var PermissionSysconsoleWriteEnvironmentElasticsearch *Permission
var PermissionSysconsoleReadEnvironmentFileStorage *Permission
var PermissionSysconsoleWriteEnvironmentFileStorage *Permission
var PermissionSysconsoleReadEnvironmentImageProxy *Permission
var PermissionSysconsoleWriteEnvironmentImageProxy *Permission
var PermissionSysconsoleReadEnvironmentSMTP *Permission
var PermissionSysconsoleWriteEnvironmentSMTP *Permission
var PermissionSysconsoleReadEnvironmentPushNotificationServer *Permission
var PermissionSysconsoleWriteEnvironmentPushNotificationServer *Permission
var PermissionSysconsoleReadEnvironmentHighAvailability *Permission
var PermissionSysconsoleWriteEnvironmentHighAvailability *Permission
var PermissionSysconsoleReadEnvironmentRateLimiting *Permission
var PermissionSysconsoleWriteEnvironmentRateLimiting *Permission
var PermissionSysconsoleReadEnvironmentLogging *Permission
var PermissionSysconsoleWriteEnvironmentLogging *Permission
var PermissionSysconsoleReadEnvironmentSessionLengths *Permission
var PermissionSysconsoleWriteEnvironmentSessionLengths *Permission
var PermissionSysconsoleReadEnvironmentPerformanceMonitoring *Permission
var PermissionSysconsoleWriteEnvironmentPerformanceMonitoring *Permission
var PermissionSysconsoleReadEnvironmentDeveloper *Permission
var PermissionSysconsoleWriteEnvironmentDeveloper *Permission
var PermissionSysconsoleReadSite *Permission
var PermissionSysconsoleWriteSite *Permission
var PermissionSysconsoleReadSiteCustomization *Permission
var PermissionSysconsoleWriteSiteCustomization *Permission
var PermissionSysconsoleReadSiteLocalization *Permission
var PermissionSysconsoleWriteSiteLocalization *Permission
var PermissionSysconsoleReadSiteUsersAndTeams *Permission
var PermissionSysconsoleWriteSiteUsersAndTeams *Permission
var PermissionSysconsoleReadSiteNotifications *Permission
var PermissionSysconsoleWriteSiteNotifications *Permission
var PermissionSysconsoleReadSiteAnnouncementBanner *Permission
var PermissionSysconsoleWriteSiteAnnouncementBanner *Permission
var PermissionSysconsoleReadSiteEmoji *Permission
var PermissionSysconsoleWriteSiteEmoji *Permission
var PermissionSysconsoleReadSitePosts *Permission
var PermissionSysconsoleWriteSitePosts *Permission
var PermissionSysconsoleReadSiteFileSharingAndDownloads *Permission
var PermissionSysconsoleWriteSiteFileSharingAndDownloads *Permission
var PermissionSysconsoleReadSitePublicLinks *Permission
var PermissionSysconsoleWriteSitePublicLinks *Permission
var PermissionSysconsoleReadSiteNotices *Permission
var PermissionSysconsoleWriteSiteNotices *Permission
var PermissionSysconsoleReadAuthentication *Permission
var PermissionSysconsoleWriteAuthentication *Permission
var PermissionSysconsoleReadAuthenticationSignup *Permission
var PermissionSysconsoleWriteAuthenticationSignup *Permission
var PermissionSysconsoleReadAuthenticationEmail *Permission
var PermissionSysconsoleWriteAuthenticationEmail *Permission
var PermissionSysconsoleReadAuthenticationPassword *Permission
var PermissionSysconsoleWriteAuthenticationPassword *Permission
var PermissionSysconsoleReadAuthenticationMfa *Permission
var PermissionSysconsoleWriteAuthenticationMfa *Permission
var PermissionSysconsoleReadAuthenticationLdap *Permission
var PermissionSysconsoleWriteAuthenticationLdap *Permission
var PermissionSysconsoleReadAuthenticationSaml *Permission
var PermissionSysconsoleWriteAuthenticationSaml *Permission
var PermissionSysconsoleReadAuthenticationOpenid *Permission
var PermissionSysconsoleWriteAuthenticationOpenid *Permission
var PermissionSysconsoleReadAuthenticationGuestAccess *Permission
var PermissionSysconsoleWriteAuthenticationGuestAccess *Permission
var PermissionSysconsoleReadPlugins *Permission
var PermissionSysconsoleWritePlugins *Permission
var PermissionSysconsoleReadIntegrations *Permission
var PermissionSysconsoleWriteIntegrations *Permission
var PermissionSysconsoleReadIntegrationsIntegrationManagement *Permission
var PermissionSysconsoleWriteIntegrationsIntegrationManagement *Permission
var PermissionSysconsoleReadIntegrationsBotAccounts *Permission
var PermissionSysconsoleWriteIntegrationsBotAccounts *Permission
var PermissionSysconsoleReadIntegrationsGif *Permission
var PermissionSysconsoleWriteIntegrationsGif *Permission
var PermissionSysconsoleReadIntegrationsCors *Permission
var PermissionSysconsoleWriteIntegrationsCors *Permission
var PermissionSysconsoleReadCompliance *Permission
var PermissionSysconsoleWriteCompliance *Permission
var PermissionSysconsoleReadComplianceDataRetentionPolicy *Permission
var PermissionSysconsoleWriteComplianceDataRetentionPolicy *Permission
var PermissionSysconsoleReadComplianceComplianceExport *Permission
var PermissionSysconsoleWriteComplianceComplianceExport *Permission
var PermissionSysconsoleReadComplianceComplianceMonitoring *Permission
var PermissionSysconsoleWriteComplianceComplianceMonitoring *Permission
var PermissionSysconsoleReadComplianceCustomTermsOfService *Permission
var PermissionSysconsoleWriteComplianceCustomTermsOfService *Permission
var PermissionSysconsoleReadExperimental *Permission
var PermissionSysconsoleWriteExperimental *Permission
var PermissionSysconsoleReadExperimentalFeatures *Permission
var PermissionSysconsoleWriteExperimentalFeatures *Permission
var PermissionSysconsoleReadExperimentalFeatureFlags *Permission
var PermissionSysconsoleWriteExperimentalFeatureFlags *Permission
var PermissionSysconsoleReadExperimentalBleve *Permission
var PermissionSysconsoleWriteExperimentalBleve *Permission
var PermissionPublicPlaybookCreate *Permission
var PermissionPublicPlaybookManageProperties *Permission
var PermissionPublicPlaybookManageMembers *Permission
var PermissionPublicPlaybookManageRoles *Permission
var PermissionPublicPlaybookView *Permission
var PermissionPublicPlaybookMakePrivate *Permission
var PermissionPrivatePlaybookCreate *Permission
var PermissionPrivatePlaybookManageProperties *Permission
var PermissionPrivatePlaybookManageMembers *Permission
var PermissionPrivatePlaybookManageRoles *Permission
var PermissionPrivatePlaybookView *Permission
var PermissionPrivatePlaybookMakePublic *Permission
var PermissionRunCreate *Permission
var PermissionRunManageProperties *Permission
var PermissionRunManageMembers *Permission
var PermissionRunView *Permission
var PermissionSysconsoleReadProductsBoards *Permission
var PermissionSysconsoleWriteProductsBoards *Permission
// General permission that encompasses all system admin functions
// in the future this could be broken up to allow access to some
// admin functions but not others
var PermissionManageSystem *Permission
var PermissionCreateCustomGroup *Permission
var PermissionManageCustomGroupMembers *Permission
var PermissionEditCustomGroup *Permission
var PermissionDeleteCustomGroup *Permission
var PermissionRestoreCustomGroup *Permission
var AllPermissions []*Permission
var DeprecatedPermissions []*Permission
var ChannelModeratedPermissions []string
var ChannelModeratedPermissionsMap map[string]string
var SysconsoleReadPermissions []*Permission
var SysconsoleWritePermissions []*Permission
func initializePermissions() {
PermissionInviteUser = &Permission{
"invite_user",
"authentication.permissions.team_invite_user.name",
"authentication.permissions.team_invite_user.description",
PermissionScopeTeam,
}
PermissionAddUserToTeam = &Permission{
"add_user_to_team",
"authentication.permissions.add_user_to_team.name",
"authentication.permissions.add_user_to_team.description",
PermissionScopeTeam,
}
PermissionUseSlashCommands = &Permission{
"use_slash_commands",
"authentication.permissions.team_use_slash_commands.name",
"authentication.permissions.team_use_slash_commands.description",
PermissionScopeChannel,
}
PermissionManageSlashCommands = &Permission{
"manage_slash_commands",
"authentication.permissions.manage_slash_commands.name",
"authentication.permissions.manage_slash_commands.description",
PermissionScopeTeam,
}
PermissionManageOthersSlashCommands = &Permission{
"manage_others_slash_commands",
"authentication.permissions.manage_others_slash_commands.name",
"authentication.permissions.manage_others_slash_commands.description",
PermissionScopeTeam,
}
PermissionCreatePublicChannel = &Permission{
"create_public_channel",
"authentication.permissions.create_public_channel.name",
"authentication.permissions.create_public_channel.description",
PermissionScopeTeam,
}
PermissionCreatePrivateChannel = &Permission{
"create_private_channel",
"authentication.permissions.create_private_channel.name",
"authentication.permissions.create_private_channel.description",
PermissionScopeTeam,
}
PermissionManagePublicChannelMembers = &Permission{
"manage_public_channel_members",
"authentication.permissions.manage_public_channel_members.name",
"authentication.permissions.manage_public_channel_members.description",
PermissionScopeChannel,
}
PermissionManagePrivateChannelMembers = &Permission{
"manage_private_channel_members",
"authentication.permissions.manage_private_channel_members.name",
"authentication.permissions.manage_private_channel_members.description",
PermissionScopeChannel,
}
PermissionConvertPublicChannelToPrivate = &Permission{
"convert_public_channel_to_private",
"authentication.permissions.convert_public_channel_to_private.name",
"authentication.permissions.convert_public_channel_to_private.description",
PermissionScopeChannel,
}
PermissionConvertPrivateChannelToPublic = &Permission{
"convert_private_channel_to_public",
"authentication.permissions.convert_private_channel_to_public.name",
"authentication.permissions.convert_private_channel_to_public.description",
PermissionScopeChannel,
}
PermissionAssignSystemAdminRole = &Permission{
"assign_system_admin_role",
"authentication.permissions.assign_system_admin_role.name",
"authentication.permissions.assign_system_admin_role.description",
PermissionScopeSystem,
}
PermissionManageRoles = &Permission{
"manage_roles",
"authentication.permissions.manage_roles.name",
"authentication.permissions.manage_roles.description",
PermissionScopeSystem,
}
PermissionManageTeamRoles = &Permission{
"manage_team_roles",
"authentication.permissions.manage_team_roles.name",
"authentication.permissions.manage_team_roles.description",
PermissionScopeTeam,
}
PermissionManageChannelRoles = &Permission{
"manage_channel_roles",
"authentication.permissions.manage_channel_roles.name",
"authentication.permissions.manage_channel_roles.description",
PermissionScopeChannel,
}
PermissionManageSystem = &Permission{
"manage_system",
"authentication.permissions.manage_system.name",
"authentication.permissions.manage_system.description",
PermissionScopeSystem,
}
PermissionCreateDirectChannel = &Permission{
"create_direct_channel",
"authentication.permissions.create_direct_channel.name",
"authentication.permissions.create_direct_channel.description",
PermissionScopeSystem,
}
PermissionCreateGroupChannel = &Permission{
"create_group_channel",
"authentication.permissions.create_group_channel.name",
"authentication.permissions.create_group_channel.description",
PermissionScopeSystem,
}
PermissionManagePublicChannelProperties = &Permission{
"manage_public_channel_properties",
"authentication.permissions.manage_public_channel_properties.name",
"authentication.permissions.manage_public_channel_properties.description",
PermissionScopeChannel,
}
PermissionManagePrivateChannelProperties = &Permission{
"manage_private_channel_properties",
"authentication.permissions.manage_private_channel_properties.name",
"authentication.permissions.manage_private_channel_properties.description",
PermissionScopeChannel,
}
PermissionListPublicTeams = &Permission{
"list_public_teams",
"authentication.permissions.list_public_teams.name",
"authentication.permissions.list_public_teams.description",
PermissionScopeSystem,
}
PermissionJoinPublicTeams = &Permission{
"join_public_teams",
"authentication.permissions.join_public_teams.name",
"authentication.permissions.join_public_teams.description",
PermissionScopeSystem,
}
PermissionListPrivateTeams = &Permission{
"list_private_teams",
"authentication.permissions.list_private_teams.name",
"authentication.permissions.list_private_teams.description",
PermissionScopeSystem,
}
PermissionJoinPrivateTeams = &Permission{
"join_private_teams",
"authentication.permissions.join_private_teams.name",
"authentication.permissions.join_private_teams.description",
PermissionScopeSystem,
}
PermissionListTeamChannels = &Permission{
"list_team_channels",
"authentication.permissions.list_team_channels.name",
"authentication.permissions.list_team_channels.description",
PermissionScopeTeam,
}
PermissionJoinPublicChannels = &Permission{
"join_public_channels",
"authentication.permissions.join_public_channels.name",
"authentication.permissions.join_public_channels.description",
PermissionScopeTeam,
}
PermissionDeletePublicChannel = &Permission{
"delete_public_channel",
"authentication.permissions.delete_public_channel.name",
"authentication.permissions.delete_public_channel.description",
PermissionScopeChannel,
}
PermissionDeletePrivateChannel = &Permission{
"delete_private_channel",
"authentication.permissions.delete_private_channel.name",
"authentication.permissions.delete_private_channel.description",
PermissionScopeChannel,
}
PermissionEditOtherUsers = &Permission{
"edit_other_users",
"authentication.permissions.edit_other_users.name",
"authentication.permissions.edit_other_users.description",
PermissionScopeSystem,
}
PermissionReadChannel = &Permission{
"read_channel",
"authentication.permissions.read_channel.name",
"authentication.permissions.read_channel.description",
PermissionScopeChannel,
}
PermissionReadPublicChannelGroups = &Permission{
"read_public_channel_groups",
"authentication.permissions.read_public_channel_groups.name",
"authentication.permissions.read_public_channel_groups.description",
PermissionScopeChannel,
}
PermissionReadPrivateChannelGroups = &Permission{
"read_private_channel_groups",
"authentication.permissions.read_private_channel_groups.name",
"authentication.permissions.read_private_channel_groups.description",
PermissionScopeChannel,
}
PermissionReadPublicChannel = &Permission{
"read_public_channel",
"authentication.permissions.read_public_channel.name",
"authentication.permissions.read_public_channel.description",
PermissionScopeTeam,
}
PermissionAddReaction = &Permission{
"add_reaction",
"authentication.permissions.add_reaction.name",
"authentication.permissions.add_reaction.description",
PermissionScopeChannel,
}
PermissionRemoveReaction = &Permission{
"remove_reaction",
"authentication.permissions.remove_reaction.name",
"authentication.permissions.remove_reaction.description",
PermissionScopeChannel,
}
PermissionRemoveOthersReactions = &Permission{
"remove_others_reactions",
"authentication.permissions.remove_others_reactions.name",
"authentication.permissions.remove_others_reactions.description",
PermissionScopeChannel,
}
// DEPRECATED
PermissionPermanentDeleteUser = &Permission{
"permanent_delete_user",
"authentication.permissions.permanent_delete_user.name",
"authentication.permissions.permanent_delete_user.description",
PermissionScopeSystem,
}
PermissionUploadFile = &Permission{
"upload_file",
"authentication.permissions.upload_file.name",
"authentication.permissions.upload_file.description",
PermissionScopeChannel,
}
PermissionGetPublicLink = &Permission{
"get_public_link",
"authentication.permissions.get_public_link.name",
"authentication.permissions.get_public_link.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionManageWebhooks = &Permission{
"manage_webhooks",
"authentication.permissions.manage_webhooks.name",
"authentication.permissions.manage_webhooks.description",
PermissionScopeTeam,
}
// DEPRECATED
PermissionManageOthersWebhooks = &Permission{
"manage_others_webhooks",
"authentication.permissions.manage_others_webhooks.name",
"authentication.permissions.manage_others_webhooks.description",
PermissionScopeTeam,
}
PermissionManageIncomingWebhooks = &Permission{
"manage_incoming_webhooks",
"authentication.permissions.manage_incoming_webhooks.name",
"authentication.permissions.manage_incoming_webhooks.description",
PermissionScopeTeam,
}
PermissionManageOutgoingWebhooks = &Permission{
"manage_outgoing_webhooks",
"authentication.permissions.manage_outgoing_webhooks.name",
"authentication.permissions.manage_outgoing_webhooks.description",
PermissionScopeTeam,
}
PermissionManageOthersIncomingWebhooks = &Permission{
"manage_others_incoming_webhooks",
"authentication.permissions.manage_others_incoming_webhooks.name",
"authentication.permissions.manage_others_incoming_webhooks.description",
PermissionScopeTeam,
}
PermissionManageOthersOutgoingWebhooks = &Permission{
"manage_others_outgoing_webhooks",
"authentication.permissions.manage_others_outgoing_webhooks.name",
"authentication.permissions.manage_others_outgoing_webhooks.description",
PermissionScopeTeam,
}
PermissionManageOAuth = &Permission{
"manage_oauth",
"authentication.permissions.manage_oauth.name",
"authentication.permissions.manage_oauth.description",
PermissionScopeSystem,
}
PermissionManageSystemWideOAuth = &Permission{
"manage_system_wide_oauth",
"authentication.permissions.manage_system_wide_oauth.name",
"authentication.permissions.manage_system_wide_oauth.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionManageEmojis = &Permission{
"manage_emojis",
"authentication.permissions.manage_emojis.name",
"authentication.permissions.manage_emojis.description",
PermissionScopeTeam,
}
// DEPRECATED
PermissionManageOthersEmojis = &Permission{
"manage_others_emojis",
"authentication.permissions.manage_others_emojis.name",
"authentication.permissions.manage_others_emojis.description",
PermissionScopeTeam,
}
PermissionCreateEmojis = &Permission{
"create_emojis",
"authentication.permissions.create_emojis.name",
"authentication.permissions.create_emojis.description",
PermissionScopeTeam,
}
PermissionDeleteEmojis = &Permission{
"delete_emojis",
"authentication.permissions.delete_emojis.name",
"authentication.permissions.delete_emojis.description",
PermissionScopeTeam,
}
PermissionDeleteOthersEmojis = &Permission{
"delete_others_emojis",
"authentication.permissions.delete_others_emojis.name",
"authentication.permissions.delete_others_emojis.description",
PermissionScopeTeam,
}
PermissionCreatePost = &Permission{
"create_post",
"authentication.permissions.create_post.name",
"authentication.permissions.create_post.description",
PermissionScopeChannel,
}
PermissionCreatePostPublic = &Permission{
"create_post_public",
"authentication.permissions.create_post_public.name",
"authentication.permissions.create_post_public.description",
PermissionScopeChannel,
}
PermissionCreatePostEphemeral = &Permission{
"create_post_ephemeral",
"authentication.permissions.create_post_ephemeral.name",
"authentication.permissions.create_post_ephemeral.description",
PermissionScopeChannel,
}
PermissionReadDeletedPosts = &Permission{
"read_deleted_posts",
"authentication.permissions.read_deleted_posts.name",
"authentication.permissions.read_deleted_posts.description",
PermissionScopeChannel,
}
PermissionEditPost = &Permission{
"edit_post",
"authentication.permissions.edit_post.name",
"authentication.permissions.edit_post.description",
PermissionScopeChannel,
}
PermissionEditOthersPosts = &Permission{
"edit_others_posts",
"authentication.permissions.edit_others_posts.name",
"authentication.permissions.edit_others_posts.description",
PermissionScopeChannel,
}
PermissionDeletePost = &Permission{
"delete_post",
"authentication.permissions.delete_post.name",
"authentication.permissions.delete_post.description",
PermissionScopeChannel,
}
PermissionDeleteOthersPosts = &Permission{
"delete_others_posts",
"authentication.permissions.delete_others_posts.name",
"authentication.permissions.delete_others_posts.description",
PermissionScopeChannel,
}
PermissionManageSharedChannels = &Permission{
"manage_shared_channels",
"authentication.permissions.manage_shared_channels.name",
"authentication.permissions.manage_shared_channels.description",
PermissionScopeSystem,
}
PermissionManageSecureConnections = &Permission{
"manage_secure_connections",
"authentication.permissions.manage_secure_connections.name",
"authentication.permissions.manage_secure_connections.description",
PermissionScopeSystem,
}
PermissionCreateDataRetentionJob = &Permission{
"create_data_retention_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadDataRetentionJob = &Permission{
"read_data_retention_job",
"",
"",
PermissionScopeSystem,
}
PermissionCreateComplianceExportJob = &Permission{
"create_compliance_export_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadComplianceExportJob = &Permission{
"read_compliance_export_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadAudits = &Permission{
"read_audits",
"",
"",
PermissionScopeSystem,
}
PermissionPurgeBleveIndexes = &Permission{
"purge_bleve_indexes",
"",
"",
PermissionScopeSystem,
}
PermissionCreatePostBleveIndexesJob = &Permission{
"create_post_bleve_indexes_job",
"",
"",
PermissionScopeSystem,
}
PermissionCreateLdapSyncJob = &Permission{
"create_ldap_sync_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadLdapSyncJob = &Permission{
"read_ldap_sync_job",
"",
"",
PermissionScopeSystem,
}
PermissionTestLdap = &Permission{
"test_ldap",
"",
"",
PermissionScopeSystem,
}
PermissionInvalidateEmailInvite = &Permission{
"invalidate_email_invite",
"",
"",
PermissionScopeSystem,
}
PermissionGetSamlMetadataFromIdp = &Permission{
"get_saml_metadata_from_idp",
"",
"",
PermissionScopeSystem,
}
PermissionAddSamlPublicCert = &Permission{
"add_saml_public_cert",
"",
"",
PermissionScopeSystem,
}
PermissionAddSamlPrivateCert = &Permission{
"add_saml_private_cert",
"",
"",
PermissionScopeSystem,
}
PermissionAddSamlIdpCert = &Permission{
"add_saml_idp_cert",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveSamlPublicCert = &Permission{
"remove_saml_public_cert",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveSamlPrivateCert = &Permission{
"remove_saml_private_cert",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveSamlIdpCert = &Permission{
"remove_saml_idp_cert",
"",
"",
PermissionScopeSystem,
}
PermissionGetSamlCertStatus = &Permission{
"get_saml_cert_status",
"",
"",
PermissionScopeSystem,
}
PermissionAddLdapPublicCert = &Permission{
"add_ldap_public_cert",
"",
"",
PermissionScopeSystem,
}
PermissionAddLdapPrivateCert = &Permission{
"add_ldap_private_cert",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveLdapPublicCert = &Permission{
"remove_ldap_public_cert",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveLdapPrivateCert = &Permission{
"remove_ldap_private_cert",
"",
"",
PermissionScopeSystem,
}
PermissionGetLogs = &Permission{
"get_logs",
"",
"",
PermissionScopeSystem,
}
PermissionReadLicenseInformation = &Permission{
"read_license_information",
"",
"",
PermissionScopeSystem,
}
PermissionGetAnalytics = &Permission{
"get_analytics",
"",
"",
PermissionScopeSystem,
}
PermissionManageLicenseInformation = &Permission{
"manage_license_information",
"",
"",
PermissionScopeSystem,
}
PermissionDownloadComplianceExportResult = &Permission{
"download_compliance_export_result",
"authentication.permissions.download_compliance_export_result.name",
"authentication.permissions.download_compliance_export_result.description",
PermissionScopeSystem,
}
PermissionTestSiteURL = &Permission{
"test_site_url",
"",
"",
PermissionScopeSystem,
}
PermissionTestElasticsearch = &Permission{
"test_elasticsearch",
"",
"",
PermissionScopeSystem,
}
PermissionTestS3 = &Permission{
"test_s3",
"",
"",
PermissionScopeSystem,
}
PermissionReloadConfig = &Permission{
"reload_config",
"",
"",
PermissionScopeSystem,
}
PermissionInvalidateCaches = &Permission{
"invalidate_caches",
"",
"",
PermissionScopeSystem,
}
PermissionRecycleDatabaseConnections = &Permission{
"recycle_database_connections",
"",
"",
PermissionScopeSystem,
}
PermissionPurgeElasticsearchIndexes = &Permission{
"purge_elasticsearch_indexes",
"",
"",
PermissionScopeSystem,
}
PermissionTestEmail = &Permission{
"test_email",
"",
"",
PermissionScopeSystem,
}
PermissionCreateElasticsearchPostIndexingJob = &Permission{
"create_elasticsearch_post_indexing_job",
"",
"",
PermissionScopeSystem,
}
PermissionCreateElasticsearchPostAggregationJob = &Permission{
"create_elasticsearch_post_aggregation_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadElasticsearchPostIndexingJob = &Permission{
"read_elasticsearch_post_indexing_job",
"",
"",
PermissionScopeSystem,
}
PermissionReadElasticsearchPostAggregationJob = &Permission{
"read_elasticsearch_post_aggregation_job",
"",
"",
PermissionScopeSystem,
}
PermissionRemoveUserFromTeam = &Permission{
"remove_user_from_team",
"authentication.permissions.remove_user_from_team.name",
"authentication.permissions.remove_user_from_team.description",
PermissionScopeTeam,
}
PermissionCreateTeam = &Permission{
"create_team",
"authentication.permissions.create_team.name",
"authentication.permissions.create_team.description",
PermissionScopeSystem,
}
PermissionManageTeam = &Permission{
"manage_team",
"authentication.permissions.manage_team.name",
"authentication.permissions.manage_team.description",
PermissionScopeTeam,
}
PermissionImportTeam = &Permission{
"import_team",
"authentication.permissions.import_team.name",
"authentication.permissions.import_team.description",
PermissionScopeTeam,
}
PermissionViewTeam = &Permission{
"view_team",
"authentication.permissions.view_team.name",
"authentication.permissions.view_team.description",
PermissionScopeTeam,
}
PermissionListUsersWithoutTeam = &Permission{
"list_users_without_team",
"authentication.permissions.list_users_without_team.name",
"authentication.permissions.list_users_without_team.description",
PermissionScopeSystem,
}
PermissionCreateUserAccessToken = &Permission{
"create_user_access_token",
"authentication.permissions.create_user_access_token.name",
"authentication.permissions.create_user_access_token.description",
PermissionScopeSystem,
}
PermissionReadUserAccessToken = &Permission{
"read_user_access_token",
"authentication.permissions.read_user_access_token.name",
"authentication.permissions.read_user_access_token.description",
PermissionScopeSystem,
}
PermissionRevokeUserAccessToken = &Permission{
"revoke_user_access_token",
"authentication.permissions.revoke_user_access_token.name",
"authentication.permissions.revoke_user_access_token.description",
PermissionScopeSystem,
}
PermissionCreateBot = &Permission{
"create_bot",
"authentication.permissions.create_bot.name",
"authentication.permissions.create_bot.description",
PermissionScopeSystem,
}
PermissionAssignBot = &Permission{
"assign_bot",
"authentication.permissions.assign_bot.name",
"authentication.permissions.assign_bot.description",
PermissionScopeSystem,
}
PermissionReadBots = &Permission{
"read_bots",
"authentication.permissions.read_bots.name",
"authentication.permissions.read_bots.description",
PermissionScopeSystem,
}
PermissionReadOthersBots = &Permission{
"read_others_bots",
"authentication.permissions.read_others_bots.name",
"authentication.permissions.read_others_bots.description",
PermissionScopeSystem,
}
PermissionManageBots = &Permission{
"manage_bots",
"authentication.permissions.manage_bots.name",
"authentication.permissions.manage_bots.description",
PermissionScopeSystem,
}
PermissionManageOthersBots = &Permission{
"manage_others_bots",
"authentication.permissions.manage_others_bots.name",
"authentication.permissions.manage_others_bots.description",
PermissionScopeSystem,
}
PermissionReadJobs = &Permission{
"read_jobs",
"authentication.permisssions.read_jobs.name",
"authentication.permisssions.read_jobs.description",
PermissionScopeSystem,
}
PermissionManageJobs = &Permission{
"manage_jobs",
"authentication.permisssions.manage_jobs.name",
"authentication.permisssions.manage_jobs.description",
PermissionScopeSystem,
}
PermissionViewMembers = &Permission{
"view_members",
"authentication.permisssions.view_members.name",
"authentication.permisssions.view_members.description",
PermissionScopeTeam,
}
PermissionInviteGuest = &Permission{
"invite_guest",
"authentication.permissions.invite_guest.name",
"authentication.permissions.invite_guest.description",
PermissionScopeTeam,
}
PermissionPromoteGuest = &Permission{
"promote_guest",
"authentication.permissions.promote_guest.name",
"authentication.permissions.promote_guest.description",
PermissionScopeSystem,
}
PermissionDemoteToGuest = &Permission{
"demote_to_guest",
"authentication.permissions.demote_to_guest.name",
"authentication.permissions.demote_to_guest.description",
PermissionScopeSystem,
}
PermissionUseChannelMentions = &Permission{
"use_channel_mentions",
"authentication.permissions.use_channel_mentions.name",
"authentication.permissions.use_channel_mentions.description",
PermissionScopeChannel,
}
PermissionUseGroupMentions = &Permission{
"use_group_mentions",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeChannel,
}
PermissionReadOtherUsersTeams = &Permission{
"read_other_users_teams",
"authentication.permissions.read_other_users_teams.name",
"authentication.permissions.read_other_users_teams.description",
PermissionScopeSystem,
}
PermissionEditBrand = &Permission{
"edit_brand",
"authentication.permissions.edit_brand.name",
"authentication.permissions.edit_brand.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadAbout = &Permission{
"sysconsole_read_about",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteAbout = &Permission{
"sysconsole_write_about",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadAboutEditionAndLicense = &Permission{
"sysconsole_read_about_edition_and_license",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAboutEditionAndLicense = &Permission{
"sysconsole_write_about_edition_and_license",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadBilling = &Permission{
"sysconsole_read_billing",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteBilling = &Permission{
"sysconsole_write_billing",
"",
"",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadReporting = &Permission{
"sysconsole_read_reporting",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteReporting = &Permission{
"sysconsole_write_reporting",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadReportingSiteStatistics = &Permission{
"sysconsole_read_reporting_site_statistics",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteReportingSiteStatistics = &Permission{
"sysconsole_write_reporting_site_statistics",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadReportingTeamStatistics = &Permission{
"sysconsole_read_reporting_team_statistics",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteReportingTeamStatistics = &Permission{
"sysconsole_write_reporting_team_statistics",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadReportingServerLogs = &Permission{
"sysconsole_read_reporting_server_logs",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteReportingServerLogs = &Permission{
"sysconsole_write_reporting_server_logs",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementUsers = &Permission{
"sysconsole_read_user_management_users",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementUsers = &Permission{
"sysconsole_write_user_management_users",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementGroups = &Permission{
"sysconsole_read_user_management_groups",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementGroups = &Permission{
"sysconsole_write_user_management_groups",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementTeams = &Permission{
"sysconsole_read_user_management_teams",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementTeams = &Permission{
"sysconsole_write_user_management_teams",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementChannels = &Permission{
"sysconsole_read_user_management_channels",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementChannels = &Permission{
"sysconsole_write_user_management_channels",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementPermissions = &Permission{
"sysconsole_read_user_management_permissions",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementPermissions = &Permission{
"sysconsole_write_user_management_permissions",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadUserManagementSystemRoles = &Permission{
"sysconsole_read_user_management_system_roles",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWriteUserManagementSystemRoles = &Permission{
"sysconsole_write_user_management_system_roles",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadEnvironment = &Permission{
"sysconsole_read_environment",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteEnvironment = &Permission{
"sysconsole_write_environment",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentWebServer = &Permission{
"sysconsole_read_environment_web_server",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentWebServer = &Permission{
"sysconsole_write_environment_web_server",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentDatabase = &Permission{
"sysconsole_read_environment_database",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentDatabase = &Permission{
"sysconsole_write_environment_database",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentElasticsearch = &Permission{
"sysconsole_read_environment_elasticsearch",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentElasticsearch = &Permission{
"sysconsole_write_environment_elasticsearch",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentFileStorage = &Permission{
"sysconsole_read_environment_file_storage",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentFileStorage = &Permission{
"sysconsole_write_environment_file_storage",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentImageProxy = &Permission{
"sysconsole_read_environment_image_proxy",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentImageProxy = &Permission{
"sysconsole_write_environment_image_proxy",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentSMTP = &Permission{
"sysconsole_read_environment_smtp",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentSMTP = &Permission{
"sysconsole_write_environment_smtp",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentPushNotificationServer = &Permission{
"sysconsole_read_environment_push_notification_server",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentPushNotificationServer = &Permission{
"sysconsole_write_environment_push_notification_server",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentHighAvailability = &Permission{
"sysconsole_read_environment_high_availability",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentHighAvailability = &Permission{
"sysconsole_write_environment_high_availability",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentRateLimiting = &Permission{
"sysconsole_read_environment_rate_limiting",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentRateLimiting = &Permission{
"sysconsole_write_environment_rate_limiting",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentLogging = &Permission{
"sysconsole_read_environment_logging",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentLogging = &Permission{
"sysconsole_write_environment_logging",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentSessionLengths = &Permission{
"sysconsole_read_environment_session_lengths",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentSessionLengths = &Permission{
"sysconsole_write_environment_session_lengths",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentPerformanceMonitoring = &Permission{
"sysconsole_read_environment_performance_monitoring",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentPerformanceMonitoring = &Permission{
"sysconsole_write_environment_performance_monitoring",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadEnvironmentDeveloper = &Permission{
"sysconsole_read_environment_developer",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteEnvironmentDeveloper = &Permission{
"sysconsole_write_environment_developer",
"",
"",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadSite = &Permission{
"sysconsole_read_site",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteSite = &Permission{
"sysconsole_write_site",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteCustomization = &Permission{
"sysconsole_read_site_customization",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteCustomization = &Permission{
"sysconsole_write_site_customization",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteLocalization = &Permission{
"sysconsole_read_site_localization",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteLocalization = &Permission{
"sysconsole_write_site_localization",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteUsersAndTeams = &Permission{
"sysconsole_read_site_users_and_teams",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteUsersAndTeams = &Permission{
"sysconsole_write_site_users_and_teams",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteNotifications = &Permission{
"sysconsole_read_site_notifications",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteNotifications = &Permission{
"sysconsole_write_site_notifications",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteAnnouncementBanner = &Permission{
"sysconsole_read_site_announcement_banner",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteAnnouncementBanner = &Permission{
"sysconsole_write_site_announcement_banner",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteEmoji = &Permission{
"sysconsole_read_site_emoji",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteEmoji = &Permission{
"sysconsole_write_site_emoji",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSitePosts = &Permission{
"sysconsole_read_site_posts",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSitePosts = &Permission{
"sysconsole_write_site_posts",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteFileSharingAndDownloads = &Permission{
"sysconsole_read_site_file_sharing_and_downloads",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteFileSharingAndDownloads = &Permission{
"sysconsole_write_site_file_sharing_and_downloads",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSitePublicLinks = &Permission{
"sysconsole_read_site_public_links",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSitePublicLinks = &Permission{
"sysconsole_write_site_public_links",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadSiteNotices = &Permission{
"sysconsole_read_site_notices",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteSiteNotices = &Permission{
"sysconsole_write_site_notices",
"",
"",
PermissionScopeSystem,
}
// Deprecated
PermissionSysconsoleReadAuthentication = &Permission{
"sysconsole_read_authentication",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// Deprecated
PermissionSysconsoleWriteAuthentication = &Permission{
"sysconsole_write_authentication",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationSignup = &Permission{
"sysconsole_read_authentication_signup",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationSignup = &Permission{
"sysconsole_write_authentication_signup",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationEmail = &Permission{
"sysconsole_read_authentication_email",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationEmail = &Permission{
"sysconsole_write_authentication_email",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationPassword = &Permission{
"sysconsole_read_authentication_password",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationPassword = &Permission{
"sysconsole_write_authentication_password",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationMfa = &Permission{
"sysconsole_read_authentication_mfa",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationMfa = &Permission{
"sysconsole_write_authentication_mfa",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationLdap = &Permission{
"sysconsole_read_authentication_ldap",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationLdap = &Permission{
"sysconsole_write_authentication_ldap",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationSaml = &Permission{
"sysconsole_read_authentication_saml",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationSaml = &Permission{
"sysconsole_write_authentication_saml",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationOpenid = &Permission{
"sysconsole_read_authentication_openid",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationOpenid = &Permission{
"sysconsole_write_authentication_openid",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadAuthenticationGuestAccess = &Permission{
"sysconsole_read_authentication_guest_access",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteAuthenticationGuestAccess = &Permission{
"sysconsole_write_authentication_guest_access",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadPlugins = &Permission{
"sysconsole_read_plugins",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleWritePlugins = &Permission{
"sysconsole_write_plugins",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadIntegrations = &Permission{
"sysconsole_read_integrations",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteIntegrations = &Permission{
"sysconsole_write_integrations",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadIntegrationsIntegrationManagement = &Permission{
"sysconsole_read_integrations_integration_management",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteIntegrationsIntegrationManagement = &Permission{
"sysconsole_write_integrations_integration_management",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadIntegrationsBotAccounts = &Permission{
"sysconsole_read_integrations_bot_accounts",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteIntegrationsBotAccounts = &Permission{
"sysconsole_write_integrations_bot_accounts",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadIntegrationsGif = &Permission{
"sysconsole_read_integrations_gif",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteIntegrationsGif = &Permission{
"sysconsole_write_integrations_gif",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadIntegrationsCors = &Permission{
"sysconsole_read_integrations_cors",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteIntegrationsCors = &Permission{
"sysconsole_write_integrations_cors",
"",
"",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadCompliance = &Permission{
"sysconsole_read_compliance",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteCompliance = &Permission{
"sysconsole_write_compliance",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadComplianceDataRetentionPolicy = &Permission{
"sysconsole_read_compliance_data_retention_policy",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteComplianceDataRetentionPolicy = &Permission{
"sysconsole_write_compliance_data_retention_policy",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadComplianceComplianceExport = &Permission{
"sysconsole_read_compliance_compliance_export",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteComplianceComplianceExport = &Permission{
"sysconsole_write_compliance_compliance_export",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadComplianceComplianceMonitoring = &Permission{
"sysconsole_read_compliance_compliance_monitoring",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteComplianceComplianceMonitoring = &Permission{
"sysconsole_write_compliance_compliance_monitoring",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadComplianceCustomTermsOfService = &Permission{
"sysconsole_read_compliance_custom_terms_of_service",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteComplianceCustomTermsOfService = &Permission{
"sysconsole_write_compliance_custom_terms_of_service",
"",
"",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleReadExperimental = &Permission{
"sysconsole_read_experimental",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
// DEPRECATED
PermissionSysconsoleWriteExperimental = &Permission{
"sysconsole_write_experimental",
"authentication.permissions.use_group_mentions.name",
"authentication.permissions.use_group_mentions.description",
PermissionScopeSystem,
}
PermissionSysconsoleReadExperimentalFeatures = &Permission{
"sysconsole_read_experimental_features",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteExperimentalFeatures = &Permission{
"sysconsole_write_experimental_features",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadExperimentalFeatureFlags = &Permission{
"sysconsole_read_experimental_feature_flags",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteExperimentalFeatureFlags = &Permission{
"sysconsole_write_experimental_feature_flags",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleReadExperimentalBleve = &Permission{
"sysconsole_read_experimental_bleve",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteExperimentalBleve = &Permission{
"sysconsole_write_experimental_bleve",
"",
"",
PermissionScopeSystem,
}
PermissionCreateCustomGroup = &Permission{
"create_custom_group",
"authentication.permissions.create_custom_group.name",
"authentication.permissions.create_custom_group.description",
PermissionScopeSystem,
}
PermissionManageCustomGroupMembers = &Permission{
"manage_custom_group_members",
"authentication.permissions.manage_custom_group_members.name",
"authentication.permissions.manage_custom_group_members.description",
PermissionScopeGroup,
}
PermissionEditCustomGroup = &Permission{
"edit_custom_group",
"authentication.permissions.edit_custom_group.name",
"authentication.permissions.edit_custom_group.description",
PermissionScopeGroup,
}
PermissionDeleteCustomGroup = &Permission{
"delete_custom_group",
"authentication.permissions.delete_custom_group.name",
"authentication.permissions.delete_custom_group.description",
PermissionScopeGroup,
}
PermissionRestoreCustomGroup = &Permission{
"restore_custom_group",
"authentication.permissions.restore_custom_group.name",
"authentication.permissions.restore_custom_group.description",
PermissionScopeGroup,
}
// Playbooks
PermissionPublicPlaybookCreate = &Permission{
"playbook_public_create",
"",
"",
PermissionScopeTeam,
}
PermissionPublicPlaybookManageProperties = &Permission{
"playbook_public_manage_properties",
"",
"",
PermissionScopePlaybook,
}
PermissionPublicPlaybookManageMembers = &Permission{
"playbook_public_manage_members",
"",
"",
PermissionScopePlaybook,
}
PermissionPublicPlaybookManageRoles = &Permission{
"playbook_public_manage_roles",
"",
"",
PermissionScopePlaybook,
}
PermissionPublicPlaybookView = &Permission{
"playbook_public_view",
"",
"",
PermissionScopePlaybook,
}
PermissionPublicPlaybookMakePrivate = &Permission{
"playbook_public_make_private",
"",
"",
PermissionScopePlaybook,
}
PermissionPrivatePlaybookCreate = &Permission{
"playbook_private_create",
"",
"",
PermissionScopeTeam,
}
PermissionPrivatePlaybookManageProperties = &Permission{
"playbook_private_manage_properties",
"",
"",
PermissionScopePlaybook,
}
PermissionPrivatePlaybookManageMembers = &Permission{
"playbook_private_manage_members",
"",
"",
PermissionScopePlaybook,
}
PermissionPrivatePlaybookManageRoles = &Permission{
"playbook_private_manage_roles",
"",
"",
PermissionScopePlaybook,
}
PermissionPrivatePlaybookView = &Permission{
"playbook_private_view",
"",
"",
PermissionScopePlaybook,
}
PermissionPrivatePlaybookMakePublic = &Permission{
"playbook_private_make_public",
"",
"",
PermissionScopePlaybook,
}
PermissionRunCreate = &Permission{
"run_create",
"",
"",
PermissionScopePlaybook,
}
PermissionRunManageProperties = &Permission{
"run_manage_properties",
"",
"",
PermissionScopeRun,
}
PermissionRunManageMembers = &Permission{
"run_manage_members",
"",
"",
PermissionScopeRun,
}
PermissionRunView = &Permission{
"run_view",
"",
"",
PermissionScopeRun,
}
PermissionSysconsoleReadProductsBoards = &Permission{
"sysconsole_read_products_boards",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteProductsBoards = &Permission{
"sysconsole_write_products_boards",
"",
"",
PermissionScopeSystem,
}
SysconsoleReadPermissions = []*Permission{
PermissionSysconsoleReadAboutEditionAndLicense,
PermissionSysconsoleReadBilling,
PermissionSysconsoleReadReportingSiteStatistics,
PermissionSysconsoleReadReportingTeamStatistics,
PermissionSysconsoleReadReportingServerLogs,
PermissionSysconsoleReadUserManagementUsers,
PermissionSysconsoleReadUserManagementGroups,
PermissionSysconsoleReadUserManagementTeams,
PermissionSysconsoleReadUserManagementChannels,
PermissionSysconsoleReadUserManagementPermissions,
PermissionSysconsoleReadUserManagementSystemRoles,
PermissionSysconsoleReadEnvironmentWebServer,
PermissionSysconsoleReadEnvironmentDatabase,
PermissionSysconsoleReadEnvironmentElasticsearch,
PermissionSysconsoleReadEnvironmentFileStorage,
PermissionSysconsoleReadEnvironmentImageProxy,
PermissionSysconsoleReadEnvironmentSMTP,
PermissionSysconsoleReadEnvironmentPushNotificationServer,
PermissionSysconsoleReadEnvironmentHighAvailability,
PermissionSysconsoleReadEnvironmentRateLimiting,
PermissionSysconsoleReadEnvironmentLogging,
PermissionSysconsoleReadEnvironmentSessionLengths,
PermissionSysconsoleReadEnvironmentPerformanceMonitoring,
PermissionSysconsoleReadEnvironmentDeveloper,
PermissionSysconsoleReadSiteCustomization,
PermissionSysconsoleReadSiteLocalization,
PermissionSysconsoleReadSiteUsersAndTeams,
PermissionSysconsoleReadSiteNotifications,
PermissionSysconsoleReadSiteAnnouncementBanner,
PermissionSysconsoleReadSiteEmoji,
PermissionSysconsoleReadSitePosts,
PermissionSysconsoleReadSiteFileSharingAndDownloads,
PermissionSysconsoleReadSitePublicLinks,
PermissionSysconsoleReadSiteNotices,
PermissionSysconsoleReadAuthenticationSignup,
PermissionSysconsoleReadAuthenticationEmail,
PermissionSysconsoleReadAuthenticationPassword,
PermissionSysconsoleReadAuthenticationMfa,
PermissionSysconsoleReadAuthenticationLdap,
PermissionSysconsoleReadAuthenticationSaml,
PermissionSysconsoleReadAuthenticationOpenid,
PermissionSysconsoleReadAuthenticationGuestAccess,
PermissionSysconsoleReadPlugins,
PermissionSysconsoleReadIntegrationsIntegrationManagement,
PermissionSysconsoleReadIntegrationsBotAccounts,
PermissionSysconsoleReadIntegrationsGif,
PermissionSysconsoleReadIntegrationsCors,
PermissionSysconsoleReadComplianceDataRetentionPolicy,
PermissionSysconsoleReadComplianceComplianceExport,
PermissionSysconsoleReadComplianceComplianceMonitoring,
PermissionSysconsoleReadComplianceCustomTermsOfService,
PermissionSysconsoleReadExperimentalFeatures,
PermissionSysconsoleReadExperimentalFeatureFlags,
PermissionSysconsoleReadExperimentalBleve,
PermissionSysconsoleReadProductsBoards,
}
SysconsoleWritePermissions = []*Permission{
PermissionSysconsoleWriteAboutEditionAndLicense,
PermissionSysconsoleWriteBilling,
PermissionSysconsoleWriteReportingSiteStatistics,
PermissionSysconsoleWriteReportingTeamStatistics,
PermissionSysconsoleWriteReportingServerLogs,
PermissionSysconsoleWriteUserManagementUsers,
PermissionSysconsoleWriteUserManagementGroups,
PermissionSysconsoleWriteUserManagementTeams,
PermissionSysconsoleWriteUserManagementChannels,
PermissionSysconsoleWriteUserManagementPermissions,
PermissionSysconsoleWriteUserManagementSystemRoles,
PermissionSysconsoleWriteEnvironmentWebServer,
PermissionSysconsoleWriteEnvironmentDatabase,
PermissionSysconsoleWriteEnvironmentElasticsearch,
PermissionSysconsoleWriteEnvironmentFileStorage,
PermissionSysconsoleWriteEnvironmentImageProxy,
PermissionSysconsoleWriteEnvironmentSMTP,
PermissionSysconsoleWriteEnvironmentPushNotificationServer,
PermissionSysconsoleWriteEnvironmentHighAvailability,
PermissionSysconsoleWriteEnvironmentRateLimiting,
PermissionSysconsoleWriteEnvironmentLogging,
PermissionSysconsoleWriteEnvironmentSessionLengths,
PermissionSysconsoleWriteEnvironmentPerformanceMonitoring,
PermissionSysconsoleWriteEnvironmentDeveloper,
PermissionSysconsoleWriteSiteCustomization,
PermissionSysconsoleWriteSiteLocalization,
PermissionSysconsoleWriteSiteUsersAndTeams,
PermissionSysconsoleWriteSiteNotifications,
PermissionSysconsoleWriteSiteAnnouncementBanner,
PermissionSysconsoleWriteSiteEmoji,
PermissionSysconsoleWriteSitePosts,
PermissionSysconsoleWriteSiteFileSharingAndDownloads,
PermissionSysconsoleWriteSitePublicLinks,
PermissionSysconsoleWriteSiteNotices,
PermissionSysconsoleWriteAuthenticationSignup,
PermissionSysconsoleWriteAuthenticationEmail,
PermissionSysconsoleWriteAuthenticationPassword,
PermissionSysconsoleWriteAuthenticationMfa,
PermissionSysconsoleWriteAuthenticationLdap,
PermissionSysconsoleWriteAuthenticationSaml,
PermissionSysconsoleWriteAuthenticationOpenid,
PermissionSysconsoleWriteAuthenticationGuestAccess,
PermissionSysconsoleWritePlugins,
PermissionSysconsoleWriteIntegrationsIntegrationManagement,
PermissionSysconsoleWriteIntegrationsBotAccounts,
PermissionSysconsoleWriteIntegrationsGif,
PermissionSysconsoleWriteIntegrationsCors,
PermissionSysconsoleWriteComplianceDataRetentionPolicy,
PermissionSysconsoleWriteComplianceComplianceExport,
PermissionSysconsoleWriteComplianceComplianceMonitoring,
PermissionSysconsoleWriteComplianceCustomTermsOfService,
PermissionSysconsoleWriteExperimentalFeatures,
PermissionSysconsoleWriteExperimentalFeatureFlags,
PermissionSysconsoleWriteExperimentalBleve,
PermissionSysconsoleWriteProductsBoards,
}
SystemScopedPermissionsMinusSysconsole := []*Permission{
PermissionAssignSystemAdminRole,
PermissionManageRoles,
PermissionManageSystem,
PermissionCreateDirectChannel,
PermissionCreateGroupChannel,
PermissionListPublicTeams,
PermissionJoinPublicTeams,
PermissionListPrivateTeams,
PermissionJoinPrivateTeams,
PermissionEditOtherUsers,
PermissionReadOtherUsersTeams,
PermissionGetPublicLink,
PermissionManageOAuth,
PermissionManageSystemWideOAuth,
PermissionCreateTeam,
PermissionListUsersWithoutTeam,
PermissionCreateUserAccessToken,
PermissionReadUserAccessToken,
PermissionRevokeUserAccessToken,
PermissionCreateBot,
PermissionAssignBot,
PermissionReadBots,
PermissionReadOthersBots,
PermissionManageBots,
PermissionManageOthersBots,
PermissionReadJobs,
PermissionManageJobs,
PermissionPromoteGuest,
PermissionDemoteToGuest,
PermissionEditBrand,
PermissionManageSharedChannels,
PermissionManageSecureConnections,
PermissionDownloadComplianceExportResult,
PermissionCreateDataRetentionJob,
PermissionReadDataRetentionJob,
PermissionCreateComplianceExportJob,
PermissionReadComplianceExportJob,
PermissionReadAudits,
PermissionTestSiteURL,
PermissionTestElasticsearch,
PermissionTestS3,
PermissionReloadConfig,
PermissionInvalidateCaches,
PermissionRecycleDatabaseConnections,
PermissionPurgeElasticsearchIndexes,
PermissionTestEmail,
PermissionCreateElasticsearchPostIndexingJob,
PermissionCreateElasticsearchPostAggregationJob,
PermissionReadElasticsearchPostIndexingJob,
PermissionReadElasticsearchPostAggregationJob,
PermissionPurgeBleveIndexes,
PermissionCreatePostBleveIndexesJob,
PermissionCreateLdapSyncJob,
PermissionReadLdapSyncJob,
PermissionTestLdap,
PermissionInvalidateEmailInvite,
PermissionGetSamlMetadataFromIdp,
PermissionAddSamlPublicCert,
PermissionAddSamlPrivateCert,
PermissionAddSamlIdpCert,
PermissionRemoveSamlPublicCert,
PermissionRemoveSamlPrivateCert,
PermissionRemoveSamlIdpCert,
PermissionGetSamlCertStatus,
PermissionAddLdapPublicCert,
PermissionAddLdapPrivateCert,
PermissionRemoveLdapPublicCert,
PermissionRemoveLdapPrivateCert,
PermissionGetAnalytics,
PermissionGetLogs,
PermissionReadLicenseInformation,
PermissionManageLicenseInformation,
PermissionCreateCustomGroup,
}
TeamScopedPermissions := []*Permission{
PermissionInviteUser,
PermissionAddUserToTeam,
PermissionManageSlashCommands,
PermissionManageOthersSlashCommands,
PermissionCreatePublicChannel,
PermissionCreatePrivateChannel,
PermissionManageTeamRoles,
PermissionListTeamChannels,
PermissionJoinPublicChannels,
PermissionReadPublicChannel,
PermissionManageIncomingWebhooks,
PermissionManageOutgoingWebhooks,
PermissionManageOthersIncomingWebhooks,
PermissionManageOthersOutgoingWebhooks,
PermissionCreateEmojis,
PermissionDeleteEmojis,
PermissionDeleteOthersEmojis,
PermissionRemoveUserFromTeam,
PermissionManageTeam,
PermissionImportTeam,
PermissionViewTeam,
PermissionViewMembers,
PermissionInviteGuest,
PermissionPublicPlaybookCreate,
PermissionPrivatePlaybookCreate,
}
ChannelScopedPermissions := []*Permission{
PermissionUseSlashCommands,
PermissionManagePublicChannelMembers,
PermissionManagePrivateChannelMembers,
PermissionManageChannelRoles,
PermissionManagePublicChannelProperties,
PermissionManagePrivateChannelProperties,
PermissionConvertPublicChannelToPrivate,
PermissionConvertPrivateChannelToPublic,
PermissionDeletePublicChannel,
PermissionDeletePrivateChannel,
PermissionReadChannel,
PermissionReadPublicChannelGroups,
PermissionReadPrivateChannelGroups,
PermissionAddReaction,
PermissionRemoveReaction,
PermissionRemoveOthersReactions,
PermissionUploadFile,
PermissionCreatePost,
PermissionCreatePostPublic,
PermissionCreatePostEphemeral,
PermissionReadDeletedPosts,
PermissionEditPost,
PermissionEditOthersPosts,
PermissionDeletePost,
PermissionDeleteOthersPosts,
PermissionUseChannelMentions,
PermissionUseGroupMentions,
}
GroupScopedPermissions := []*Permission{
PermissionManageCustomGroupMembers,
PermissionEditCustomGroup,
PermissionDeleteCustomGroup,
PermissionRestoreCustomGroup,
}
DeprecatedPermissions = []*Permission{
PermissionPermanentDeleteUser,
PermissionManageWebhooks,
PermissionManageOthersWebhooks,
PermissionManageEmojis,
PermissionManageOthersEmojis,
PermissionSysconsoleReadAuthentication,
PermissionSysconsoleWriteAuthentication,
PermissionSysconsoleReadSite,
PermissionSysconsoleWriteSite,
PermissionSysconsoleReadEnvironment,
PermissionSysconsoleWriteEnvironment,
PermissionSysconsoleReadReporting,
PermissionSysconsoleWriteReporting,
PermissionSysconsoleReadAbout,
PermissionSysconsoleWriteAbout,
PermissionSysconsoleReadExperimental,
PermissionSysconsoleWriteExperimental,
PermissionSysconsoleReadIntegrations,
PermissionSysconsoleWriteIntegrations,
PermissionSysconsoleReadCompliance,
PermissionSysconsoleWriteCompliance,
}
PlaybookScopedPermissions := []*Permission{
PermissionPublicPlaybookManageProperties,
PermissionPublicPlaybookManageMembers,
PermissionPublicPlaybookManageRoles,
PermissionPublicPlaybookView,
PermissionPublicPlaybookMakePrivate,
PermissionPrivatePlaybookManageProperties,
PermissionPrivatePlaybookManageMembers,
PermissionPrivatePlaybookManageRoles,
PermissionPrivatePlaybookView,
PermissionPrivatePlaybookMakePublic,
PermissionRunCreate,
}
RunScopedPermissions := []*Permission{
PermissionRunManageProperties,
PermissionRunManageMembers,
PermissionRunView,
}
AllPermissions = []*Permission{}
AllPermissions = append(AllPermissions, SystemScopedPermissionsMinusSysconsole...)
AllPermissions = append(AllPermissions, TeamScopedPermissions...)
AllPermissions = append(AllPermissions, ChannelScopedPermissions...)
AllPermissions = append(AllPermissions, SysconsoleReadPermissions...)
AllPermissions = append(AllPermissions, SysconsoleWritePermissions...)
AllPermissions = append(AllPermissions, GroupScopedPermissions...)
AllPermissions = append(AllPermissions, PlaybookScopedPermissions...)
AllPermissions = append(AllPermissions, RunScopedPermissions...)
ChannelModeratedPermissions = []string{
PermissionCreatePost.Id,
"create_reactions",
"manage_members",
PermissionUseChannelMentions.Id,
}
ChannelModeratedPermissionsMap = map[string]string{
PermissionCreatePost.Id: ChannelModeratedPermissions[0],
PermissionAddReaction.Id: ChannelModeratedPermissions[1],
PermissionRemoveReaction.Id: ChannelModeratedPermissions[1],
PermissionManagePublicChannelMembers.Id: ChannelModeratedPermissions[2],
PermissionManagePrivateChannelMembers.Id: ChannelModeratedPermissions[2],
PermissionUseChannelMentions.Id: ChannelModeratedPermissions[3],
}
}
func init() {
initializePermissions()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"unicode/utf8"
)
const (
KeyValuePluginIdMaxRunes = 190
KeyValueKeyMaxRunes = 150
)
type PluginKeyValue struct {
PluginId string `json:"plugin_id"`
Key string `json:"key" db:"PKey"`
Value []byte `json:"value" db:"PValue"`
ExpireAt int64 `json:"expire_at"`
}
func (kv *PluginKeyValue) IsValid() *AppError {
if kv.PluginId == "" || utf8.RuneCountInString(kv.PluginId) > KeyValuePluginIdMaxRunes {
return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.plugin_id.app_error", map[string]any{"Max": KeyValueKeyMaxRunes, "Min": 0}, "key="+kv.Key, http.StatusBadRequest)
}
if kv.Key == "" || utf8.RuneCountInString(kv.Key) > KeyValueKeyMaxRunes {
return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.key.app_error", map[string]any{"Max": KeyValueKeyMaxRunes, "Min": 0}, "key="+kv.Key, http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
// PluginKVSetOptions contains information on how to store a value in the plugin KV store.
type PluginKVSetOptions struct {
Atomic bool // Only store the value if the current value matches the oldValue
OldValue []byte // The value to compare with the current value. Only used when Atomic is true
ExpireInSeconds int64 // Set an expire counter
}
// IsValid returns nil if the chosen options are valid.
func (opt *PluginKVSetOptions) IsValid() *AppError {
if !opt.Atomic && opt.OldValue != nil {
return NewAppError(
"PluginKVSetOptions.IsValid",
"model.plugin_kvset_options.is_valid.old_value.app_error",
nil,
"",
http.StatusBadRequest,
)
}
return nil
}
// NewPluginKeyValueFromOptions return a PluginKeyValue given a pluginID, a KV pair and options.
func NewPluginKeyValueFromOptions(pluginId, key string, value []byte, opt PluginKVSetOptions) (*PluginKeyValue, *AppError) {
expireAt := int64(0)
if opt.ExpireInSeconds != 0 {
expireAt = GetMillis() + (opt.ExpireInSeconds * 1000)
}
kv := &PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: value,
ExpireAt: expireAt,
}
return kv, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"regexp"
"unicode/utf8"
)
const (
MinIdLength = 3
MaxIdLength = 190
ValidIdRegex = `^[a-zA-Z0-9-_\.]+$`
)
// ValidId constrains the set of valid plugin identifiers:
//
// ^[a-zA-Z0-9-_\.]+
var validId *regexp.Regexp
func init() {
validId = regexp.MustCompile(ValidIdRegex)
}
// IsValidPluginId verifies that the plugin id has a minimum length of 3, maximum length of 190, and
// contains only alphanumeric characters, dashes, underscores and periods.
//
// These constraints are necessary since the plugin id is used as part of a filesystem path.
func IsValidPluginId(id string) bool {
if utf8.RuneCountInString(id) < MinIdLength {
return false
}
if utf8.RuneCountInString(id) > MaxIdLength {
return false
}
return validId.MatchString(id)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"sync"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
)
const (
PostSystemMessagePrefix = "system_"
PostTypeDefault = ""
PostTypeSlackAttachment = "slack_attachment"
PostTypeSystemGeneric = "system_generic"
PostTypeJoinLeave = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead
PostTypeJoinChannel = "system_join_channel"
PostTypeGuestJoinChannel = "system_guest_join_channel"
PostTypeLeaveChannel = "system_leave_channel"
PostTypeJoinTeam = "system_join_team"
PostTypeLeaveTeam = "system_leave_team"
PostTypeAutoResponder = "system_auto_responder"
PostTypeAddRemove = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead
PostTypeAddToChannel = "system_add_to_channel"
PostTypeAddGuestToChannel = "system_add_guest_to_chan"
PostTypeRemoveFromChannel = "system_remove_from_channel"
PostTypeMoveChannel = "system_move_channel"
PostTypeAddToTeam = "system_add_to_team"
PostTypeRemoveFromTeam = "system_remove_from_team"
PostTypeHeaderChange = "system_header_change"
PostTypeDisplaynameChange = "system_displayname_change"
PostTypeConvertChannel = "system_convert_channel"
PostTypePurposeChange = "system_purpose_change"
PostTypeChannelDeleted = "system_channel_deleted"
PostTypeChannelRestored = "system_channel_restored"
PostTypeEphemeral = "system_ephemeral"
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
PostTypeWelcomePost = "system_welcome_post"
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
PostTypeSystemWarnMetricStatus = "warn_metric_status"
PostTypeMe = "me"
PostCustomTypePrefix = "custom_"
PostTypeReminder = "reminder"
PostFileidsMaxRunes = 300
PostFilenamesMaxRunes = 4000
PostHashtagsMaxRunes = 1000
PostMessageMaxRunesV1 = 4000
PostMessageMaxBytesV2 = 65535 // Maximum size of a TEXT column in MySQL
PostMessageMaxRunesV2 = PostMessageMaxBytesV2 / 4 // Assume a worst-case representation
PostPropsMaxRunes = 800000
PostPropsMaxUserRunes = PostPropsMaxRunes - 40000 // Leave some room for system / pre-save modifications
PropsAddChannelMember = "add_channel_member"
PostPropsAddedUserId = "addedUserId"
PostPropsDeleteBy = "deleteBy"
PostPropsOverrideIconURL = "override_icon_url"
PostPropsOverrideIconEmoji = "override_icon_emoji"
PostPropsMentionHighlightDisabled = "mentionHighlightDisabled"
PostPropsGroupHighlightDisabled = "disable_group_highlight"
PostPropsPreviewedPost = "previewed_post"
PostPriorityUrgent = "urgent"
PostPropsRequestedAck = "requested_ack"
PostPropsPersistentNotifications = "persistent_notifications"
)
const (
ModifierMessages string = "messages"
ModifierFiles string = "files"
)
type Post struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
EditAt int64 `json:"edit_at"`
DeleteAt int64 `json:"delete_at"`
IsPinned bool `json:"is_pinned"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
RootId string `json:"root_id"`
OriginalId string `json:"original_id"`
Message string `json:"message"`
// MessageSource will contain the message as submitted by the user if Message has been modified
// by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
// populate edit boxes if present.
MessageSource string `json:"message_source,omitempty"`
Type string `json:"type"`
propsMu sync.RWMutex `db:"-"` // Unexported mutex used to guard Post.Props.
Props StringInterface `json:"props"` // Deprecated: use GetProps()
Hashtags string `json:"hashtags"`
Filenames StringArray `json:"-"` // Deprecated, do not use this field any more
FileIds StringArray `json:"file_ids,omitempty"`
PendingPostId string `json:"pending_post_id"`
HasReactions bool `json:"has_reactions,omitempty"`
RemoteId *string `json:"remote_id,omitempty"`
// Transient data populated before sending a post to the client
ReplyCount int64 `json:"reply_count"`
LastReplyAt int64 `json:"last_reply_at"`
Participants []*User `json:"participants"`
IsFollowing *bool `json:"is_following,omitempty"` // for root posts in collapsed thread mode indicates if the current user is following this thread
Metadata *PostMetadata `json:"metadata,omitempty"`
}
func (o *Post) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"edit_at": o.EditAt,
"delete_at": o.DeleteAt,
"is_pinned": o.IsPinned,
"user_id": o.UserId,
"channel_id": o.ChannelId,
"root_id": o.RootId,
"original_id": o.OriginalId,
"type": o.Type,
"props": o.GetProps(),
"file_ids": o.FileIds,
"pending_post_id": o.PendingPostId,
"remote_id": o.RemoteId,
"reply_count": o.ReplyCount,
"last_reply_at": o.LastReplyAt,
"is_following": o.IsFollowing,
"metadata": o.Metadata,
}
}
type PostEphemeral struct {
UserID string `json:"user_id"`
Post *Post `json:"post"`
}
type PostPatch struct {
IsPinned *bool `json:"is_pinned"`
Message *string `json:"message"`
Props *StringInterface `json:"props"`
FileIds *StringArray `json:"file_ids"`
HasReactions *bool `json:"has_reactions"`
}
type PostReminder struct {
TargetTime int64 `json:"target_time"`
// These fields are only used internally for interacting with DB.
PostId string `json:",omitempty"`
UserId string `json:",omitempty"`
}
type PostPriority struct {
Priority *string `json:"priority"`
RequestedAck *bool `json:"requested_ack"`
PersistentNotifications *bool `json:"persistent_notifications"`
// These fields are only used internally for interacting with DB.
PostId string `json:",omitempty"`
ChannelId string `json:",omitempty"`
}
type SearchParameter struct {
Terms *string `json:"terms"`
IsOrSearch *bool `json:"is_or_search"`
TimeZoneOffset *int `json:"time_zone_offset"`
Page *int `json:"page"`
PerPage *int `json:"per_page"`
IncludeDeletedChannels *bool `json:"include_deleted_channels"`
Modifier *string `json:"modifier"` // whether it's messages or file
}
type AnalyticsPostCountsOptions struct {
TeamId string
BotsOnly bool
YesterdayOnly bool
}
func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
copy := *o
if copy.Message != nil {
*copy.Message = RewriteImageURLs(*o.Message, f)
}
return ©
}
func (o *PostPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"is_pinned": o.IsPinned,
"props": o.Props,
"file_ids": o.FileIds,
"has_reactions": o.HasReactions,
}
}
type PostForExport struct {
Post
TeamName string
ChannelName string
Username string
ReplyCount int
}
type DirectPostForExport struct {
Post
User string
ChannelMembers *[]string
}
type ReplyForExport struct {
Post
Username string
}
type PostForIndexing struct {
Post
TeamId string `json:"team_id"`
ParentCreateAt *int64 `json:"parent_create_at"`
}
type FileForIndexing struct {
FileInfo
ChannelId string `json:"channel_id"`
Content string `json:"content"`
}
// ShallowCopy is an utility function to shallow copy a Post to the given
// destination without touching the internal RWMutex.
func (o *Post) ShallowCopy(dst *Post) error {
if dst == nil {
return errors.New("dst cannot be nil")
}
o.propsMu.RLock()
defer o.propsMu.RUnlock()
dst.propsMu.Lock()
defer dst.propsMu.Unlock()
dst.Id = o.Id
dst.CreateAt = o.CreateAt
dst.UpdateAt = o.UpdateAt
dst.EditAt = o.EditAt
dst.DeleteAt = o.DeleteAt
dst.IsPinned = o.IsPinned
dst.UserId = o.UserId
dst.ChannelId = o.ChannelId
dst.RootId = o.RootId
dst.OriginalId = o.OriginalId
dst.Message = o.Message
dst.MessageSource = o.MessageSource
dst.Type = o.Type
dst.Props = o.Props
dst.Hashtags = o.Hashtags
dst.Filenames = o.Filenames
dst.FileIds = o.FileIds
dst.PendingPostId = o.PendingPostId
dst.HasReactions = o.HasReactions
dst.ReplyCount = o.ReplyCount
dst.Participants = o.Participants
dst.LastReplyAt = o.LastReplyAt
dst.Metadata = o.Metadata
if o.IsFollowing != nil {
dst.IsFollowing = NewBool(*o.IsFollowing)
}
dst.RemoteId = o.RemoteId
return nil
}
// Clone shallowly copies the post and returns the copy.
func (o *Post) Clone() *Post {
copy := &Post{}
o.ShallowCopy(copy)
return copy
}
func (o *Post) ToJSON() (string, error) {
copy := o.Clone()
copy.StripActionIntegrations()
b, err := json.Marshal(copy)
return string(b), err
}
func (o *Post) EncodeJSON(w io.Writer) error {
o.StripActionIntegrations()
return json.NewEncoder(w).Encode(o)
}
type GetPostsSinceOptions struct {
UserId string
ChannelId string
Time int64
SkipFetchThreads bool
CollapsedThreads bool
CollapsedThreadsExtended bool
SortAscending bool
}
type GetPostsSinceForSyncCursor struct {
LastPostUpdateAt int64
LastPostId string
}
type GetPostsSinceForSyncOptions struct {
ChannelId string
ExcludeRemoteId string
IncludeDeleted bool
}
type GetPostsOptions struct {
UserId string
ChannelId string
PostId string
Page int
PerPage int
SkipFetchThreads bool
CollapsedThreads bool
CollapsedThreadsExtended bool
FromPost string // PostId after which to send the items
FromCreateAt int64 // CreateAt after which to send the items
Direction string // Only accepts up|down. Indicates the order in which to send the items.
IncludeDeleted bool
IncludePostPriority bool
}
type PostCountOptions struct {
// Only include posts on a specific team. "" for any team.
TeamId string
MustHaveFile bool
MustHaveHashtag bool
ExcludeDeleted bool
ExcludeSystemPosts bool
UsersPostsOnly bool
// AllowFromCache looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
AllowFromCache bool
SincePostID string
SinceUpdateAt int64
}
func (o *Post) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Post) IsValid(maxPostSize int) *AppError {
if !IsValidId(o.Id) {
return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.ChannelId) {
return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if !(IsValidId(o.RootId) || o.RootId == "") {
return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
}
if !(len(o.OriginalId) == 26 || o.OriginalId == "") {
return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Message) > maxPostSize {
return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Hashtags) > PostHashtagsMaxRunes {
return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
switch o.Type {
case
PostTypeDefault,
PostTypeSystemGeneric,
PostTypeJoinLeave,
PostTypeAutoResponder,
PostTypeAddRemove,
PostTypeJoinChannel,
PostTypeGuestJoinChannel,
PostTypeLeaveChannel,
PostTypeJoinTeam,
PostTypeLeaveTeam,
PostTypeAddToChannel,
PostTypeAddGuestToChannel,
PostTypeRemoveFromChannel,
PostTypeMoveChannel,
PostTypeAddToTeam,
PostTypeRemoveFromTeam,
PostTypeSlackAttachment,
PostTypeHeaderChange,
PostTypePurposeChange,
PostTypeDisplaynameChange,
PostTypeConvertChannel,
PostTypeChannelDeleted,
PostTypeChannelRestored,
PostTypeChangeChannelPrivacy,
PostTypeAddBotTeamsChannels,
PostTypeSystemWarnMetricStatus,
PostTypeWelcomePost,
PostTypeReminder,
PostTypeMe:
default:
if !strings.HasPrefix(o.Type, PostCustomTypePrefix) {
return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
}
}
if utf8.RuneCountInString(ArrayToJSON(o.Filenames)) > PostFilenamesMaxRunes {
return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(ArrayToJSON(o.FileIds)) > PostFileidsMaxRunes {
return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(StringInterfaceToJSON(o.GetProps())) > PostPropsMaxRunes {
return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
return nil
}
func (o *Post) SanitizeProps() {
if o == nil {
return
}
membersToSanitize := []string{
PropsAddChannelMember,
}
for _, member := range membersToSanitize {
if _, ok := o.GetProps()[member]; ok {
o.DelProp(member)
}
}
for _, p := range o.Participants {
p.Sanitize(map[string]bool{})
}
}
func (o *Post) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.OriginalId = ""
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = o.CreateAt
o.PreCommit()
}
func (o *Post) PreCommit() {
if o.GetProps() == nil {
o.SetProps(make(map[string]any))
}
if o.Filenames == nil {
o.Filenames = []string{}
}
if o.FileIds == nil {
o.FileIds = []string{}
}
o.GenerateActionIds()
// There's a rare bug where the client sends up duplicate FileIds so protect against that
o.FileIds = RemoveDuplicateStrings(o.FileIds)
}
func (o *Post) MakeNonNil() {
if o.GetProps() == nil {
o.SetProps(make(map[string]any))
}
}
func (o *Post) DelProp(key string) {
o.propsMu.Lock()
defer o.propsMu.Unlock()
propsCopy := make(map[string]any, len(o.Props)-1)
for k, v := range o.Props {
propsCopy[k] = v
}
delete(propsCopy, key)
o.Props = propsCopy
}
func (o *Post) AddProp(key string, value any) {
o.propsMu.Lock()
defer o.propsMu.Unlock()
propsCopy := make(map[string]any, len(o.Props)+1)
for k, v := range o.Props {
propsCopy[k] = v
}
propsCopy[key] = value
o.Props = propsCopy
}
func (o *Post) GetProps() StringInterface {
o.propsMu.RLock()
defer o.propsMu.RUnlock()
return o.Props
}
func (o *Post) SetProps(props StringInterface) {
o.propsMu.Lock()
defer o.propsMu.Unlock()
o.Props = props
}
func (o *Post) GetProp(key string) any {
o.propsMu.RLock()
defer o.propsMu.RUnlock()
return o.Props[key]
}
func (o *Post) IsSystemMessage() bool {
return len(o.Type) >= len(PostSystemMessagePrefix) && o.Type[:len(PostSystemMessagePrefix)] == PostSystemMessagePrefix
}
// IsRemote returns true if the post originated on a remote cluster.
func (o *Post) IsRemote() bool {
return o.RemoteId != nil && *o.RemoteId != ""
}
// GetRemoteID safely returns the remoteID or empty string if not remote.
func (o *Post) GetRemoteID() string {
if o.RemoteId != nil {
return *o.RemoteId
}
return ""
}
func (o *Post) IsJoinLeaveMessage() bool {
return o.Type == PostTypeJoinLeave ||
o.Type == PostTypeAddRemove ||
o.Type == PostTypeJoinChannel ||
o.Type == PostTypeLeaveChannel ||
o.Type == PostTypeJoinTeam ||
o.Type == PostTypeLeaveTeam ||
o.Type == PostTypeAddToChannel ||
o.Type == PostTypeRemoveFromChannel ||
o.Type == PostTypeAddToTeam ||
o.Type == PostTypeRemoveFromTeam
}
func (o *Post) Patch(patch *PostPatch) {
if patch.IsPinned != nil {
o.IsPinned = *patch.IsPinned
}
if patch.Message != nil {
o.Message = *patch.Message
}
if patch.Props != nil {
newProps := *patch.Props
o.SetProps(newProps)
}
if patch.FileIds != nil {
o.FileIds = *patch.FileIds
}
if patch.HasReactions != nil {
o.HasReactions = *patch.HasReactions
}
}
func (o *Post) ChannelMentions() []string {
return ChannelMentions(o.Message)
}
// DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message.
func (o *Post) DisableMentionHighlights() string {
mention, hasMentions := findAtChannelMention(o.Message)
if hasMentions {
o.AddProp(PostPropsMentionHighlightDisabled, true)
}
return mention
}
// DisableMentionHighlights disables mention highlighting for a post patch if required.
func (o *PostPatch) DisableMentionHighlights() {
if o.Message == nil {
return
}
if _, hasMentions := findAtChannelMention(*o.Message); hasMentions {
if o.Props == nil {
o.Props = &StringInterface{}
}
(*o.Props)[PostPropsMentionHighlightDisabled] = true
}
}
func findAtChannelMention(message string) (mention string, found bool) {
re := regexp.MustCompile(`(?i)\B@(channel|all|here)\b`)
matched := re.FindStringSubmatch(message)
if found = (len(matched) > 0); found {
mention = strings.ToLower(matched[0])
}
return
}
func (o *Post) Attachments() []*SlackAttachment {
if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
return attachments
}
var ret []*SlackAttachment
if attachments, ok := o.GetProp("attachments").([]any); ok {
for _, attachment := range attachments {
if enc, err := json.Marshal(attachment); err == nil {
var decoded SlackAttachment
if json.Unmarshal(enc, &decoded) == nil {
// Ignoring nil actions
i := 0
for _, action := range decoded.Actions {
if action != nil {
decoded.Actions[i] = action
i++
}
}
decoded.Actions = decoded.Actions[:i]
// Ignoring nil fields
i = 0
for _, field := range decoded.Fields {
if field != nil {
decoded.Fields[i] = field
i++
}
}
decoded.Fields = decoded.Fields[:i]
ret = append(ret, &decoded)
}
}
}
}
return ret
}
func (o *Post) AttachmentsEqual(input *Post) bool {
attachments := o.Attachments()
inputAttachments := input.Attachments()
if len(attachments) != len(inputAttachments) {
return false
}
for i := range attachments {
if !attachments[i].Equals(inputAttachments[i]) {
return false
}
}
return true
}
var markdownDestinationEscaper = strings.NewReplacer(
`\`, `\\`,
`<`, `\<`,
`>`, `\>`,
`(`, `\(`,
`)`, `\)`,
)
// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
// rewritten via RewriteImageURLs.
func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
copy := o.Clone()
copy.Message = RewriteImageURLs(o.Message, f)
if copy.MessageSource == "" && copy.Message != o.Message {
copy.MessageSource = o.Message
}
return copy
}
// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
// according to the function f. For each image URL, f will be invoked, and the resulting markdown
// will contain the URL returned by that invocation instead.
//
// Image URLs are destination URLs used in inline images or reference definitions that are used
// anywhere in the input markdown as an image.
func RewriteImageURLs(message string, f func(string) string) string {
if !strings.Contains(message, "![") {
return message
}
var ranges []markdown.Range
markdown.Inspect(message, func(blockOrInline any) bool {
switch v := blockOrInline.(type) {
case *markdown.ReferenceImage:
ranges = append(ranges, v.ReferenceDefinition.RawDestination)
case *markdown.InlineImage:
ranges = append(ranges, v.RawDestination)
default:
return true
}
return true
})
if ranges == nil {
return message
}
sort.Slice(ranges, func(i, j int) bool {
return ranges[i].Position < ranges[j].Position
})
copyRanges := make([]markdown.Range, 0, len(ranges))
urls := make([]string, 0, len(ranges))
resultLength := len(message)
start := 0
for i, r := range ranges {
switch {
case i == 0:
case r.Position != ranges[i-1].Position:
start = ranges[i-1].End
default:
continue
}
original := message[r.Position:r.End]
replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
resultLength += len(replacement) - len(original)
copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
urls = append(urls, replacement)
}
result := make([]byte, resultLength)
offset := 0
for i, r := range copyRanges {
offset += copy(result[offset:], message[r.Position:r.End])
offset += copy(result[offset:], urls[i])
}
copy(result[offset:], message[ranges[len(ranges)-1].End:])
return string(result)
}
func (o *Post) IsFromOAuthBot() bool {
props := o.GetProps()
return props["from_webhook"] == "true" && props["override_username"] != ""
}
func (o *Post) ToNilIfInvalid() *Post {
if o.Id == "" {
return nil
}
return o
}
func (o *Post) ForPlugin() *Post {
p := o.Clone()
p.Metadata = nil
if p.Type == fmt.Sprintf("%sup_notification", PostCustomTypePrefix) {
p.DelProp("requested_features")
}
return p
}
func (o *Post) GetPreviewPost() *PreviewPost {
for _, embed := range o.Metadata.Embeds {
if embed.Type == PostEmbedPermalink {
if previewPost, ok := embed.Data.(*PreviewPost); ok {
return previewPost
}
}
}
return nil
}
func (o *Post) GetPreviewedPostProp() string {
if val, ok := o.GetProp(PostPropsPreviewedPost).(string); ok {
return val
}
return ""
}
func (o *Post) GetPriority() *PostPriority {
if o.Metadata != nil && o.Metadata.Priority != nil {
return o.Metadata.Priority
}
return nil
}
func (o *Post) IsUrgent() bool {
postPriority := o.GetPriority()
if postPriority == nil {
return false
}
return *postPriority.Priority == PostPriorityUrgent
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "net/http"
type PostAcknowledgement struct {
UserId string `json:"user_id"`
PostId string `json:"post_id"`
AcknowledgedAt int64 `json:"acknowledged_at"`
}
func (o *PostAcknowledgement) IsValid() *AppError {
if !IsValidId(o.UserId) {
return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.user_id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest)
}
if !IsValidId(o.PostId) {
return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.post_id.app_error", nil, "post_id="+o.PostId, http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"sort"
)
type PostList struct {
Order []string `json:"order"`
Posts map[string]*Post `json:"posts"`
NextPostId string `json:"next_post_id"`
PrevPostId string `json:"prev_post_id"`
// HasNext indicates whether there are more items to be fetched or not.
HasNext bool `json:"has_next"`
// If there are inaccessible posts, FirstInaccessiblePostTime is the time of the latest inaccessible post
FirstInaccessiblePostTime int64 `json:"first_inaccessible_post_time"`
}
func NewPostList() *PostList {
return &PostList{
Order: make([]string, 0),
Posts: make(map[string]*Post),
NextPostId: "",
PrevPostId: "",
}
}
func (o *PostList) Clone() *PostList {
orderCopy := make([]string, len(o.Order))
postsCopy := make(map[string]*Post)
copy(orderCopy, o.Order)
for k, v := range o.Posts {
postsCopy[k] = v.Clone()
}
return &PostList{
Order: orderCopy,
Posts: postsCopy,
NextPostId: o.NextPostId,
PrevPostId: o.PrevPostId,
HasNext: o.HasNext,
FirstInaccessiblePostTime: o.FirstInaccessiblePostTime,
}
}
func (o *PostList) ForPlugin() *PostList {
copy := o.Clone()
for k, p := range copy.Posts {
copy.Posts[k] = p.ForPlugin()
}
return copy
}
func (o *PostList) ToSlice() []*Post {
var posts []*Post
if l := len(o.Posts); l > 0 {
posts = make([]*Post, 0, l)
}
for _, id := range o.Order {
posts = append(posts, o.Posts[id])
}
return posts
}
func (o *PostList) WithRewrittenImageURLs(f func(string) string) *PostList {
copy := *o
copy.Posts = make(map[string]*Post)
for id, post := range o.Posts {
copy.Posts[id] = post.WithRewrittenImageURLs(f)
}
return ©
}
func (o *PostList) StripActionIntegrations() {
posts := o.Posts
o.Posts = make(map[string]*Post)
for id, post := range posts {
pcopy := post.Clone()
pcopy.StripActionIntegrations()
o.Posts[id] = pcopy
}
}
func (o *PostList) ToJSON() (string, error) {
copy := *o
copy.StripActionIntegrations()
b, err := json.Marshal(©)
return string(b), err
}
func (o *PostList) EncodeJSON(w io.Writer) error {
o.StripActionIntegrations()
return json.NewEncoder(w).Encode(o)
}
func (o *PostList) MakeNonNil() {
if o.Order == nil {
o.Order = make([]string, 0)
}
if o.Posts == nil {
o.Posts = make(map[string]*Post)
}
for _, v := range o.Posts {
v.MakeNonNil()
}
}
func (o *PostList) AddOrder(id string) {
if o.Order == nil {
o.Order = make([]string, 0, 128)
}
o.Order = append(o.Order, id)
}
func (o *PostList) AddPost(post *Post) {
if o.Posts == nil {
o.Posts = make(map[string]*Post)
}
o.Posts[post.Id] = post
}
func (o *PostList) UniqueOrder() {
keys := make(map[string]bool)
order := []string{}
for _, postId := range o.Order {
if _, value := keys[postId]; !value {
keys[postId] = true
order = append(order, postId)
}
}
o.Order = order
}
func (o *PostList) Extend(other *PostList) {
for postId := range other.Posts {
o.AddPost(other.Posts[postId])
}
for _, postId := range other.Order {
o.AddOrder(postId)
}
o.UniqueOrder()
}
func (o *PostList) SortByCreateAt() {
sort.Slice(o.Order, func(i, j int) bool {
return o.Posts[o.Order[i]].CreateAt > o.Posts[o.Order[j]].CreateAt
})
}
func (o *PostList) Etag() string {
id := "0"
var t int64 = 0
for _, v := range o.Posts {
if v.UpdateAt > t {
t = v.UpdateAt
id = v.Id
} else if v.UpdateAt == t && v.Id > id {
t = v.UpdateAt
id = v.Id
}
}
orderId := ""
if len(o.Order) > 0 {
orderId = o.Order[0]
}
return Etag(orderId, id, t)
}
func (o *PostList) IsChannelId(channelId string) bool {
for _, v := range o.Posts {
if v.ChannelId != channelId {
return false
}
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type PostMetadata struct {
// Embeds holds information required to render content embedded in the post. This includes the OpenGraph metadata
// for links in the post.
Embeds []*PostEmbed `json:"embeds,omitempty"`
// Emojis holds all custom emojis used in the post or used in reaction to the post.
Emojis []*Emoji `json:"emojis,omitempty"`
// Files holds information about the file attachments on the post.
Files []*FileInfo `json:"files,omitempty"`
// Images holds the dimensions of all external images in the post as a map of the image URL to its dimensions.
// This includes image embeds (when the message contains a plaintext link to an image), Markdown images, images
// contained in the OpenGraph metadata, and images contained in message attachments. It does not contain
// the dimensions of any file attachments as those are stored in FileInfos.
Images map[string]*PostImage `json:"images,omitempty"`
// Reactions holds reactions made to the post.
Reactions []*Reaction `json:"reactions,omitempty"`
// Priority holds info about priority settings for the post.
Priority *PostPriority `json:"priority,omitempty"`
// Acknowledgements holds acknowledgements made by users to the post
Acknowledgements []*PostAcknowledgement `json:"acknowledgements,omitempty"`
}
type PostImage struct {
Width int `json:"width"`
Height int `json:"height"`
// Format is the name of the image format as used by image/go such as "png", "gif", or "jpeg".
Format string `json:"format"`
// FrameCount stores the number of frames in this image, if it is an animated gif. It will be 0 for other formats.
FrameCount int `json:"frame_count"`
}
// Copy does a deep copy
func (p *PostMetadata) Copy() *PostMetadata {
embedsCopy := make([]*PostEmbed, len(p.Embeds))
copy(embedsCopy, p.Embeds)
emojisCopy := make([]*Emoji, len(p.Emojis))
copy(emojisCopy, p.Emojis)
filesCopy := make([]*FileInfo, len(p.Files))
copy(filesCopy, p.Files)
imagesCopy := map[string]*PostImage{}
for k, v := range p.Images {
imagesCopy[k] = v
}
reactionsCopy := make([]*Reaction, len(p.Reactions))
copy(reactionsCopy, p.Reactions)
acknowledgementsCopy := make([]*PostAcknowledgement, len(p.Acknowledgements))
copy(acknowledgementsCopy, p.Acknowledgements)
var postPriorityCopy *PostPriority
if p.Priority != nil {
postPriorityCopy = &PostPriority{
Priority: p.Priority.Priority,
RequestedAck: p.Priority.RequestedAck,
PersistentNotifications: p.Priority.PersistentNotifications,
PostId: p.Priority.PostId,
ChannelId: p.Priority.ChannelId,
}
}
return &PostMetadata{
Embeds: embedsCopy,
Emojis: emojisCopy,
Files: filesCopy,
Images: imagesCopy,
Reactions: reactionsCopy,
Priority: postPriorityCopy,
Acknowledgements: acknowledgementsCopy,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
type PostSearchMatches map[string][]string
type PostSearchResults struct {
*PostList
Matches PostSearchMatches `json:"matches"`
}
func MakePostSearchResults(posts *PostList, matches PostSearchMatches) *PostSearchResults {
return &PostSearchResults{
posts,
matches,
}
}
func (o *PostSearchResults) ToJSON() (string, error) {
copy := *o
copy.PostList.StripActionIntegrations()
b, err := json.Marshal(©)
return string(b), err
}
func (o *PostSearchResults) EncodeJSON(w io.Writer) error {
o.PostList.StripActionIntegrations()
return json.NewEncoder(w).Encode(o)
}
func (o *PostSearchResults) ForPlugin() *PostSearchResults {
copy := *o
copy.PostList = copy.PostList.ForPlugin()
return ©
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"net/http"
"regexp"
"strings"
"unicode/utf8"
)
const (
PreferenceCategoryDirectChannelShow = "direct_channel_show"
PreferenceCategoryGroupChannelShow = "group_channel_show"
PreferenceCategoryTutorialSteps = "tutorial_step"
PreferenceCategoryAdvancedSettings = "advanced_settings"
PreferenceCategoryFlaggedPost = "flagged_post"
PreferenceCategoryFavoriteChannel = "favorite_channel"
PreferenceCategorySidebarSettings = "sidebar_settings"
PreferenceCategoryInsights = "insights"
PreferenceCategoryDisplaySettings = "display_settings"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
PreferenceNameChannelDisplayMode = "channel_display_mode"
PreferenceNameCollapseSetting = "collapse_previews"
PreferenceNameMessageDisplay = "message_display"
PreferenceNameCollapseConsecutive = "collapse_consecutive_messages"
PreferenceNameColorizeUsernames = "colorize_usernames"
PreferenceNameNameFormat = "name_format"
PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceRecommendedNextSteps = "recommended_next_steps"
PreferenceNameInsights = "insights_tutorial_state"
// initial onboarding preferences
PreferenceOnboarding = "onboarding"
PreferenceCategoryTheme = "theme"
// the name for theme props is the team id
PreferenceCategoryAuthorizedOAuthApp = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope
PreferenceCategoryLast = "last"
PreferenceNameLastChannel = "channel"
PreferenceNameLastTeam = "team"
PreferenceCategoryCustomStatus = "custom_status"
PreferenceNameRecentCustomStatuses = "recent_custom_statuses"
PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state"
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed"
PreferenceCategoryNotifications = "notifications"
PreferenceNameEmailInterval = "email_interval"
PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s
PreferenceEmailIntervalBatchingSeconds = "900" // fifteen minutes is 900 seconds
PreferenceEmailIntervalImmediately = "immediately"
PreferenceEmailIntervalFifteen = "fifteen"
PreferenceEmailIntervalFifteenAsSeconds = "900"
PreferenceEmailIntervalHour = "hour"
PreferenceEmailIntervalHourAsSeconds = "3600"
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
)
type Preference struct {
UserId string `json:"user_id"`
Category string `json:"category"`
Name string `json:"name"`
Value string `json:"value"`
}
type Preferences []Preference
func (o *Preference) IsValid() *AppError {
if !IsValidId(o.UserId) {
return NewAppError("Preference.IsValid", "model.preference.is_valid.id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest)
}
if o.Category == "" || len(o.Category) > 32 {
return NewAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category, http.StatusBadRequest)
}
if len(o.Name) > 32 {
return NewAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Value) > 2000 {
return NewAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value, http.StatusBadRequest)
}
if o.Category == PreferenceCategoryTheme {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
return NewAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value, http.StatusBadRequest).Wrap(err)
}
}
return nil
}
var preUpdateColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
func (o *Preference) PreUpdate() {
if o.Category == PreferenceCategoryTheme {
// decode the value of theme (a map of strings to string) and eliminate any invalid values
var props map[string]string
// just continue, the invalid preference value should get caught by IsValid before saving
json.NewDecoder(strings.NewReader(o.Value)).Decode(&props)
// blank out any invalid theme values
for name, value := range props {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !preUpdateColorPattern.MatchString(value) {
props[name] = "#ffffff"
}
}
if b, err := json.Marshal(props); err == nil {
o.Value = string(b)
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"github.com/pkg/errors"
)
type ProductNotices []ProductNotice
func (r *ProductNotices) Marshal() ([]byte, error) {
return json.Marshal(r)
}
func UnmarshalProductNotices(data []byte) (ProductNotices, error) {
var r ProductNotices
err := json.Unmarshal(data, &r)
return r, err
}
// List of product notices. Order is important and is used to resolve priorities.
// Each notice will only be show if conditions are met.
type ProductNotice struct {
Conditions Conditions `json:"conditions"`
ID string `json:"id"` // Unique identifier for this notice. Can be a running number. Used for storing 'viewed'; state on the server.
LocalizedMessages map[string]NoticeMessageInternal `json:"localizedMessages"` // Notice message data, organized by locale.; Example:; "localizedMessages": {; "en": { "title": "English", description: "English description"},; "frFR": { "title": "Frances", description: "French description"}; }
Repeatable *bool `json:"repeatable,omitempty"` // Configurable flag if the notice should reappear after it’s seen and dismissed
}
func (n *ProductNotice) SysAdminOnly() bool {
return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudienceSysadmin
}
func (n *ProductNotice) TeamAdminOnly() bool {
return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudienceTeamAdmin
}
type Conditions struct {
Audience *NoticeAudience `json:"audience,omitempty"`
ClientType *NoticeClientType `json:"clientType,omitempty"` // Only show the notice on specific clients. Defaults to 'all'
DesktopVersion []string `json:"desktopVersion,omitempty"` // What desktop client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
DisplayDate *string `json:"displayDate,omitempty"` // When to display the notice.; Examples:; "2020-03-01T00:00:00Z" - show on specified date; ">= 2020-03-01T00:00:00Z" - show after specified date; "< 2020-03-01T00:00:00Z" - show before the specified date; "> 2020-03-01T00:00:00Z <= 2020-04-01T00:00:00Z" - show only between the specified dates
InstanceType *NoticeInstanceType `json:"instanceType,omitempty"`
MobileVersion []string `json:"mobileVersion,omitempty"` // What mobile client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
NumberOfPosts *int64 `json:"numberOfPosts,omitempty"` // Only show the notice when server has more than specified number of posts
NumberOfUsers *int64 `json:"numberOfUsers,omitempty"` // Only show the notice when server has more than specified number of users
ServerConfig map[string]any `json:"serverConfig,omitempty"` // Map of mattermost server config paths and their values. Notice will be displayed only if; the values match the target server config; Example: serverConfig: { "PluginSettings.Enable": true, "GuestAccountsSettings.Enable":; false }
ServerVersion []string `json:"serverVersion,omitempty"` // What server versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
Sku *NoticeSKU `json:"sku,omitempty"`
UserConfig map[string]any `json:"userConfig,omitempty"` // Map of user's settings and their values. Notice will be displayed only if the values; match the viewing users' config; Example: userConfig: { "new_sidebar.disabled": true }
DeprecatingDependency *ExternalDependency `json:"deprecating_dependency,omitempty"` // External dependency which is going to be deprecated
}
type NoticeMessageInternal struct {
Action *NoticeAction `json:"action,omitempty"` // Optional action to perform on action button click. (defaults to closing the notice)
ActionParam *string `json:"actionParam,omitempty"` // Optional action parameter.; Example: {"action": "url", actionParam: "/console/some-page"}
ActionText *string `json:"actionText,omitempty"` // Optional override for the action button text (defaults to OK)
Description string `json:"description"` // Notice content. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown.
Image *string `json:"image,omitempty"`
Title string `json:"title"` // Notice title. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown.
}
type NoticeMessages []NoticeMessage
type NoticeMessage struct {
NoticeMessageInternal
ID string `json:"id"`
SysAdminOnly bool `json:"sysAdminOnly"`
TeamAdminOnly bool `json:"teamAdminOnly"`
}
func (r *NoticeMessages) Marshal() ([]byte, error) {
return json.Marshal(r)
}
func UnmarshalProductNoticeMessages(data io.Reader) (NoticeMessages, error) {
var r NoticeMessages
err := json.NewDecoder(data).Decode(&r)
return r, err
}
// User role, i.e. who will see the notice. Defaults to "all"
type NoticeAudience string
func NewNoticeAudience(s NoticeAudience) *NoticeAudience {
return &s
}
func (a *NoticeAudience) Matches(sysAdmin bool, teamAdmin bool) bool {
switch *a {
case NoticeAudienceAll:
return true
case NoticeAudienceMember:
return !sysAdmin && !teamAdmin
case NoticeAudienceSysadmin:
return sysAdmin
case NoticeAudienceTeamAdmin:
return teamAdmin
}
return false
}
const (
NoticeAudienceAll NoticeAudience = "all"
NoticeAudienceMember NoticeAudience = "member"
NoticeAudienceSysadmin NoticeAudience = "sysadmin"
NoticeAudienceTeamAdmin NoticeAudience = "teamadmin"
)
// Only show the notice on specific clients. Defaults to 'all'
//
// Client type. Defaults to "all"
type NoticeClientType string
func NewNoticeClientType(s NoticeClientType) *NoticeClientType { return &s }
func (c *NoticeClientType) Matches(other NoticeClientType) bool {
switch *c {
case NoticeClientTypeAll:
return true
case NoticeClientTypeMobile:
return other == NoticeClientTypeMobileIos || other == NoticeClientTypeMobileAndroid
default:
return *c == other
}
}
const (
NoticeClientTypeAll NoticeClientType = "all"
NoticeClientTypeDesktop NoticeClientType = "desktop"
NoticeClientTypeMobile NoticeClientType = "mobile"
NoticeClientTypeMobileAndroid NoticeClientType = "mobile-android"
NoticeClientTypeMobileIos NoticeClientType = "mobile-ios"
NoticeClientTypeWeb NoticeClientType = "web"
)
func NoticeClientTypeFromString(s string) (NoticeClientType, error) {
switch s {
case "web":
return NoticeClientTypeWeb, nil
case "mobile-ios":
return NoticeClientTypeMobileIos, nil
case "mobile-android":
return NoticeClientTypeMobileAndroid, nil
case "desktop":
return NoticeClientTypeDesktop, nil
}
return NoticeClientTypeAll, errors.New("Invalid client type supplied")
}
// Instance type. Defaults to "both"
type NoticeInstanceType string
func NewNoticeInstanceType(n NoticeInstanceType) *NoticeInstanceType { return &n }
func (t *NoticeInstanceType) Matches(isCloud bool) bool {
if *t == NoticeInstanceTypeBoth {
return true
}
if *t == NoticeInstanceTypeCloud && !isCloud {
return false
}
if *t == NoticeInstanceTypeOnPrem && isCloud {
return false
}
return true
}
const (
NoticeInstanceTypeBoth NoticeInstanceType = "both"
NoticeInstanceTypeCloud NoticeInstanceType = "cloud"
NoticeInstanceTypeOnPrem NoticeInstanceType = "onprem"
)
// SKU. Defaults to "all"
type NoticeSKU string
func NewNoticeSKU(s NoticeSKU) *NoticeSKU { return &s }
func (c *NoticeSKU) Matches(s string) bool {
switch *c {
case NoticeSKUAll:
return true
case NoticeSKUE0, NoticeSKUTeam:
return s == ""
default:
return s == string(*c)
}
}
const (
NoticeSKUE0 NoticeSKU = "e0"
NoticeSKUE10 NoticeSKU = "e10"
NoticeSKUE20 NoticeSKU = "e20"
NoticeSKUAll NoticeSKU = "all"
NoticeSKUTeam NoticeSKU = "team"
)
// Optional action to perform on action button click. (defaults to closing the notice)
//
// Possible actions to execute on button press
type NoticeAction string
const (
URL NoticeAction = "url"
)
// Definition of the table keeping the 'viewed' state of each in-product notice per user
type ProductNoticeViewState struct {
UserId string
NoticeId string
Viewed int32
Timestamp int64
}
type ExternalDependency struct {
Name string `json:"name"`
MinimumVersion string `json:"minimum_version"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"strings"
)
const (
PushNotifyApple = "apple"
PushNotifyAndroid = "android"
PushNotifyAppleReactNative = "apple_rn"
PushNotifyAndroidReactNative = "android_rn"
PushTypeMessage = "message"
PushTypeClear = "clear"
PushTypeUpdateBadge = "update_badge"
PushTypeSession = "session"
PushTypeTest = "test"
PushMessageV2 = "v2"
PushSoundNone = "none"
// The category is set to handle a set of interactive Actions
// with the push notifications
CategoryCanReply = "CAN_REPLY"
MHPNS = "https://push.mattermost.com"
PushSendPrepare = "Prepared to send"
PushSendSuccess = "Successful"
PushNotSent = "Not Sent due to preferences"
PushReceived = "Received by device"
)
type PushNotificationAck struct {
Id string `json:"id"`
ClientReceivedAt int64 `json:"received_at"`
ClientPlatform string `json:"platform"`
NotificationType string `json:"type"`
PostId string `json:"post_id,omitempty"`
IsIdLoaded bool `json:"is_id_loaded"`
}
type PushNotification struct {
AckId string `json:"ack_id"`
Platform string `json:"platform"`
ServerId string `json:"server_id"`
DeviceId string `json:"device_id"`
PostId string `json:"post_id"`
Category string `json:"category,omitempty"`
Sound string `json:"sound,omitempty"`
Message string `json:"message,omitempty"`
Badge int `json:"badge,omitempty"`
ContentAvailable int `json:"cont_ava,omitempty"`
TeamId string `json:"team_id,omitempty"`
ChannelId string `json:"channel_id,omitempty"`
RootId string `json:"root_id,omitempty"`
ChannelName string `json:"channel_name,omitempty"`
Type string `json:"type,omitempty"`
SenderId string `json:"sender_id,omitempty"`
SenderName string `json:"sender_name,omitempty"`
OverrideUsername string `json:"override_username,omitempty"`
OverrideIconURL string `json:"override_icon_url,omitempty"`
FromWebhook string `json:"from_webhook,omitempty"`
Version string `json:"version,omitempty"`
IsCRTEnabled bool `json:"is_crt_enabled"`
IsIdLoaded bool `json:"is_id_loaded"`
}
func (pn *PushNotification) DeepCopy() *PushNotification {
copy := *pn
return ©
}
func (pn *PushNotification) SetDeviceIdAndPlatform(deviceId string) {
index := strings.Index(deviceId, ":")
if index > -1 {
pn.Platform = deviceId[:index]
pn.DeviceId = deviceId[index+1:]
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
PushStatus = "status"
PushStatusOk = "OK"
PushStatusFail = "FAIL"
PushStatusRemove = "REMOVE"
PushStatusErrorMsg = "error"
)
type PushResponse map[string]string
func NewOkPushResponse() PushResponse {
m := make(map[string]string)
m[PushStatus] = PushStatusOk
return m
}
func NewRemovePushResponse() PushResponse {
m := make(map[string]string)
m[PushStatus] = PushStatusRemove
return m
}
func NewErrorPushResponse(message string) PushResponse {
m := make(map[string]string)
m[PushStatus] = PushStatusFail
m[PushStatusErrorMsg] = message
return m
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"regexp"
)
type Reaction struct {
UserId string `json:"user_id"`
PostId string `json:"post_id"`
EmojiName string `json:"emoji_name"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
RemoteId *string `json:"remote_id"`
ChannelId string `json:"channel_id"`
}
func (o *Reaction) IsValid() *AppError {
if !IsValidId(o.UserId) {
return NewAppError("Reaction.IsValid", "model.reaction.is_valid.user_id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest)
}
if !IsValidId(o.PostId) {
return NewAppError("Reaction.IsValid", "model.reaction.is_valid.post_id.app_error", nil, "post_id="+o.PostId, http.StatusBadRequest)
}
validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
if o.EmojiName == "" || len(o.EmojiName) > EmojiNameMaxLength || !validName.MatchString(o.EmojiName) {
return NewAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName, http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Reaction.IsValid", "model.reaction.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Reaction.IsValid", "model.reaction.is_valid.update_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (o *Reaction) PreSave() {
if o.CreateAt == 0 {
o.CreateAt = GetMillis()
}
o.UpdateAt = GetMillis()
o.DeleteAt = 0
if o.RemoteId == nil {
o.RemoteId = NewString("")
}
}
func (o *Reaction) PreUpdate() {
o.UpdateAt = GetMillis()
if o.RemoteId == nil {
o.RemoteId = NewString("")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"io"
"net/http"
"regexp"
"strings"
"golang.org/x/crypto/scrypt"
)
const (
RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes
RemoteNameMinLength = 1
RemoteNameMaxLength = 64
)
var (
validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`)
)
type RemoteCluster struct {
RemoteId string `json:"remote_id"`
RemoteTeamId string `json:"remote_team_id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
SiteURL string `json:"site_url"`
CreateAt int64 `json:"create_at"`
LastPingAt int64 `json:"last_ping_at"`
Token string `json:"token"`
RemoteToken string `json:"remote_token"`
Topics string `json:"topics"`
CreatorId string `json:"creator_id"`
}
func (rc *RemoteCluster) Auditable() map[string]interface{} {
return map[string]interface{}{
"remote_id": rc.RemoteId,
"remote_team_id": rc.RemoteTeamId,
"name": rc.Name,
"display_name": rc.DisplayName,
"site_url": rc.SiteURL,
"create_at": rc.CreateAt,
"last_ping_at": rc.LastPingAt,
"creator_id": rc.CreatorId,
}
}
func (rc *RemoteCluster) PreSave() {
if rc.RemoteId == "" {
rc.RemoteId = NewId()
}
if rc.DisplayName == "" {
rc.DisplayName = rc.Name
}
rc.Name = SanitizeUnicode(rc.Name)
rc.DisplayName = SanitizeUnicode(rc.DisplayName)
rc.Name = NormalizeRemoteName(rc.Name)
if rc.Token == "" {
rc.Token = NewId()
}
if rc.CreateAt == 0 {
rc.CreateAt = GetMillis()
}
rc.fixTopics()
}
func (rc *RemoteCluster) IsValid() *AppError {
if !IsValidId(rc.RemoteId) {
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "id="+rc.RemoteId, http.StatusBadRequest)
}
if !IsValidRemoteName(rc.Name) {
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "name="+rc.Name, http.StatusBadRequest)
}
if rc.CreateAt == 0 {
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "create_at=0", http.StatusBadRequest)
}
if !IsValidId(rc.CreatorId) {
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "creator_id="+rc.CreatorId, http.StatusBadRequest)
}
return nil
}
func IsValidRemoteName(s string) bool {
if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength {
return false
}
return validRemoteNameChars.MatchString(s)
}
func (rc *RemoteCluster) PreUpdate() {
if rc.DisplayName == "" {
rc.DisplayName = rc.Name
}
rc.Name = SanitizeUnicode(rc.Name)
rc.DisplayName = SanitizeUnicode(rc.DisplayName)
rc.Name = NormalizeRemoteName(rc.Name)
rc.fixTopics()
}
func (rc *RemoteCluster) IsOnline() bool {
return rc.LastPingAt > GetMillis()-RemoteOfflineAfterMillis
}
// fixTopics ensures all topics are separated by one, and only one, space.
func (rc *RemoteCluster) fixTopics() {
trimmed := strings.TrimSpace(rc.Topics)
if trimmed == "" || trimmed == "*" {
rc.Topics = trimmed
return
}
var sb strings.Builder
sb.WriteString(" ")
ss := strings.Split(rc.Topics, " ")
for _, c := range ss {
cc := strings.TrimSpace(c)
if cc != "" {
sb.WriteString(cc)
sb.WriteString(" ")
}
}
rc.Topics = sb.String()
}
func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo {
return RemoteClusterInfo{
Name: rc.Name,
DisplayName: rc.DisplayName,
CreateAt: rc.CreateAt,
LastPingAt: rc.LastPingAt,
}
}
func NormalizeRemoteName(name string) string {
return strings.ToLower(name)
}
// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients.
type RemoteClusterInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
CreateAt int64 `json:"create_at"`
LastPingAt int64 `json:"last_ping_at"`
}
// RemoteClusterFrame wraps a `RemoteClusterMsg` with credentials specific to a remote cluster.
type RemoteClusterFrame struct {
RemoteId string `json:"remote_id"`
Msg RemoteClusterMsg `json:"msg"`
}
func (f *RemoteClusterFrame) Auditable() map[string]interface{} {
return map[string]interface{}{
"remote_id": f.RemoteId,
"msg": f.Msg,
}
}
func (f *RemoteClusterFrame) IsValid() *AppError {
if !IsValidId(f.RemoteId) {
return NewAppError("RemoteClusterFrame.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "RemoteId="+f.RemoteId, http.StatusBadRequest)
}
if appErr := f.Msg.IsValid(); appErr != nil {
return appErr
}
return nil
}
// RemoteClusterMsg represents a message that is sent and received between clusters.
// These are processed and routed via the RemoteClusters service.
type RemoteClusterMsg struct {
Id string `json:"id"`
Topic string `json:"topic"`
CreateAt int64 `json:"create_at"`
Payload json.RawMessage `json:"payload"`
}
func NewRemoteClusterMsg(topic string, payload json.RawMessage) RemoteClusterMsg {
return RemoteClusterMsg{
Id: NewId(),
Topic: topic,
CreateAt: GetMillis(),
Payload: payload,
}
}
func (m RemoteClusterMsg) IsValid() *AppError {
if !IsValidId(m.Id) {
return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "Id="+m.Id, http.StatusBadRequest)
}
if m.Topic == "" {
return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_topic.app_error", nil, "Topic empty", http.StatusBadRequest)
}
if len(m.Payload) == 0 {
return NewAppError("RemoteClusterMsg.IsValid", "api.context.invalid_body_param.app_error", map[string]any{"Name": "PayLoad"}, "", http.StatusBadRequest)
}
return nil
}
// RemoteClusterPing represents a ping that is sent and received between clusters
// to indicate a connection is alive. This is the payload for a `RemoteClusterMsg`.
type RemoteClusterPing struct {
SentAt int64 `json:"sent_at"`
RecvAt int64 `json:"recv_at"`
}
// RemoteClusterInvite represents an invitation to establish a simple trust with a remote cluster.
type RemoteClusterInvite struct {
RemoteId string `json:"remote_id"`
RemoteTeamId string `json:"remote_team_id"`
SiteURL string `json:"site_url"`
Token string `json:"token"`
}
func (rci *RemoteClusterInvite) Encrypt(password string) ([]byte, error) {
raw, err := json.Marshal(&rci)
if err != nil {
return nil, err
}
// create random salt to be prepended to the blob.
salt := make([]byte, 16)
if _, err = io.ReadFull(rand.Reader, salt); err != nil {
return nil, err
}
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// create random nonce
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// prefix the nonce to the cyphertext so we don't need to keep track of it.
sealed := gcm.Seal(nonce, nonce, raw, nil)
return append(salt, sealed...), nil
}
func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error {
if len(encrypted) <= 16 {
return errors.New("invalid length")
}
// first 16 bytes is the salt that was used to derive a key
salt := encrypted[:16]
encrypted = encrypted[16:]
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return err
}
block, err := aes.NewCipher(key[:])
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
// nonce was prefixed to the cyphertext when encrypting so we need to extract it.
nonceSize := gcm.NonceSize()
nonce, cyphertext := encrypted[:nonceSize], encrypted[nonceSize:]
plain, err := gcm.Open(nil, nonce, cyphertext, nil)
if err != nil {
return err
}
// try to unmarshall the decrypted JSON to this invite struct.
return json.Unmarshal(plain, &rci)
}
// RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll
type RemoteClusterQueryFilter struct {
ExcludeOffline bool
InChannel string
NotInChannel string
Topic string
CreatorId string
OnlyConfirmed bool
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"strings"
)
// SysconsoleAncillaryPermissions maps the non-sysconsole permissions required by each sysconsole view.
var SysconsoleAncillaryPermissions map[string][]*Permission
var SystemManagerDefaultPermissions []string
var SystemUserManagerDefaultPermissions []string
var SystemReadOnlyAdminDefaultPermissions []string
var SystemCustomGroupAdminDefaultPermissions []string
var BuiltInSchemeManagedRoleIDs []string
var NewSystemRoleIDs []string
func init() {
NewSystemRoleIDs = []string{
SystemUserManagerRoleId,
SystemReadOnlyAdminRoleId,
SystemManagerRoleId,
}
BuiltInSchemeManagedRoleIDs = append([]string{
SystemGuestRoleId,
SystemUserRoleId,
SystemAdminRoleId,
SystemPostAllRoleId,
SystemPostAllPublicRoleId,
SystemUserAccessTokenRoleId,
TeamGuestRoleId,
TeamUserRoleId,
TeamAdminRoleId,
TeamPostAllRoleId,
TeamPostAllPublicRoleId,
ChannelGuestRoleId,
ChannelUserRoleId,
ChannelAdminRoleId,
CustomGroupUserRoleId,
PlaybookAdminRoleId,
PlaybookMemberRoleId,
RunAdminRoleId,
RunMemberRoleId,
}, NewSystemRoleIDs...)
// When updating the values here, the values in mattermost-redux must also be updated.
SysconsoleAncillaryPermissions = map[string][]*Permission{
PermissionSysconsoleReadAboutEditionAndLicense.Id: {
PermissionReadLicenseInformation,
},
PermissionSysconsoleWriteAboutEditionAndLicense.Id: {
PermissionManageLicenseInformation,
},
PermissionSysconsoleReadUserManagementChannels.Id: {
PermissionReadPublicChannel,
PermissionReadChannel,
PermissionReadPublicChannelGroups,
PermissionReadPrivateChannelGroups,
},
PermissionSysconsoleReadUserManagementUsers.Id: {
PermissionReadOtherUsersTeams,
PermissionGetAnalytics,
},
PermissionSysconsoleReadUserManagementTeams.Id: {
PermissionListPrivateTeams,
PermissionListPublicTeams,
PermissionViewTeam,
},
PermissionSysconsoleReadEnvironmentElasticsearch.Id: {
PermissionReadElasticsearchPostIndexingJob,
PermissionReadElasticsearchPostAggregationJob,
},
PermissionSysconsoleWriteEnvironmentWebServer.Id: {
PermissionTestSiteURL,
PermissionReloadConfig,
PermissionInvalidateCaches,
},
PermissionSysconsoleWriteEnvironmentDatabase.Id: {
PermissionRecycleDatabaseConnections,
},
PermissionSysconsoleWriteEnvironmentElasticsearch.Id: {
PermissionTestElasticsearch,
PermissionCreateElasticsearchPostIndexingJob,
PermissionCreateElasticsearchPostAggregationJob,
PermissionPurgeElasticsearchIndexes,
},
PermissionSysconsoleWriteEnvironmentFileStorage.Id: {
PermissionTestS3,
},
PermissionSysconsoleWriteEnvironmentSMTP.Id: {
PermissionTestEmail,
},
PermissionSysconsoleReadReportingServerLogs.Id: {
PermissionGetLogs,
},
PermissionSysconsoleReadReportingSiteStatistics.Id: {
PermissionGetAnalytics,
},
PermissionSysconsoleReadReportingTeamStatistics.Id: {
PermissionViewTeam,
},
PermissionSysconsoleWriteUserManagementUsers.Id: {
PermissionEditOtherUsers,
PermissionDemoteToGuest,
PermissionPromoteGuest,
},
PermissionSysconsoleWriteUserManagementChannels.Id: {
PermissionManageTeam,
PermissionManagePublicChannelProperties,
PermissionManagePrivateChannelProperties,
PermissionManagePrivateChannelMembers,
PermissionManagePublicChannelMembers,
PermissionDeletePrivateChannel,
PermissionDeletePublicChannel,
PermissionManageChannelRoles,
PermissionConvertPublicChannelToPrivate,
PermissionConvertPrivateChannelToPublic,
},
PermissionSysconsoleWriteUserManagementTeams.Id: {
PermissionManageTeam,
PermissionManageTeamRoles,
PermissionRemoveUserFromTeam,
PermissionJoinPrivateTeams,
PermissionJoinPublicTeams,
PermissionAddUserToTeam,
},
PermissionSysconsoleWriteUserManagementGroups.Id: {
PermissionManageTeam,
PermissionManagePrivateChannelMembers,
PermissionManagePublicChannelMembers,
PermissionConvertPublicChannelToPrivate,
PermissionConvertPrivateChannelToPublic,
},
PermissionSysconsoleWriteSiteCustomization.Id: {
PermissionEditBrand,
},
PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id: {
PermissionCreateDataRetentionJob,
},
PermissionSysconsoleReadComplianceDataRetentionPolicy.Id: {
PermissionReadDataRetentionJob,
},
PermissionSysconsoleWriteComplianceComplianceExport.Id: {
PermissionCreateComplianceExportJob,
PermissionDownloadComplianceExportResult,
},
PermissionSysconsoleReadComplianceComplianceExport.Id: {
PermissionReadComplianceExportJob,
PermissionDownloadComplianceExportResult,
},
PermissionSysconsoleReadComplianceCustomTermsOfService.Id: {
PermissionReadAudits,
},
PermissionSysconsoleWriteExperimentalBleve.Id: {
PermissionCreatePostBleveIndexesJob,
PermissionPurgeBleveIndexes,
},
PermissionSysconsoleWriteAuthenticationLdap.Id: {
PermissionCreateLdapSyncJob,
PermissionAddLdapPublicCert,
PermissionRemoveLdapPublicCert,
PermissionAddLdapPrivateCert,
PermissionRemoveLdapPrivateCert,
},
PermissionSysconsoleReadAuthenticationLdap.Id: {
PermissionTestLdap,
PermissionReadLdapSyncJob,
},
PermissionSysconsoleWriteAuthenticationEmail.Id: {
PermissionInvalidateEmailInvite,
},
PermissionSysconsoleWriteAuthenticationSaml.Id: {
PermissionGetSamlMetadataFromIdp,
PermissionAddSamlPublicCert,
PermissionAddSamlPrivateCert,
PermissionAddSamlIdpCert,
PermissionRemoveSamlPublicCert,
PermissionRemoveSamlPrivateCert,
PermissionRemoveSamlIdpCert,
PermissionGetSamlCertStatus,
},
}
SystemUserManagerDefaultPermissions = []string{
PermissionSysconsoleReadUserManagementGroups.Id,
PermissionSysconsoleReadUserManagementTeams.Id,
PermissionSysconsoleReadUserManagementChannels.Id,
PermissionSysconsoleReadUserManagementPermissions.Id,
PermissionSysconsoleWriteUserManagementGroups.Id,
PermissionSysconsoleWriteUserManagementTeams.Id,
PermissionSysconsoleWriteUserManagementChannels.Id,
PermissionSysconsoleReadAuthenticationSignup.Id,
PermissionSysconsoleReadAuthenticationEmail.Id,
PermissionSysconsoleReadAuthenticationPassword.Id,
PermissionSysconsoleReadAuthenticationMfa.Id,
PermissionSysconsoleReadAuthenticationLdap.Id,
PermissionSysconsoleReadAuthenticationSaml.Id,
PermissionSysconsoleReadAuthenticationOpenid.Id,
PermissionSysconsoleReadAuthenticationGuestAccess.Id,
}
SystemReadOnlyAdminDefaultPermissions = []string{
PermissionSysconsoleReadAboutEditionAndLicense.Id,
PermissionSysconsoleReadReportingSiteStatistics.Id,
PermissionSysconsoleReadReportingTeamStatistics.Id,
PermissionSysconsoleReadReportingServerLogs.Id,
PermissionSysconsoleReadUserManagementUsers.Id,
PermissionSysconsoleReadUserManagementGroups.Id,
PermissionSysconsoleReadUserManagementTeams.Id,
PermissionSysconsoleReadUserManagementChannels.Id,
PermissionSysconsoleReadUserManagementPermissions.Id,
PermissionSysconsoleReadEnvironmentWebServer.Id,
PermissionSysconsoleReadEnvironmentDatabase.Id,
PermissionSysconsoleReadEnvironmentElasticsearch.Id,
PermissionSysconsoleReadEnvironmentFileStorage.Id,
PermissionSysconsoleReadEnvironmentImageProxy.Id,
PermissionSysconsoleReadEnvironmentSMTP.Id,
PermissionSysconsoleReadEnvironmentPushNotificationServer.Id,
PermissionSysconsoleReadEnvironmentHighAvailability.Id,
PermissionSysconsoleReadEnvironmentRateLimiting.Id,
PermissionSysconsoleReadEnvironmentLogging.Id,
PermissionSysconsoleReadEnvironmentSessionLengths.Id,
PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id,
PermissionSysconsoleReadEnvironmentDeveloper.Id,
PermissionSysconsoleReadSiteCustomization.Id,
PermissionSysconsoleReadSiteLocalization.Id,
PermissionSysconsoleReadSiteUsersAndTeams.Id,
PermissionSysconsoleReadSiteNotifications.Id,
PermissionSysconsoleReadSiteAnnouncementBanner.Id,
PermissionSysconsoleReadSiteEmoji.Id,
PermissionSysconsoleReadSitePosts.Id,
PermissionSysconsoleReadSiteFileSharingAndDownloads.Id,
PermissionSysconsoleReadSitePublicLinks.Id,
PermissionSysconsoleReadSiteNotices.Id,
PermissionSysconsoleReadAuthenticationSignup.Id,
PermissionSysconsoleReadAuthenticationEmail.Id,
PermissionSysconsoleReadAuthenticationPassword.Id,
PermissionSysconsoleReadAuthenticationMfa.Id,
PermissionSysconsoleReadAuthenticationLdap.Id,
PermissionSysconsoleReadAuthenticationSaml.Id,
PermissionSysconsoleReadAuthenticationOpenid.Id,
PermissionSysconsoleReadAuthenticationGuestAccess.Id,
PermissionSysconsoleReadPlugins.Id,
PermissionSysconsoleReadIntegrationsIntegrationManagement.Id,
PermissionSysconsoleReadIntegrationsBotAccounts.Id,
PermissionSysconsoleReadIntegrationsGif.Id,
PermissionSysconsoleReadIntegrationsCors.Id,
PermissionSysconsoleReadComplianceDataRetentionPolicy.Id,
PermissionSysconsoleReadComplianceComplianceExport.Id,
PermissionSysconsoleReadComplianceComplianceMonitoring.Id,
PermissionSysconsoleReadComplianceCustomTermsOfService.Id,
PermissionSysconsoleReadExperimentalFeatures.Id,
PermissionSysconsoleReadExperimentalFeatureFlags.Id,
PermissionSysconsoleReadExperimentalBleve.Id,
PermissionSysconsoleReadProductsBoards.Id,
}
SystemManagerDefaultPermissions = []string{
PermissionSysconsoleReadAboutEditionAndLicense.Id,
PermissionSysconsoleReadReportingSiteStatistics.Id,
PermissionSysconsoleReadReportingTeamStatistics.Id,
PermissionSysconsoleReadReportingServerLogs.Id,
PermissionSysconsoleReadUserManagementGroups.Id,
PermissionSysconsoleReadUserManagementTeams.Id,
PermissionSysconsoleReadUserManagementChannels.Id,
PermissionSysconsoleReadUserManagementPermissions.Id,
PermissionSysconsoleWriteUserManagementGroups.Id,
PermissionSysconsoleWriteUserManagementTeams.Id,
PermissionSysconsoleWriteUserManagementChannels.Id,
PermissionSysconsoleWriteUserManagementPermissions.Id,
PermissionSysconsoleReadEnvironmentWebServer.Id,
PermissionSysconsoleReadEnvironmentDatabase.Id,
PermissionSysconsoleReadEnvironmentElasticsearch.Id,
PermissionSysconsoleReadEnvironmentFileStorage.Id,
PermissionSysconsoleReadEnvironmentImageProxy.Id,
PermissionSysconsoleReadEnvironmentSMTP.Id,
PermissionSysconsoleReadEnvironmentPushNotificationServer.Id,
PermissionSysconsoleReadEnvironmentHighAvailability.Id,
PermissionSysconsoleReadEnvironmentRateLimiting.Id,
PermissionSysconsoleReadEnvironmentLogging.Id,
PermissionSysconsoleReadEnvironmentSessionLengths.Id,
PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id,
PermissionSysconsoleReadEnvironmentDeveloper.Id,
PermissionSysconsoleWriteEnvironmentWebServer.Id,
PermissionSysconsoleWriteEnvironmentDatabase.Id,
PermissionSysconsoleWriteEnvironmentElasticsearch.Id,
PermissionSysconsoleWriteEnvironmentFileStorage.Id,
PermissionSysconsoleWriteEnvironmentImageProxy.Id,
PermissionSysconsoleWriteEnvironmentSMTP.Id,
PermissionSysconsoleWriteEnvironmentPushNotificationServer.Id,
PermissionSysconsoleWriteEnvironmentHighAvailability.Id,
PermissionSysconsoleWriteEnvironmentRateLimiting.Id,
PermissionSysconsoleWriteEnvironmentLogging.Id,
PermissionSysconsoleWriteEnvironmentSessionLengths.Id,
PermissionSysconsoleWriteEnvironmentPerformanceMonitoring.Id,
PermissionSysconsoleWriteEnvironmentDeveloper.Id,
PermissionSysconsoleReadSiteCustomization.Id,
PermissionSysconsoleWriteSiteCustomization.Id,
PermissionSysconsoleReadSiteLocalization.Id,
PermissionSysconsoleWriteSiteLocalization.Id,
PermissionSysconsoleReadSiteUsersAndTeams.Id,
PermissionSysconsoleWriteSiteUsersAndTeams.Id,
PermissionSysconsoleReadSiteNotifications.Id,
PermissionSysconsoleWriteSiteNotifications.Id,
PermissionSysconsoleReadSiteAnnouncementBanner.Id,
PermissionSysconsoleWriteSiteAnnouncementBanner.Id,
PermissionSysconsoleReadSiteEmoji.Id,
PermissionSysconsoleWriteSiteEmoji.Id,
PermissionSysconsoleReadSitePosts.Id,
PermissionSysconsoleWriteSitePosts.Id,
PermissionSysconsoleReadSiteFileSharingAndDownloads.Id,
PermissionSysconsoleWriteSiteFileSharingAndDownloads.Id,
PermissionSysconsoleReadSitePublicLinks.Id,
PermissionSysconsoleWriteSitePublicLinks.Id,
PermissionSysconsoleReadSiteNotices.Id,
PermissionSysconsoleWriteSiteNotices.Id,
PermissionSysconsoleReadAuthenticationSignup.Id,
PermissionSysconsoleReadAuthenticationEmail.Id,
PermissionSysconsoleReadAuthenticationPassword.Id,
PermissionSysconsoleReadAuthenticationMfa.Id,
PermissionSysconsoleReadAuthenticationLdap.Id,
PermissionSysconsoleReadAuthenticationSaml.Id,
PermissionSysconsoleReadAuthenticationOpenid.Id,
PermissionSysconsoleReadAuthenticationGuestAccess.Id,
PermissionSysconsoleReadPlugins.Id,
PermissionSysconsoleReadIntegrationsIntegrationManagement.Id,
PermissionSysconsoleReadIntegrationsBotAccounts.Id,
PermissionSysconsoleReadIntegrationsGif.Id,
PermissionSysconsoleReadIntegrationsCors.Id,
PermissionSysconsoleWriteIntegrationsIntegrationManagement.Id,
PermissionSysconsoleWriteIntegrationsBotAccounts.Id,
PermissionSysconsoleWriteIntegrationsGif.Id,
PermissionSysconsoleWriteIntegrationsCors.Id,
PermissionSysconsoleReadProductsBoards.Id,
PermissionSysconsoleWriteProductsBoards.Id,
}
SystemCustomGroupAdminDefaultPermissions = []string{
PermissionCreateCustomGroup.Id,
PermissionEditCustomGroup.Id,
PermissionDeleteCustomGroup.Id,
PermissionRestoreCustomGroup.Id,
PermissionManageCustomGroupMembers.Id,
}
// Add the ancillary permissions to each system role
SystemUserManagerDefaultPermissions = AddAncillaryPermissions(SystemUserManagerDefaultPermissions)
SystemReadOnlyAdminDefaultPermissions = AddAncillaryPermissions(SystemReadOnlyAdminDefaultPermissions)
SystemManagerDefaultPermissions = AddAncillaryPermissions(SystemManagerDefaultPermissions)
SystemCustomGroupAdminDefaultPermissions = AddAncillaryPermissions(SystemCustomGroupAdminDefaultPermissions)
}
type RoleType string
type RoleScope string
const (
SystemGuestRoleId = "system_guest"
SystemUserRoleId = "system_user"
SystemAdminRoleId = "system_admin"
SystemPostAllRoleId = "system_post_all"
SystemPostAllPublicRoleId = "system_post_all_public"
SystemUserAccessTokenRoleId = "system_user_access_token"
SystemUserManagerRoleId = "system_user_manager"
SystemReadOnlyAdminRoleId = "system_read_only_admin"
SystemManagerRoleId = "system_manager"
SystemCustomGroupAdminRoleId = "system_custom_group_admin"
TeamGuestRoleId = "team_guest"
TeamUserRoleId = "team_user"
TeamAdminRoleId = "team_admin"
TeamPostAllRoleId = "team_post_all"
TeamPostAllPublicRoleId = "team_post_all_public"
ChannelGuestRoleId = "channel_guest"
ChannelUserRoleId = "channel_user"
ChannelAdminRoleId = "channel_admin"
CustomGroupUserRoleId = "custom_group_user"
PlaybookAdminRoleId = "playbook_admin"
PlaybookMemberRoleId = "playbook_member"
RunAdminRoleId = "run_admin"
RunMemberRoleId = "run_member"
RoleNameMaxLength = 64
RoleDisplayNameMaxLength = 128
RoleDescriptionMaxLength = 1024
RoleScopeSystem RoleScope = "System"
RoleScopeTeam RoleScope = "Team"
RoleScopeChannel RoleScope = "Channel"
RoleScopeGroup RoleScope = "Group"
RoleTypeGuest RoleType = "Guest"
RoleTypeUser RoleType = "User"
RoleTypeAdmin RoleType = "Admin"
)
type Role struct {
Id string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Permissions []string `json:"permissions"`
SchemeManaged bool `json:"scheme_managed"`
BuiltIn bool `json:"built_in"`
}
func (r *Role) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": r.Id,
"name": r.Name,
"display_name": r.DisplayName,
"description": r.Description,
"create_at": r.CreateAt,
"update_at": r.UpdateAt,
"delete_at": r.DeleteAt,
"permissions": r.Permissions,
"scheme_managed": r.SchemeManaged,
"built_in": r.BuiltIn,
}
}
type RolePatch struct {
Permissions *[]string `json:"permissions"`
}
func (r *RolePatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"permissions": r.Permissions,
}
}
type RolePermissions struct {
RoleID string
Permissions []string
}
func (r *Role) Patch(patch *RolePatch) {
if patch.Permissions != nil {
r.Permissions = *patch.Permissions
}
}
func (r *Role) CreateAt_() float64 {
return float64(r.CreateAt)
}
func (r *Role) UpdateAt_() float64 {
return float64(r.UpdateAt)
}
func (r *Role) DeleteAt_() float64 {
return float64(r.DeleteAt)
}
// MergeChannelHigherScopedPermissions is meant to be invoked on a channel scheme's role and merges the higher-scoped
// channel role's permissions.
func (r *Role) MergeChannelHigherScopedPermissions(higherScopedPermissions *RolePermissions) {
mergedPermissions := []string{}
higherScopedPermissionsMap := asStringBoolMap(higherScopedPermissions.Permissions)
rolePermissionsMap := asStringBoolMap(r.Permissions)
for _, cp := range AllPermissions {
if cp.Scope != PermissionScopeChannel {
continue
}
_, presentOnHigherScope := higherScopedPermissionsMap[cp.Id]
// For the channel admin role always look to the higher scope to determine if the role has their permission.
// The channel admin is a special case because they're not part of the UI to be "channel moderated", only
// channel members and channel guests are.
if higherScopedPermissions.RoleID == ChannelAdminRoleId && presentOnHigherScope {
mergedPermissions = append(mergedPermissions, cp.Id)
continue
}
_, permissionIsModerated := ChannelModeratedPermissionsMap[cp.Id]
if permissionIsModerated {
_, presentOnRole := rolePermissionsMap[cp.Id]
if presentOnRole && presentOnHigherScope {
mergedPermissions = append(mergedPermissions, cp.Id)
}
} else {
if presentOnHigherScope {
mergedPermissions = append(mergedPermissions, cp.Id)
}
}
}
r.Permissions = mergedPermissions
}
// Returns an array of permissions that are in either role.Permissions
// or patch.Permissions, but not both.
func PermissionsChangedByPatch(role *Role, patch *RolePatch) []string {
var result []string
if patch.Permissions == nil {
return result
}
roleMap := make(map[string]bool)
patchMap := make(map[string]bool)
for _, permission := range role.Permissions {
roleMap[permission] = true
}
for _, permission := range *patch.Permissions {
patchMap[permission] = true
}
for _, permission := range role.Permissions {
if !patchMap[permission] {
result = append(result, permission)
}
}
for _, permission := range *patch.Permissions {
if !roleMap[permission] {
result = append(result, permission)
}
}
return result
}
func ChannelModeratedPermissionsChangedByPatch(role *Role, patch *RolePatch) []string {
var result []string
if role == nil {
return result
}
if patch.Permissions == nil {
return result
}
roleMap := make(map[string]bool)
patchMap := make(map[string]bool)
for _, permission := range role.Permissions {
if channelModeratedPermissionName, found := ChannelModeratedPermissionsMap[permission]; found {
roleMap[channelModeratedPermissionName] = true
}
}
for _, permission := range *patch.Permissions {
if channelModeratedPermissionName, found := ChannelModeratedPermissionsMap[permission]; found {
patchMap[channelModeratedPermissionName] = true
}
}
for permissionKey := range roleMap {
if !patchMap[permissionKey] {
result = append(result, permissionKey)
}
}
for permissionKey := range patchMap {
if !roleMap[permissionKey] {
result = append(result, permissionKey)
}
}
return result
}
// GetChannelModeratedPermissions returns a map of channel moderated permissions that the role has access to
func (r *Role) GetChannelModeratedPermissions(channelType ChannelType) map[string]bool {
moderatedPermissions := make(map[string]bool)
for _, permission := range r.Permissions {
if _, found := ChannelModeratedPermissionsMap[permission]; !found {
continue
}
for moderated, moderatedPermissionValue := range ChannelModeratedPermissionsMap {
// the moderated permission has already been found to be true so skip this iteration
if moderatedPermissions[moderatedPermissionValue] {
continue
}
if moderated == permission {
// Special case where the channel moderated permission for `manage_members` is different depending on whether the channel is private or public
if moderated == PermissionManagePublicChannelMembers.Id || moderated == PermissionManagePrivateChannelMembers.Id {
canManagePublic := channelType == ChannelTypeOpen && moderated == PermissionManagePublicChannelMembers.Id
canManagePrivate := channelType == ChannelTypePrivate && moderated == PermissionManagePrivateChannelMembers.Id
moderatedPermissions[moderatedPermissionValue] = canManagePublic || canManagePrivate
} else {
moderatedPermissions[moderatedPermissionValue] = true
}
}
}
}
return moderatedPermissions
}
// RolePatchFromChannelModerationsPatch Creates and returns a RolePatch based on a slice of ChannelModerationPatches, roleName is expected to be either "members" or "guests".
func (r *Role) RolePatchFromChannelModerationsPatch(channelModerationsPatch []*ChannelModerationPatch, roleName string) *RolePatch {
permissionsToAddToPatch := make(map[string]bool)
// Iterate through the list of existing permissions on the role and append permissions that we want to keep.
for _, permission := range r.Permissions {
// Permission is not moderated so dont add it to the patch and skip the channelModerationsPatch
if _, isModerated := ChannelModeratedPermissionsMap[permission]; !isModerated {
continue
}
permissionEnabled := true
// Check if permission has a matching moderated permission name inside the channel moderation patch
for _, channelModerationPatch := range channelModerationsPatch {
if *channelModerationPatch.Name == ChannelModeratedPermissionsMap[permission] {
// Permission key exists in patch with a value of false so skip over it
if roleName == "members" {
if channelModerationPatch.Roles.Members != nil && !*channelModerationPatch.Roles.Members {
permissionEnabled = false
}
} else if roleName == "guests" {
if channelModerationPatch.Roles.Guests != nil && !*channelModerationPatch.Roles.Guests {
permissionEnabled = false
}
}
}
}
if permissionEnabled {
permissionsToAddToPatch[permission] = true
}
}
// Iterate through the patch and add any permissions that dont already exist on the role
for _, channelModerationPatch := range channelModerationsPatch {
for permission, moderatedPermissionName := range ChannelModeratedPermissionsMap {
if roleName == "members" && channelModerationPatch.Roles.Members != nil && *channelModerationPatch.Roles.Members && *channelModerationPatch.Name == moderatedPermissionName {
permissionsToAddToPatch[permission] = true
}
if roleName == "guests" && channelModerationPatch.Roles.Guests != nil && *channelModerationPatch.Roles.Guests && *channelModerationPatch.Name == moderatedPermissionName {
permissionsToAddToPatch[permission] = true
}
}
}
patchPermissions := make([]string, 0, len(permissionsToAddToPatch))
for permission := range permissionsToAddToPatch {
patchPermissions = append(patchPermissions, permission)
}
return &RolePatch{Permissions: &patchPermissions}
}
func (r *Role) IsValid() bool {
if !IsValidId(r.Id) {
return false
}
return r.IsValidWithoutId()
}
func (r *Role) IsValidWithoutId() bool {
if !IsValidRoleName(r.Name) {
return false
}
if r.DisplayName == "" || len(r.DisplayName) > RoleDisplayNameMaxLength {
return false
}
if len(r.Description) > RoleDescriptionMaxLength {
return false
}
check := func(perms []*Permission, permission string) bool {
for _, p := range perms {
if permission == p.Id {
return true
}
}
return false
}
for _, permission := range r.Permissions {
permissionValidated := check(AllPermissions, permission) || check(DeprecatedPermissions, permission)
if !permissionValidated {
return false
}
}
return true
}
func CleanRoleNames(roleNames []string) ([]string, bool) {
var cleanedRoleNames []string
for _, roleName := range roleNames {
if strings.TrimSpace(roleName) == "" {
continue
}
if !IsValidRoleName(roleName) {
return roleNames, false
}
cleanedRoleNames = append(cleanedRoleNames, roleName)
}
return cleanedRoleNames, true
}
func IsValidRoleName(roleName string) bool {
if roleName == "" || len(roleName) > RoleNameMaxLength {
return false
}
if strings.TrimLeft(roleName, "abcdefghijklmnopqrstuvwxyz0123456789_") != "" {
return false
}
return true
}
func MakeDefaultRoles() map[string]*Role {
roles := make(map[string]*Role)
roles[CustomGroupUserRoleId] = &Role{
Name: CustomGroupUserRoleId,
DisplayName: fmt.Sprintf("authentication.roles.%s.name", CustomGroupUserRoleId),
Description: fmt.Sprintf("authentication.roles.%s.description", CustomGroupUserRoleId),
Permissions: []string{},
}
roles[ChannelGuestRoleId] = &Role{
Name: "channel_guest",
DisplayName: "authentication.roles.channel_guest.name",
Description: "authentication.roles.channel_guest.description",
Permissions: []string{
PermissionReadChannel.Id,
PermissionAddReaction.Id,
PermissionRemoveReaction.Id,
PermissionUploadFile.Id,
PermissionEditPost.Id,
PermissionCreatePost.Id,
PermissionUseChannelMentions.Id,
PermissionUseSlashCommands.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[ChannelUserRoleId] = &Role{
Name: "channel_user",
DisplayName: "authentication.roles.channel_user.name",
Description: "authentication.roles.channel_user.description",
Permissions: []string{
PermissionReadChannel.Id,
PermissionAddReaction.Id,
PermissionRemoveReaction.Id,
PermissionManagePublicChannelMembers.Id,
PermissionUploadFile.Id,
PermissionGetPublicLink.Id,
PermissionCreatePost.Id,
PermissionUseChannelMentions.Id,
PermissionUseSlashCommands.Id,
PermissionManagePublicChannelProperties.Id,
PermissionDeletePublicChannel.Id,
PermissionManagePrivateChannelProperties.Id,
PermissionDeletePrivateChannel.Id,
PermissionManagePrivateChannelMembers.Id,
PermissionDeletePost.Id,
PermissionEditPost.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[ChannelAdminRoleId] = &Role{
Name: "channel_admin",
DisplayName: "authentication.roles.channel_admin.name",
Description: "authentication.roles.channel_admin.description",
Permissions: []string{
PermissionManageChannelRoles.Id,
PermissionUseGroupMentions.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[TeamGuestRoleId] = &Role{
Name: "team_guest",
DisplayName: "authentication.roles.team_guest.name",
Description: "authentication.roles.team_guest.description",
Permissions: []string{
PermissionViewTeam.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[TeamUserRoleId] = &Role{
Name: "team_user",
DisplayName: "authentication.roles.team_user.name",
Description: "authentication.roles.team_user.description",
Permissions: []string{
PermissionListTeamChannels.Id,
PermissionJoinPublicChannels.Id,
PermissionReadPublicChannel.Id,
PermissionViewTeam.Id,
PermissionCreatePublicChannel.Id,
PermissionCreatePrivateChannel.Id,
PermissionInviteUser.Id,
PermissionAddUserToTeam.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[TeamPostAllRoleId] = &Role{
Name: "team_post_all",
DisplayName: "authentication.roles.team_post_all.name",
Description: "authentication.roles.team_post_all.description",
Permissions: []string{
PermissionCreatePost.Id,
PermissionUseChannelMentions.Id,
},
SchemeManaged: false,
BuiltIn: true,
}
roles[TeamPostAllPublicRoleId] = &Role{
Name: "team_post_all_public",
DisplayName: "authentication.roles.team_post_all_public.name",
Description: "authentication.roles.team_post_all_public.description",
Permissions: []string{
PermissionCreatePostPublic.Id,
PermissionUseChannelMentions.Id,
},
SchemeManaged: false,
BuiltIn: true,
}
roles[TeamAdminRoleId] = &Role{
Name: "team_admin",
DisplayName: "authentication.roles.team_admin.name",
Description: "authentication.roles.team_admin.description",
Permissions: []string{
PermissionRemoveUserFromTeam.Id,
PermissionManageTeam.Id,
PermissionImportTeam.Id,
PermissionManageTeamRoles.Id,
PermissionManageChannelRoles.Id,
PermissionManageOthersIncomingWebhooks.Id,
PermissionManageOthersOutgoingWebhooks.Id,
PermissionManageSlashCommands.Id,
PermissionManageOthersSlashCommands.Id,
PermissionManageIncomingWebhooks.Id,
PermissionManageOutgoingWebhooks.Id,
PermissionConvertPublicChannelToPrivate.Id,
PermissionConvertPrivateChannelToPublic.Id,
PermissionDeletePost.Id,
PermissionDeleteOthersPosts.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[PlaybookAdminRoleId] = &Role{
Name: PlaybookAdminRoleId,
DisplayName: "authentication.roles.playbook_admin.name",
Description: "authentication.roles.playbook_admin.description",
Permissions: []string{
PermissionPublicPlaybookManageMembers.Id,
PermissionPublicPlaybookManageRoles.Id,
PermissionPublicPlaybookManageProperties.Id,
PermissionPrivatePlaybookManageMembers.Id,
PermissionPrivatePlaybookManageRoles.Id,
PermissionPrivatePlaybookManageProperties.Id,
PermissionPublicPlaybookMakePrivate.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[PlaybookMemberRoleId] = &Role{
Name: PlaybookMemberRoleId,
DisplayName: "authentication.roles.playbook_member.name",
Description: "authentication.roles.playbook_member.description",
Permissions: []string{
PermissionPublicPlaybookView.Id,
PermissionPublicPlaybookManageMembers.Id,
PermissionPublicPlaybookManageProperties.Id,
PermissionPrivatePlaybookView.Id,
PermissionPrivatePlaybookManageMembers.Id,
PermissionPrivatePlaybookManageProperties.Id,
PermissionRunCreate.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[RunAdminRoleId] = &Role{
Name: RunAdminRoleId,
DisplayName: "authentication.roles.run_admin.name",
Description: "authentication.roles.run_admin.description",
Permissions: []string{
PermissionRunManageMembers.Id,
PermissionRunManageProperties.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[RunMemberRoleId] = &Role{
Name: RunMemberRoleId,
DisplayName: "authentication.roles.run_member.name",
Description: "authentication.roles.run_member.description",
Permissions: []string{
PermissionRunView.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[SystemGuestRoleId] = &Role{
Name: "system_guest",
DisplayName: "authentication.roles.global_guest.name",
Description: "authentication.roles.global_guest.description",
Permissions: []string{
PermissionCreateDirectChannel.Id,
PermissionCreateGroupChannel.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[SystemUserRoleId] = &Role{
Name: "system_user",
DisplayName: "authentication.roles.global_user.name",
Description: "authentication.roles.global_user.description",
Permissions: []string{
PermissionListPublicTeams.Id,
PermissionJoinPublicTeams.Id,
PermissionCreateDirectChannel.Id,
PermissionCreateGroupChannel.Id,
PermissionViewMembers.Id,
PermissionCreateTeam.Id,
PermissionCreateCustomGroup.Id,
PermissionEditCustomGroup.Id,
PermissionDeleteCustomGroup.Id,
PermissionRestoreCustomGroup.Id,
PermissionManageCustomGroupMembers.Id,
},
SchemeManaged: true,
BuiltIn: true,
}
roles[SystemPostAllRoleId] = &Role{
Name: "system_post_all",
DisplayName: "authentication.roles.system_post_all.name",
Description: "authentication.roles.system_post_all.description",
Permissions: []string{
PermissionCreatePost.Id,
PermissionUseChannelMentions.Id,
},
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemPostAllPublicRoleId] = &Role{
Name: "system_post_all_public",
DisplayName: "authentication.roles.system_post_all_public.name",
Description: "authentication.roles.system_post_all_public.description",
Permissions: []string{
PermissionCreatePostPublic.Id,
PermissionUseChannelMentions.Id,
},
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemUserAccessTokenRoleId] = &Role{
Name: "system_user_access_token",
DisplayName: "authentication.roles.system_user_access_token.name",
Description: "authentication.roles.system_user_access_token.description",
Permissions: []string{
PermissionCreateUserAccessToken.Id,
PermissionReadUserAccessToken.Id,
PermissionRevokeUserAccessToken.Id,
},
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemUserManagerRoleId] = &Role{
Name: "system_user_manager",
DisplayName: "authentication.roles.system_user_manager.name",
Description: "authentication.roles.system_user_manager.description",
Permissions: SystemUserManagerDefaultPermissions,
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemReadOnlyAdminRoleId] = &Role{
Name: "system_read_only_admin",
DisplayName: "authentication.roles.system_read_only_admin.name",
Description: "authentication.roles.system_read_only_admin.description",
Permissions: SystemReadOnlyAdminDefaultPermissions,
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemManagerRoleId] = &Role{
Name: "system_manager",
DisplayName: "authentication.roles.system_manager.name",
Description: "authentication.roles.system_manager.description",
Permissions: SystemManagerDefaultPermissions,
SchemeManaged: false,
BuiltIn: true,
}
roles[SystemCustomGroupAdminRoleId] = &Role{
Name: "system_custom_group_admin",
DisplayName: "authentication.roles.system_custom_group_admin.name",
Description: "authentication.roles.system_custom_group_admin.description",
Permissions: SystemCustomGroupAdminDefaultPermissions,
SchemeManaged: false,
BuiltIn: true,
}
allPermissionIDs := []string{}
for _, permission := range AllPermissions {
allPermissionIDs = append(allPermissionIDs, permission.Id)
}
roles[SystemAdminRoleId] = &Role{
Name: "system_admin",
DisplayName: "authentication.roles.global_admin.name",
Description: "authentication.roles.global_admin.description",
// System admins can do anything channel and team admins can do
// plus everything members of teams and channels can do to all teams
// and channels on the system
Permissions: allPermissionIDs,
SchemeManaged: true,
BuiltIn: true,
}
return roles
}
func AddAncillaryPermissions(permissions []string) []string {
for _, permission := range permissions {
if ancillaryPermissions, ok := SysconsoleAncillaryPermissions[permission]; ok {
for _, ancillaryPermission := range ancillaryPermissions {
permissions = append(permissions, ancillaryPermission.Id)
}
}
}
return permissions
}
func asStringBoolMap(list []string) map[string]bool {
listMap := make(map[string]bool, len(list))
for _, p := range list {
listMap[p] = true
}
return listMap
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"time"
)
type TaskFunc func()
type ScheduledTask struct {
Name string `json:"name"`
Interval time.Duration `json:"interval"`
Recurring bool `json:"recurring"`
function func()
cancel chan struct{}
cancelled chan struct{}
fromNextIntervalTime bool
}
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask {
return createTask(name, function, timeToExecution, false, false)
}
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
return createTask(name, function, interval, true, false)
}
func CreateRecurringTaskFromNextIntervalTime(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
return createTask(name, function, interval, true, true)
}
func createTask(name string, function TaskFunc, interval time.Duration, recurring bool, fromNextIntervalTime bool) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: interval,
Recurring: recurring,
function: function,
cancel: make(chan struct{}),
cancelled: make(chan struct{}),
fromNextIntervalTime: fromNextIntervalTime,
}
go func() {
defer close(task.cancelled)
var firstTick <-chan time.Time
var ticker *time.Ticker
if task.fromNextIntervalTime {
currTime := time.Now()
first := currTime.Truncate(interval)
if first.Before(currTime) {
first = first.Add(interval)
}
firstTick = time.After(time.Until(first))
ticker = &time.Ticker{C: nil}
} else {
firstTick = nil
ticker = time.NewTicker(interval)
}
defer func() {
ticker.Stop()
}()
for {
select {
case <-firstTick:
ticker = time.NewTicker(interval)
function()
case <-ticker.C:
function()
case <-task.cancel:
return
}
if !task.Recurring {
break
}
}
}()
return task
}
func (task *ScheduledTask) Cancel() {
close(task.cancel)
<-task.cancelled
}
func (task *ScheduledTask) String() string {
return fmt.Sprintf(
"%s\nInterval: %s\nRecurring: %t\n",
task.Name,
task.Interval.String(),
task.Recurring,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"regexp"
)
const (
SchemeDisplayNameMaxLength = 128
SchemeNameMaxLength = 64
SchemeDescriptionMaxLength = 1024
SchemeScopeTeam = "team"
SchemeScopeChannel = "channel"
SchemeScopePlaybook = "playbook"
SchemeScopeRun = "run"
)
type Scheme struct {
Id string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Scope string `json:"scope"`
DefaultTeamAdminRole string `json:"default_team_admin_role"`
DefaultTeamUserRole string `json:"default_team_user_role"`
DefaultChannelAdminRole string `json:"default_channel_admin_role"`
DefaultChannelUserRole string `json:"default_channel_user_role"`
DefaultTeamGuestRole string `json:"default_team_guest_role"`
DefaultChannelGuestRole string `json:"default_channel_guest_role"`
DefaultPlaybookAdminRole string `json:"default_playbook_admin_role"`
DefaultPlaybookMemberRole string `json:"default_playbook_member_role"`
DefaultRunAdminRole string `json:"default_run_admin_role"`
DefaultRunMemberRole string `json:"default_run_member_role"`
}
func (scheme *Scheme) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": scheme.Id,
"name": scheme.Name,
"display_name": scheme.DisplayName,
"description": scheme.Description,
"create_at": scheme.CreateAt,
"update_at": scheme.UpdateAt,
"delete_at": scheme.DeleteAt,
"scope": scheme.Scope,
"default_team_admin_role": scheme.DefaultTeamAdminRole,
"default_team_user_role": scheme.DefaultTeamUserRole,
"default_channel_admin_role": scheme.DefaultChannelAdminRole,
"default_channel_user_role": scheme.DefaultChannelUserRole,
"default_team_guest_role": scheme.DefaultTeamGuestRole,
"default_channel_guest_role": scheme.DefaultChannelGuestRole,
"default_playbook_admin_role": scheme.DefaultPlaybookAdminRole,
"default_playbook_member_role": scheme.DefaultPlaybookMemberRole,
"default_run_admin_role": scheme.DefaultRunAdminRole,
"default_run_member_role": scheme.DefaultRunMemberRole,
}
}
type SchemePatch struct {
Name *string `json:"name"`
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
}
func (scheme *SchemePatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"name": scheme.Name,
"display_name": scheme.DisplayName,
"description": scheme.Description,
}
}
type SchemeIDPatch struct {
SchemeID *string `json:"scheme_id"`
}
func (p *SchemeIDPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"scheme_id": p.SchemeID,
}
}
// SchemeConveyor is used for importing and exporting a Scheme and its associated Roles.
type SchemeConveyor struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Scope string `json:"scope"`
TeamAdmin string `json:"default_team_admin_role"`
TeamUser string `json:"default_team_user_role"`
TeamGuest string `json:"default_team_guest_role"`
ChannelAdmin string `json:"default_channel_admin_role"`
ChannelUser string `json:"default_channel_user_role"`
ChannelGuest string `json:"default_channel_guest_role"`
PlaybookAdmin string `json:"default_playbook_admin_role"`
PlaybookMember string `json:"default_playbook_member_role"`
RunAdmin string `json:"default_run_admin_role"`
RunMember string `json:"default_run_member_role"`
Roles []*Role `json:"roles"`
}
func (sc *SchemeConveyor) Scheme() *Scheme {
return &Scheme{
DisplayName: sc.DisplayName,
Name: sc.Name,
Description: sc.Description,
Scope: sc.Scope,
DefaultTeamAdminRole: sc.TeamAdmin,
DefaultTeamUserRole: sc.TeamUser,
DefaultTeamGuestRole: sc.TeamGuest,
DefaultChannelAdminRole: sc.ChannelAdmin,
DefaultChannelUserRole: sc.ChannelUser,
DefaultChannelGuestRole: sc.ChannelGuest,
DefaultPlaybookAdminRole: sc.PlaybookAdmin,
DefaultPlaybookMemberRole: sc.PlaybookMember,
DefaultRunAdminRole: sc.RunAdmin,
DefaultRunMemberRole: sc.RunMember,
}
}
type SchemeRoles struct {
SchemeAdmin bool `json:"scheme_admin"`
SchemeUser bool `json:"scheme_user"`
SchemeGuest bool `json:"scheme_guest"`
}
func (s *SchemeRoles) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
func (scheme *Scheme) IsValid() bool {
if !IsValidId(scheme.Id) {
return false
}
return scheme.IsValidForCreate()
}
func (scheme *Scheme) IsValidForCreate() bool {
if scheme.DisplayName == "" || len(scheme.DisplayName) > SchemeDisplayNameMaxLength {
return false
}
if !IsValidSchemeName(scheme.Name) {
return false
}
if len(scheme.Description) > SchemeDescriptionMaxLength {
return false
}
switch scheme.Scope {
case SchemeScopeTeam, SchemeScopeChannel, SchemeScopePlaybook, SchemeScopeRun:
default:
return false
}
if !IsValidRoleName(scheme.DefaultChannelAdminRole) {
return false
}
if !IsValidRoleName(scheme.DefaultChannelUserRole) {
return false
}
if !IsValidRoleName(scheme.DefaultChannelGuestRole) {
return false
}
if scheme.Scope == SchemeScopeTeam {
if !IsValidRoleName(scheme.DefaultTeamAdminRole) {
return false
}
if !IsValidRoleName(scheme.DefaultTeamUserRole) {
return false
}
if !IsValidRoleName(scheme.DefaultTeamGuestRole) {
return false
}
if !IsValidRoleName(scheme.DefaultPlaybookAdminRole) {
return false
}
if !IsValidRoleName(scheme.DefaultPlaybookMemberRole) {
return false
}
if !IsValidRoleName(scheme.DefaultRunAdminRole) {
return false
}
if !IsValidRoleName(scheme.DefaultRunMemberRole) {
return false
}
}
if scheme.Scope == SchemeScopeChannel {
if scheme.DefaultTeamAdminRole != "" {
return false
}
if scheme.DefaultTeamUserRole != "" {
return false
}
if scheme.DefaultTeamGuestRole != "" {
return false
}
}
return true
}
func (scheme *Scheme) Patch(patch *SchemePatch) {
if patch.DisplayName != nil {
scheme.DisplayName = *patch.DisplayName
}
if patch.Name != nil {
scheme.Name = *patch.Name
}
if patch.Description != nil {
scheme.Description = *patch.Description
}
}
func IsValidSchemeName(name string) bool {
re := regexp.MustCompile(fmt.Sprintf("^[a-z0-9_]{2,%d}$", SchemeNameMaxLength))
return re.MatchString(name)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"regexp"
"strings"
"time"
)
var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`)
var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`)
type SearchParams struct {
Terms string `json:"terms,omitempty"`
ExcludedTerms string `json:"excluded_terms,omitempty"`
IsHashtag bool `json:"ishashtag,omitempty"`
InChannels []string `json:"in_channels,omitempty"`
ExcludedChannels []string `json:"excluded_channels,omitempty"`
FromUsers []string `json:"from_users,omitempty"`
ExcludedUsers []string `json:"excluded_users,omitempty"`
AfterDate string `json:"after_date,omitempty"`
ExcludedAfterDate string `json:"excluded_after_date,omitempty"`
BeforeDate string `json:"before_date,omitempty"`
ExcludedBeforeDate string `json:"excluded_before_date,omitempty"`
Extensions []string `json:"extensions,omitempty"`
ExcludedExtensions []string `json:"excluded_extensions,omitempty"`
OnDate string `json:"on_date,omitempty"`
ExcludedDate string `json:"excluded_date,omitempty"`
OrTerms bool `json:"or_terms,omitempty"`
IncludeDeletedChannels bool `json:"include_deleted_channels,omitempty"`
TimeZoneOffset int `json:"timezone_offset,omitempty"`
// True if this search doesn't originate from a "current user".
SearchWithoutUserId bool `json:"search_without_userid,omitempty"`
Modifier string `json:"modifier"`
}
// Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate
func (p *SearchParams) GetAfterDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.AfterDate))
if err != nil {
date = time.Now()
}
// travel forward 1 day
oneDay := time.Hour * 24
afterDate := date.Add(oneDay)
return GetStartOfDayMillis(afterDate, p.TimeZoneOffset)
}
// Returns the epoch timestamp of the start of the day specified by SearchParams.ExcludedAfterDate
func (p *SearchParams) GetExcludedAfterDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedAfterDate))
if err != nil {
date = time.Now()
}
// travel forward 1 day
oneDay := time.Hour * 24
afterDate := date.Add(oneDay)
return GetStartOfDayMillis(afterDate, p.TimeZoneOffset)
}
// Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate
func (p *SearchParams) GetBeforeDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate))
if err != nil {
return 0
}
// travel back 1 day
oneDay := time.Hour * -24
beforeDate := date.Add(oneDay)
return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset)
}
// Returns the epoch timestamp of the end of the day specified by SearchParams.ExcludedBeforeDate
func (p *SearchParams) GetExcludedBeforeDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedBeforeDate))
if err != nil {
return 0
}
// travel back 1 day
oneDay := time.Hour * -24
beforeDate := date.Add(oneDay)
return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset)
}
// Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate
func (p *SearchParams) GetOnDateMillis() (int64, int64) {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate))
if err != nil {
return 0, 0
}
return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset)
}
// Returns the epoch timestamps of the start and end of the day specified by SearchParams.ExcludedDate
func (p *SearchParams) GetExcludedDateMillis() (int64, int64) {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedDate))
if err != nil {
return 0, 0
}
return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset)
}
var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on", "ext"}
type flag struct {
name string
value string
exclude bool
}
type searchWord struct {
value string
exclude bool
}
func splitWords(text string) []string {
words := []string{}
foundQuote := false
location := 0
for i, char := range text {
if char == '"' {
if foundQuote {
// Grab the quoted section
word := text[location : i+1]
words = append(words, word)
foundQuote = false
location = i + 1
} else {
nextStart := i
if i > 0 && text[i-1] == '-' {
nextStart = i - 1
}
words = append(words, strings.Fields(text[location:nextStart])...)
foundQuote = true
location = nextStart
}
}
}
words = append(words, strings.Fields(text[location:])...)
return words
}
func parseSearchFlags(input []string) ([]searchWord, []flag) {
words := []searchWord{}
flags := []flag{}
skipNextWord := false
for i, word := range input {
if skipNextWord {
skipNextWord = false
continue
}
isFlag := false
if colon := strings.Index(word, ":"); colon != -1 {
var flagName string
var exclude bool
if strings.HasPrefix(word, "-") {
flagName = word[1:colon]
exclude = true
} else {
flagName = word[:colon]
exclude = false
}
value := word[colon+1:]
for _, searchFlag := range searchFlags {
// check for case insensitive equality
if strings.EqualFold(flagName, searchFlag) {
if value != "" {
flags = append(flags, flag{
searchFlag,
value,
exclude,
})
isFlag = true
} else if i < len(input)-1 {
flags = append(flags, flag{
searchFlag,
input[i+1],
exclude,
})
skipNextWord = true
isFlag = true
}
if isFlag {
break
}
}
}
}
if !isFlag {
exclude := false
if strings.HasPrefix(word, "-") {
exclude = true
}
// trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards)
word = searchTermPuncStart.ReplaceAllString(word, "")
word = searchTermPuncEnd.ReplaceAllString(word, "")
// and remove extra pound #s
word = hashtagStart.ReplaceAllString(word, "#")
if word != "" {
words = append(words, searchWord{
word,
exclude,
})
}
}
}
return words, flags
}
func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams {
words, flags := parseSearchFlags(splitWords(text))
hashtagTermList := []string{}
excludedHashtagTermList := []string{}
plainTermList := []string{}
excludedPlainTermList := []string{}
for _, word := range words {
if validHashtag.MatchString(word.value) {
if word.exclude {
excludedHashtagTermList = append(excludedHashtagTermList, word.value)
} else {
hashtagTermList = append(hashtagTermList, word.value)
}
} else {
if word.exclude {
excludedPlainTermList = append(excludedPlainTermList, word.value)
} else {
plainTermList = append(plainTermList, word.value)
}
}
}
hashtagTerms := strings.Join(hashtagTermList, " ")
excludedHashtagTerms := strings.Join(excludedHashtagTermList, " ")
plainTerms := strings.Join(plainTermList, " ")
excludedPlainTerms := strings.Join(excludedPlainTermList, " ")
inChannels := []string{}
excludedChannels := []string{}
fromUsers := []string{}
excludedUsers := []string{}
afterDate := ""
excludedAfterDate := ""
beforeDate := ""
excludedBeforeDate := ""
onDate := ""
excludedDate := ""
excludedExtensions := []string{}
extensions := []string{}
for _, flag := range flags {
if flag.name == "in" || flag.name == "channel" {
if flag.exclude {
excludedChannels = append(excludedChannels, flag.value)
} else {
inChannels = append(inChannels, flag.value)
}
} else if flag.name == "from" {
if flag.exclude {
excludedUsers = append(excludedUsers, flag.value)
} else {
fromUsers = append(fromUsers, flag.value)
}
} else if flag.name == "after" {
if flag.exclude {
excludedAfterDate = flag.value
} else {
afterDate = flag.value
}
} else if flag.name == "before" {
if flag.exclude {
excludedBeforeDate = flag.value
} else {
beforeDate = flag.value
}
} else if flag.name == "on" {
if flag.exclude {
excludedDate = flag.value
} else {
onDate = flag.value
}
} else if flag.name == "ext" {
if flag.exclude {
excludedExtensions = append(excludedExtensions, flag.value)
} else {
extensions = append(extensions, flag.value)
}
}
}
paramsList := []*SearchParams{}
if plainTerms != "" || excludedPlainTerms != "" {
paramsList = append(paramsList, &SearchParams{
Terms: plainTerms,
ExcludedTerms: excludedPlainTerms,
IsHashtag: false,
InChannels: inChannels,
ExcludedChannels: excludedChannels,
FromUsers: fromUsers,
ExcludedUsers: excludedUsers,
AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
Extensions: extensions,
ExcludedExtensions: excludedExtensions,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
})
}
if hashtagTerms != "" || excludedHashtagTerms != "" {
paramsList = append(paramsList, &SearchParams{
Terms: hashtagTerms,
ExcludedTerms: excludedHashtagTerms,
IsHashtag: true,
InChannels: inChannels,
ExcludedChannels: excludedChannels,
FromUsers: fromUsers,
ExcludedUsers: excludedUsers,
AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
Extensions: extensions,
ExcludedExtensions: excludedExtensions,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
})
}
// special case for when no terms are specified but we still have a filter
if plainTerms == "" && hashtagTerms == "" &&
excludedPlainTerms == "" && excludedHashtagTerms == "" &&
(len(inChannels) != 0 || len(fromUsers) != 0 ||
len(excludedChannels) != 0 || len(excludedUsers) != 0 ||
len(extensions) != 0 || len(excludedExtensions) != 0 ||
afterDate != "" || excludedAfterDate != "" ||
beforeDate != "" || excludedBeforeDate != "" ||
onDate != "" || excludedDate != "") {
paramsList = append(paramsList, &SearchParams{
Terms: "",
ExcludedTerms: "",
IsHashtag: false,
InChannels: inChannels,
ExcludedChannels: excludedChannels,
FromUsers: fromUsers,
ExcludedUsers: excludedUsers,
AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
Extensions: extensions,
ExcludedExtensions: excludedExtensions,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
})
}
return paramsList
}
func IsSearchParamsListValid(paramsList []*SearchParams) *AppError {
// All SearchParams should have same IncludeDeletedChannels value.
for _, params := range paramsList {
if params.IncludeDeletedChannels != paramsList[0].IncludeDeletedChannels {
return NewAppError("IsSearchParamsListValid", "model.search_params_list.is_valid.include_deleted_channels.app_error", nil, "", http.StatusInternalServerError)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
SessionCookieToken = "MMAUTHTOKEN"
SessionCookieUser = "MMUSERID"
SessionCookieCsrf = "MMCSRF"
SessionCookieCloudUrl = "MMCLOUDURL"
SessionCacheSize = 35000
SessionPropPlatform = "platform"
SessionPropOs = "os"
SessionPropBrowser = "browser"
SessionPropType = "type"
SessionPropUserAccessTokenId = "user_access_token_id"
SessionPropIsBot = "is_bot"
SessionPropIsBotValue = "true"
SessionPropOAuthAppID = "oauth_app_id"
SessionPropMattermostAppID = "mattermost_app_id"
SessionTypeUserAccessToken = "UserAccessToken"
SessionTypeCloudKey = "CloudKey"
SessionTypeRemoteclusterToken = "RemoteClusterToken"
SessionPropIsGuest = "is_guest"
SessionActivityTimeout = 1000 * 60 * 5 // 5 minutes
SessionUserAccessTokenExpiryHours = 100 * 365 * 24 // 100 years
)
//msgp:tuple StringMap
type StringMap map[string]string
// Session contains the user session details.
// This struct's serializer methods are auto-generated. If a new field is added/removed,
// please run make gen-serialized.
//
//msgp:tuple Session
type Session struct {
Id string `json:"id"`
Token string `json:"token"`
CreateAt int64 `json:"create_at"`
ExpiresAt int64 `json:"expires_at"`
LastActivityAt int64 `json:"last_activity_at"`
UserId string `json:"user_id"`
DeviceId string `json:"device_id"`
Roles string `json:"roles"`
IsOAuth bool `json:"is_oauth"`
ExpiredNotify bool `json:"expired_notify"`
Props StringMap `json:"props"`
TeamMembers []*TeamMember `json:"team_members" db:"-"`
Local bool `json:"local" db:"-"`
}
func (s *Session) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": s.Id,
"create_at": s.CreateAt,
"expires_at": s.ExpiresAt,
"last_activity_at": s.LastActivityAt,
"user_id": s.UserId,
"device_id": s.DeviceId,
"roles": s.Roles,
"is_oauth": s.IsOAuth,
"expired_notify": s.ExpiredNotify,
"local": s.Local,
}
}
// Returns true if the session is unrestricted, which should grant it
// with all permissions. This is used for local mode sessions
func (s *Session) IsUnrestricted() bool {
return s.Local
}
func (s *Session) DeepCopy() *Session {
copySession := *s
if s.Props != nil {
copySession.Props = CopyStringMap(s.Props)
}
if s.TeamMembers != nil {
copySession.TeamMembers = make([]*TeamMember, len(s.TeamMembers))
for index, tm := range s.TeamMembers {
copySession.TeamMembers[index] = new(TeamMember)
*copySession.TeamMembers[index] = *tm
}
}
return ©Session
}
func (s *Session) IsValid() *AppError {
if !IsValidId(s.Id) {
return NewAppError("Session.IsValid", "model.session.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(s.UserId) {
return NewAppError("Session.IsValid", "model.session.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if s.CreateAt == 0 {
return NewAppError("Session.IsValid", "model.session.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
if len(s.Roles) > UserRolesMaxLength {
return NewAppError("Session.IsValid", "model.session.is_valid.roles_limit.app_error",
map[string]any{"Limit": UserRolesMaxLength}, "session_id="+s.Id, http.StatusBadRequest)
}
return nil
}
func (s *Session) PreSave() {
if s.Id == "" {
s.Id = NewId()
}
if s.Token == "" {
s.Token = NewId()
}
s.CreateAt = GetMillis()
s.LastActivityAt = s.CreateAt
if s.Props == nil {
s.Props = make(map[string]string)
}
}
func (s *Session) Sanitize() {
s.Token = ""
}
func (s *Session) IsExpired() bool {
if s.ExpiresAt <= 0 {
return false
}
if GetMillis() > s.ExpiresAt {
return true
}
return false
}
func (s *Session) AddProp(key string, value string) {
if s.Props == nil {
s.Props = make(map[string]string)
}
s.Props[key] = value
}
func (s *Session) GetTeamByTeamId(teamId string) *TeamMember {
for _, tm := range s.TeamMembers {
if tm.TeamId == teamId {
return tm
}
}
return nil
}
func (s *Session) IsMobileApp() bool {
return s.DeviceId != "" || s.IsMobile()
}
func (s *Session) IsMobile() bool {
val, ok := s.Props[UserAuthServiceIsMobile]
if !ok {
return false
}
isMobile, err := strconv.ParseBool(val)
if err != nil {
mlog.Debug("Error parsing boolean property from Session", mlog.Err(err))
return false
}
return isMobile
}
func (s *Session) IsSaml() bool {
val, ok := s.Props[UserAuthServiceIsSaml]
if !ok {
return false
}
isSaml, err := strconv.ParseBool(val)
if err != nil {
mlog.Debug("Error parsing boolean property from Session", mlog.Err(err))
return false
}
return isSaml
}
func (s *Session) IsOAuthUser() bool {
val, ok := s.Props[UserAuthServiceIsOAuth]
if !ok {
return false
}
isOAuthUser, err := strconv.ParseBool(val)
if err != nil {
mlog.Debug("Error parsing boolean property from Session", mlog.Err(err))
return false
}
return isOAuthUser
}
func (s *Session) IsSSOLogin() bool {
return s.IsOAuthUser() || s.IsSaml()
}
func (s *Session) GetUserRoles() []string {
return strings.Fields(s.Roles)
}
func (s *Session) GenerateCSRF() string {
token := NewId()
s.AddProp("csrf", token)
return token
}
func (s *Session) GetCSRF() string {
if s.Props == nil {
return ""
}
return s.Props["csrf"]
}
func (s *Session) CreateAt_() float64 {
return float64(s.CreateAt)
}
func (s *Session) ExpiresAt_() float64 {
return float64(s.ExpiresAt)
}
func (s *Session) LastActivityAt_() float64 {
return float64(s.LastActivityAt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *Session) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0001 uint32
zb0001, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 13 {
err = msgp.ArrayError{Wanted: 13, Got: zb0001}
return
}
z.Id, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
z.Token, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Token")
return
}
z.CreateAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
z.ExpiresAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "ExpiresAt")
return
}
z.LastActivityAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
z.UserId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
z.DeviceId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "DeviceId")
return
}
z.Roles, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.IsOAuth, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "IsOAuth")
return
}
z.ExpiredNotify, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "ExpiredNotify")
return
}
var zb0002 uint32
zb0002, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
if z.Props == nil {
z.Props = make(StringMap, zb0002)
} else if len(z.Props) > 0 {
for key := range z.Props {
delete(z.Props, key)
}
}
for zb0002 > 0 {
zb0002--
var za0001 string
var za0002 string
za0001, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
za0002, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Props", za0001)
return
}
z.Props[za0001] = za0002
}
var zb0003 uint32
zb0003, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "TeamMembers")
return
}
if cap(z.TeamMembers) >= int(zb0003) {
z.TeamMembers = (z.TeamMembers)[:zb0003]
} else {
z.TeamMembers = make([]*TeamMember, zb0003)
}
for za0003 := range z.TeamMembers {
if dc.IsNil() {
err = dc.ReadNil()
if err != nil {
err = msgp.WrapError(err, "TeamMembers", za0003)
return
}
z.TeamMembers[za0003] = nil
} else {
if z.TeamMembers[za0003] == nil {
z.TeamMembers[za0003] = new(TeamMember)
}
err = z.TeamMembers[za0003].DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "TeamMembers", za0003)
return
}
}
}
z.Local, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "Local")
return
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *Session) EncodeMsg(en *msgp.Writer) (err error) {
// array header, size 13
err = en.Append(0x9d)
if err != nil {
return
}
err = en.WriteString(z.Id)
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
err = en.WriteString(z.Token)
if err != nil {
err = msgp.WrapError(err, "Token")
return
}
err = en.WriteInt64(z.CreateAt)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
err = en.WriteInt64(z.ExpiresAt)
if err != nil {
err = msgp.WrapError(err, "ExpiresAt")
return
}
err = en.WriteInt64(z.LastActivityAt)
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
err = en.WriteString(z.UserId)
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
err = en.WriteString(z.DeviceId)
if err != nil {
err = msgp.WrapError(err, "DeviceId")
return
}
err = en.WriteString(z.Roles)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
err = en.WriteBool(z.IsOAuth)
if err != nil {
err = msgp.WrapError(err, "IsOAuth")
return
}
err = en.WriteBool(z.ExpiredNotify)
if err != nil {
err = msgp.WrapError(err, "ExpiredNotify")
return
}
err = en.WriteMapHeader(uint32(len(z.Props)))
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
for za0001, za0002 := range z.Props {
err = en.WriteString(za0001)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
err = en.WriteString(za0002)
if err != nil {
err = msgp.WrapError(err, "Props", za0001)
return
}
}
err = en.WriteArrayHeader(uint32(len(z.TeamMembers)))
if err != nil {
err = msgp.WrapError(err, "TeamMembers")
return
}
for za0003 := range z.TeamMembers {
if z.TeamMembers[za0003] == nil {
err = en.WriteNil()
if err != nil {
return
}
} else {
err = z.TeamMembers[za0003].EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "TeamMembers", za0003)
return
}
}
}
err = en.WriteBool(z.Local)
if err != nil {
err = msgp.WrapError(err, "Local")
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *Session) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// array header, size 13
o = append(o, 0x9d)
o = msgp.AppendString(o, z.Id)
o = msgp.AppendString(o, z.Token)
o = msgp.AppendInt64(o, z.CreateAt)
o = msgp.AppendInt64(o, z.ExpiresAt)
o = msgp.AppendInt64(o, z.LastActivityAt)
o = msgp.AppendString(o, z.UserId)
o = msgp.AppendString(o, z.DeviceId)
o = msgp.AppendString(o, z.Roles)
o = msgp.AppendBool(o, z.IsOAuth)
o = msgp.AppendBool(o, z.ExpiredNotify)
o = msgp.AppendMapHeader(o, uint32(len(z.Props)))
for za0001, za0002 := range z.Props {
o = msgp.AppendString(o, za0001)
o = msgp.AppendString(o, za0002)
}
o = msgp.AppendArrayHeader(o, uint32(len(z.TeamMembers)))
for za0003 := range z.TeamMembers {
if z.TeamMembers[za0003] == nil {
o = msgp.AppendNil(o)
} else {
o, err = z.TeamMembers[za0003].MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "TeamMembers", za0003)
return
}
}
}
o = msgp.AppendBool(o, z.Local)
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *Session) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0001 uint32
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 13 {
err = msgp.ArrayError{Wanted: 13, Got: zb0001}
return
}
z.Id, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
z.Token, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Token")
return
}
z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
z.ExpiresAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "ExpiresAt")
return
}
z.LastActivityAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
z.UserId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
z.DeviceId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "DeviceId")
return
}
z.Roles, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.IsOAuth, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "IsOAuth")
return
}
z.ExpiredNotify, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "ExpiredNotify")
return
}
var zb0002 uint32
zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
if z.Props == nil {
z.Props = make(StringMap, zb0002)
} else if len(z.Props) > 0 {
for key := range z.Props {
delete(z.Props, key)
}
}
for zb0002 > 0 {
var za0001 string
var za0002 string
zb0002--
za0001, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
za0002, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Props", za0001)
return
}
z.Props[za0001] = za0002
}
var zb0003 uint32
zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "TeamMembers")
return
}
if cap(z.TeamMembers) >= int(zb0003) {
z.TeamMembers = (z.TeamMembers)[:zb0003]
} else {
z.TeamMembers = make([]*TeamMember, zb0003)
}
for za0003 := range z.TeamMembers {
if msgp.IsNil(bts) {
bts, err = msgp.ReadNilBytes(bts)
if err != nil {
return
}
z.TeamMembers[za0003] = nil
} else {
if z.TeamMembers[za0003] == nil {
z.TeamMembers[za0003] = new(TeamMember)
}
bts, err = z.TeamMembers[za0003].UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "TeamMembers", za0003)
return
}
}
}
z.Local, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Local")
return
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *Session) Msgsize() (s int) {
s = 1 + msgp.StringPrefixSize + len(z.Id) + msgp.StringPrefixSize + len(z.Token) + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size + msgp.StringPrefixSize + len(z.UserId) + msgp.StringPrefixSize + len(z.DeviceId) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + msgp.BoolSize + msgp.MapHeaderSize
if z.Props != nil {
for za0001, za0002 := range z.Props {
_ = za0002
s += msgp.StringPrefixSize + len(za0001) + msgp.StringPrefixSize + len(za0002)
}
}
s += msgp.ArrayHeaderSize
for za0003 := range z.TeamMembers {
if z.TeamMembers[za0003] == nil {
s += msgp.NilSize
} else {
s += z.TeamMembers[za0003].Msgsize()
}
}
s += msgp.BoolSize
return
}
// DecodeMsg implements msgp.Decodable
func (z *StringMap) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0003 uint32
zb0003, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if (*z) == nil {
(*z) = make(StringMap, zb0003)
} else if len((*z)) > 0 {
for key := range *z {
delete((*z), key)
}
}
for zb0003 > 0 {
zb0003--
var zb0001 string
var zb0002 string
zb0001, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err)
return
}
zb0002, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
(*z)[zb0001] = zb0002
}
return
}
// EncodeMsg implements msgp.Encodable
func (z StringMap) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteMapHeader(uint32(len(z)))
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0004, zb0005 := range z {
err = en.WriteString(zb0004)
if err != nil {
err = msgp.WrapError(err)
return
}
err = en.WriteString(zb0005)
if err != nil {
err = msgp.WrapError(err, zb0004)
return
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z StringMap) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendMapHeader(o, uint32(len(z)))
for zb0004, zb0005 := range z {
o = msgp.AppendString(o, zb0004)
o = msgp.AppendString(o, zb0005)
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *StringMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0003 uint32
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if (*z) == nil {
(*z) = make(StringMap, zb0003)
} else if len((*z)) > 0 {
for key := range *z {
delete((*z), key)
}
}
for zb0003 > 0 {
var zb0001 string
var zb0002 string
zb0003--
zb0001, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
zb0002, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
(*z)[zb0001] = zb0002
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z StringMap) Msgsize() (s int) {
s = msgp.MapHeaderSize
if z != nil {
for zb0004, zb0005 := range z {
_ = zb0005
s += msgp.StringPrefixSize + len(zb0004) + msgp.StringPrefixSize + len(zb0005)
}
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"unicode/utf8"
)
// SharedChannel represents a channel that can be synchronized with a remote cluster.
// If "home" is true, then the shared channel is homed locally and "SharedChannelRemote"
// table contains the remote clusters that have been invited.
// If "home" is false, then the shared channel is homed remotely, and "RemoteId"
// field points to the remote cluster connection in "RemoteClusters" table.
type SharedChannel struct {
ChannelId string `json:"id"`
TeamId string `json:"team_id"`
Home bool `json:"home"`
ReadOnly bool `json:"readonly"`
ShareName string `json:"name"`
ShareDisplayName string `json:"display_name"`
SharePurpose string `json:"purpose"`
ShareHeader string `json:"header"`
CreatorId string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
RemoteId string `json:"remote_id,omitempty"` // if not "home"
Type ChannelType `db:"-"`
}
func (sc *SharedChannel) IsValid() *AppError {
if !IsValidId(sc.ChannelId) {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest)
}
if sc.Type != ChannelTypeDirect && !IsValidId(sc.TeamId) {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "TeamId="+sc.TeamId, http.StatusBadRequest)
}
if sc.CreateAt == 0 {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if sc.UpdateAt == 0 {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(sc.ShareDisplayName) > ChannelDisplayNameMaxRunes {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if !IsValidChannelIdentifier(sc.ShareName) {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.1_or_more.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(sc.ShareHeader) > ChannelHeaderMaxRunes {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(sc.SharePurpose) > ChannelPurposeMaxRunes {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if !IsValidId(sc.CreatorId) {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "CreatorId="+sc.CreatorId, http.StatusBadRequest)
}
if !sc.Home {
if !IsValidId(sc.RemoteId) {
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+sc.RemoteId, http.StatusBadRequest)
}
}
return nil
}
func (sc *SharedChannel) PreSave() {
sc.ShareName = SanitizeUnicode(sc.ShareName)
sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName)
sc.CreateAt = GetMillis()
sc.UpdateAt = sc.CreateAt
}
func (sc *SharedChannel) PreUpdate() {
sc.UpdateAt = GetMillis()
sc.ShareName = SanitizeUnicode(sc.ShareName)
sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName)
}
// SharedChannelRemote represents a remote cluster that has been invited
// to a shared channel.
type SharedChannelRemote struct {
Id string `json:"id"`
ChannelId string `json:"channel_id"`
CreatorId string `json:"creator_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
IsInviteAccepted bool `json:"is_invite_accepted"`
IsInviteConfirmed bool `json:"is_invite_confirmed"`
RemoteId string `json:"remote_id"`
LastPostUpdateAt int64 `json:"last_post_update_at"`
LastPostId string `json:"last_post_id"`
}
func (sc *SharedChannelRemote) IsValid() *AppError {
if !IsValidId(sc.Id) {
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+sc.Id, http.StatusBadRequest)
}
if !IsValidId(sc.ChannelId) {
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest)
}
if sc.CreateAt == 0 {
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if sc.UpdateAt == 0 {
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
}
if !IsValidId(sc.CreatorId) {
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "id="+sc.CreatorId, http.StatusBadRequest)
}
return nil
}
func (sc *SharedChannelRemote) PreSave() {
if sc.Id == "" {
sc.Id = NewId()
}
sc.CreateAt = GetMillis()
sc.UpdateAt = sc.CreateAt
}
func (sc *SharedChannelRemote) PreUpdate() {
sc.UpdateAt = GetMillis()
}
type SharedChannelRemoteStatus struct {
ChannelId string `json:"channel_id"`
DisplayName string `json:"display_name"`
SiteURL string `json:"site_url"`
LastPingAt int64 `json:"last_ping_at"`
NextSyncAt int64 `json:"next_sync_at"`
ReadOnly bool `json:"readonly"`
IsInviteAccepted bool `json:"is_invite_accepted"`
Token string `json:"token"`
}
// SharedChannelUser stores a lastSyncAt timestamp on behalf of a remote cluster for
// each user that has been synchronized.
type SharedChannelUser struct {
Id string `json:"id"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
RemoteId string `json:"remote_id"`
CreateAt int64 `json:"create_at"`
LastSyncAt int64 `json:"last_sync_at"`
}
func (scu *SharedChannelUser) PreSave() {
scu.Id = NewId()
scu.CreateAt = GetMillis()
}
func (scu *SharedChannelUser) IsValid() *AppError {
if !IsValidId(scu.Id) {
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scu.Id, http.StatusBadRequest)
}
if !IsValidId(scu.UserId) {
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "UserId="+scu.UserId, http.StatusBadRequest)
}
if !IsValidId(scu.ChannelId) {
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+scu.ChannelId, http.StatusBadRequest)
}
if !IsValidId(scu.RemoteId) {
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scu.RemoteId, http.StatusBadRequest)
}
if scu.CreateAt == 0 {
return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
type GetUsersForSyncFilter struct {
CheckProfileImage bool
ChannelID string
Limit uint64
}
// SharedChannelAttachment stores a lastSyncAt timestamp on behalf of a remote cluster for
// each file attachment that has been synchronized.
type SharedChannelAttachment struct {
Id string `json:"id"`
FileId string `json:"file_id"`
RemoteId string `json:"remote_id"`
CreateAt int64 `json:"create_at"`
LastSyncAt int64 `json:"last_sync_at"`
}
func (scf *SharedChannelAttachment) PreSave() {
if scf.Id == "" {
scf.Id = NewId()
}
if scf.CreateAt == 0 {
scf.CreateAt = GetMillis()
scf.LastSyncAt = scf.CreateAt
} else {
scf.LastSyncAt = GetMillis()
}
}
func (scf *SharedChannelAttachment) IsValid() *AppError {
if !IsValidId(scf.Id) {
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scf.Id, http.StatusBadRequest)
}
if !IsValidId(scf.FileId) {
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "FileId="+scf.FileId, http.StatusBadRequest)
}
if !IsValidId(scf.RemoteId) {
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scf.RemoteId, http.StatusBadRequest)
}
if scf.CreateAt == 0 {
return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
type SharedChannelFilterOpts struct {
TeamId string
CreatorId string
MemberId string
ExcludeHome bool
ExcludeRemote bool
}
type SharedChannelRemoteFilterOpts struct {
ChannelId string
RemoteId string
InclUnconfirmed bool
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"regexp"
)
var linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
type SlackAttachment struct {
Id int64 `json:"id"`
Fallback string `json:"fallback"`
Color string `json:"color"`
Pretext string `json:"pretext"`
AuthorName string `json:"author_name"`
AuthorLink string `json:"author_link"`
AuthorIcon string `json:"author_icon"`
Title string `json:"title"`
TitleLink string `json:"title_link"`
Text string `json:"text"`
Fields []*SlackAttachmentField `json:"fields"`
ImageURL string `json:"image_url"`
ThumbURL string `json:"thumb_url"`
Footer string `json:"footer"`
FooterIcon string `json:"footer_icon"`
Timestamp any `json:"ts"` // This is either a string or an int64
Actions []*PostAction `json:"actions,omitempty"`
}
func (s *SlackAttachment) Equals(input *SlackAttachment) bool {
// Direct comparison of simple types
if s.Id != input.Id {
return false
}
if s.Fallback != input.Fallback {
return false
}
if s.Color != input.Color {
return false
}
if s.Pretext != input.Pretext {
return false
}
if s.AuthorName != input.AuthorName {
return false
}
if s.AuthorLink != input.AuthorLink {
return false
}
if s.AuthorIcon != input.AuthorIcon {
return false
}
if s.Title != input.Title {
return false
}
if s.TitleLink != input.TitleLink {
return false
}
if s.Text != input.Text {
return false
}
if s.ImageURL != input.ImageURL {
return false
}
if s.ThumbURL != input.ThumbURL {
return false
}
if s.Footer != input.Footer {
return false
}
if s.FooterIcon != input.FooterIcon {
return false
}
// Compare length & slice values of fields
if len(s.Fields) != len(input.Fields) {
return false
}
for j := range s.Fields {
if !s.Fields[j].Equals(input.Fields[j]) {
return false
}
}
// Compare length & slice values of actions
if len(s.Actions) != len(input.Actions) {
return false
}
for j := range s.Actions {
if !s.Actions[j].Equals(input.Actions[j]) {
return false
}
}
return s.Timestamp == input.Timestamp
}
type SlackAttachmentField struct {
Title string `json:"title"`
Value any `json:"value"`
Short SlackCompatibleBool `json:"short"`
}
func (s *SlackAttachmentField) Equals(input *SlackAttachmentField) bool {
if s.Title != input.Title {
return false
}
if s.Value != input.Value {
return false
}
if s.Short != input.Short {
return false
}
return true
}
func StringifySlackFieldValue(a []*SlackAttachment) []*SlackAttachment {
var nonNilAttachments []*SlackAttachment
for _, attachment := range a {
if attachment == nil {
continue
}
nonNilAttachments = append(nonNilAttachments, attachment)
var nonNilFields []*SlackAttachmentField
for _, field := range attachment.Fields {
if field == nil {
continue
}
nonNilFields = append(nonNilFields, field)
if field.Value != nil {
// Ensure the value is set to a string if it is set
field.Value = fmt.Sprintf("%v", field.Value)
}
}
attachment.Fields = nonNilFields
}
return nonNilAttachments
}
// This method only parses and processes the attachments,
// all else should be set in the post which is passed
func ParseSlackAttachment(post *Post, attachments []*SlackAttachment) {
if post.Type == "" {
post.Type = PostTypeSlackAttachment
}
postAttachments := []*SlackAttachment{}
for _, attachment := range attachments {
if attachment == nil {
continue
}
attachment.Text = ParseSlackLinksToMarkdown(attachment.Text)
attachment.Pretext = ParseSlackLinksToMarkdown(attachment.Pretext)
for _, field := range attachment.Fields {
if field == nil {
continue
}
if value, ok := field.Value.(string); ok {
field.Value = ParseSlackLinksToMarkdown(value)
}
}
postAttachments = append(postAttachments, attachment)
}
post.AddProp("attachments", postAttachments)
}
func ParseSlackLinksToMarkdown(text string) string {
return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"strings"
)
// SlackCompatibleBool is an alias for bool that implements json.Unmarshaler
type SlackCompatibleBool bool
// UnmarshalJSON implements json.Unmarshaler
//
// Slack allows bool values to be represented as strings ("true"/"false") or
// literals (true/false). To maintain compatibility, we define an Unmarshaler
// that supports both.
func (b *SlackCompatibleBool) UnmarshalJSON(data []byte) error {
value := strings.ToLower(string(data))
if value == "true" || value == `"true"` {
*b = true
} else if value == "false" || value == `"false"` {
*b = false
} else {
return fmt.Errorf("unmarshal: unable to convert %s to bool", data)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
)
const (
StatusOutOfOffice = "ooo"
StatusOffline = "offline"
StatusAway = "away"
StatusDnd = "dnd"
StatusOnline = "online"
StatusCacheSize = SessionCacheSize
StatusChannelTimeout = 20000 // 20 seconds
StatusMinUpdateTime = 120000 // 2 minutes
)
type Status struct {
UserId string `json:"user_id"`
Status string `json:"status"`
Manual bool `json:"manual"`
LastActivityAt int64 `json:"last_activity_at"`
ActiveChannel string `json:"active_channel,omitempty" db:"-"`
DNDEndTime int64 `json:"dnd_end_time"`
PrevStatus string `json:"-"`
}
func (s *Status) ToJSON() ([]byte, error) {
sCopy := *s
sCopy.ActiveChannel = ""
return json.Marshal(sCopy)
}
// The following are some GraphQL methods necessary to return the
// data in float64 type. The spec doesn't support 64 bit integers,
// so we have to pass the data in float64. The _ at the end is
// a hack to keep the attribute name same in GraphQL schema.
func (s *Status) LastActivityAt_() float64 {
return float64(s.LastActivityAt)
}
func (s *Status) DNDEndTime_() float64 {
return float64(s.DNDEndTime)
}
func StatusListToJSON(u []*Status) ([]byte, error) {
list := make([]Status, len(u))
for i, s := range u {
list[i] = *s
list[i].ActiveChannel = ""
}
return json.Marshal(list)
}
func StatusMapToInterfaceMap(statusMap map[string]*Status) map[string]any {
interfaceMap := map[string]any{}
for _, s := range statusMap {
// Omitted statues mean offline
if s.Status != StatusOffline {
interfaceMap[s.UserId] = s.Status
}
}
return interfaceMap
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type SwitchRequest struct {
CurrentService string `json:"current_service"`
NewService string `json:"new_service"`
Email string `json:"email"`
Password string `json:"password"`
NewPassword string `json:"new_password"`
MfaCode string `json:"mfa_code"`
LdapLoginId string `json:"ldap_id"`
}
func (o *SwitchRequest) Auditable() map[string]interface{} {
return map[string]interface{}{
"current_service": o.CurrentService,
"new_service": o.NewService,
"email": o.Email,
"ldap_login_id": o.LdapLoginId,
}
}
func (o *SwitchRequest) EmailToOAuth() bool {
return o.CurrentService == UserAuthServiceEmail &&
(o.NewService == UserAuthServiceSaml ||
o.NewService == UserAuthServiceGitlab ||
o.NewService == ServiceGoogle ||
o.NewService == ServiceOffice365 ||
o.NewService == ServiceOpenid)
}
func (o *SwitchRequest) OAuthToEmail() bool {
return (o.CurrentService == UserAuthServiceSaml ||
o.CurrentService == UserAuthServiceGitlab ||
o.CurrentService == ServiceGoogle ||
o.CurrentService == ServiceOffice365 ||
o.CurrentService == ServiceOpenid) && o.NewService == UserAuthServiceEmail
}
func (o *SwitchRequest) EmailToLdap() bool {
return o.CurrentService == UserAuthServiceEmail && o.NewService == UserAuthServiceLdap
}
func (o *SwitchRequest) LdapToEmail() bool {
return o.CurrentService == UserAuthServiceLdap && o.NewService == UserAuthServiceEmail
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"regexp"
"strings"
"unicode/utf8"
)
const (
TeamOpen = "O"
TeamInvite = "I"
TeamAllowedDomainsMaxLength = 500
TeamCompanyNameMaxLength = 64
TeamDescriptionMaxLength = 255
TeamDisplayNameMaxRunes = 64
TeamEmailMaxLength = 128
TeamNameMaxLength = 64
TeamNameMinLength = 2
)
type Team struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Description string `json:"description"`
Email string `json:"email"`
Type string `json:"type"`
CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"`
InviteId string `json:"invite_id"`
AllowOpenInvite bool `json:"allow_open_invite"`
LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
SchemeId *string `json:"scheme_id"`
GroupConstrained *bool `json:"group_constrained"`
PolicyID *string `json:"policy_id"`
CloudLimitsArchived bool `json:"cloud_limits_archived"`
}
func (o *Team) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": o.Id,
"create_at": o.CreateAt,
"update_at": o.UpdateAt,
"delete_at": o.DeleteAt,
"type": o.Type,
"invite_id": o.InviteId,
"allow_open_invite": o.AllowOpenInvite,
"scheme_id": o.SchemeId,
"group_constrained": o.GroupConstrained,
"policy_id": o.PolicyID,
"cloud_limits_archived": o.CloudLimitsArchived,
}
}
type TeamPatch struct {
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
CompanyName *string `json:"company_name"`
AllowedDomains *string `json:"allowed_domains"`
AllowOpenInvite *bool `json:"allow_open_invite"`
GroupConstrained *bool `json:"group_constrained"`
CloudLimitsArchived *bool `json:"cloud_limits_archived"`
}
func (o *TeamPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"allow_open_invite": o.AllowOpenInvite,
"group_constrained": o.GroupConstrained,
"cloud_limits_archived": o.CloudLimitsArchived,
}
}
type TeamForExport struct {
Team
SchemeName *string
}
type Invites struct {
Invites []map[string]string `json:"invites"`
}
type TeamsWithCount struct {
Teams []*Team `json:"teams"`
TotalCount int64 `json:"total_count"`
}
func (o *Invites) ToEmailList() []string {
emailList := make([]string, len(o.Invites))
for _, invite := range o.Invites {
emailList = append(emailList, invite["email"])
}
return emailList
}
func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Team) IsValid() *AppError {
if !IsValidId(o.Id) {
return NewAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Team.IsValid", "model.team.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Team.IsValid", "model.team.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.Email) > TeamEmailMaxLength {
return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.Email != "" && !IsValidEmail(o.Email) {
return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.DisplayName) == 0 || utf8.RuneCountInString(o.DisplayName) > TeamDisplayNameMaxRunes {
return NewAppError("Team.IsValid", "model.team.is_valid.name.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.Name) > TeamNameMaxLength {
return NewAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.Description) > TeamDescriptionMaxLength {
return NewAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.InviteId == "" {
return NewAppError("Team.IsValid", "model.team.is_valid.invite_id.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if IsReservedTeamName(o.Name) {
return NewAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidTeamName(o.Name) {
return NewAppError("Team.IsValid", "model.team.is_valid.characters.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !(o.Type == TeamOpen || o.Type == TeamInvite) {
return NewAppError("Team.IsValid", "model.team.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.CompanyName) > TeamCompanyNameMaxLength {
return NewAppError("Team.IsValid", "model.team.is_valid.company.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.AllowedDomains) > TeamAllowedDomainsMaxLength {
return NewAppError("Team.IsValid", "model.team.is_valid.domains.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
return nil
}
func (o *Team) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
o.Name = SanitizeUnicode(o.Name)
o.DisplayName = SanitizeUnicode(o.DisplayName)
o.Description = SanitizeUnicode(o.Description)
o.CompanyName = SanitizeUnicode(o.CompanyName)
if o.InviteId == "" {
o.InviteId = NewId()
}
}
func (o *Team) PreUpdate() {
o.UpdateAt = GetMillis()
o.Name = SanitizeUnicode(o.Name)
o.DisplayName = SanitizeUnicode(o.DisplayName)
o.Description = SanitizeUnicode(o.Description)
o.CompanyName = SanitizeUnicode(o.CompanyName)
}
func IsReservedTeamName(s string) bool {
s = strings.ToLower(s)
for _, value := range reservedName {
if strings.Index(s, value) == 0 {
return true
}
}
return false
}
func IsValidTeamName(s string) bool {
if !isValidAlphaNum(s) {
return false
}
if len(s) < TeamNameMinLength {
return false
}
return true
}
var validTeamNameCharacter = regexp.MustCompile(`^[a-z0-9-]$`)
func CleanTeamName(s string) string {
s = strings.ToLower(strings.Replace(s, " ", "-", -1))
for _, value := range reservedName {
if strings.Index(s, value) == 0 {
s = strings.Replace(s, value, "", -1)
}
}
s = strings.TrimSpace(s)
for _, c := range s {
char := fmt.Sprintf("%c", c)
if !validTeamNameCharacter.MatchString(char) {
s = strings.Replace(s, char, "", -1)
}
}
s = strings.Trim(s, "-")
if !IsValidTeamName(s) {
s = NewId()
}
return s
}
func (o *Team) Sanitize() {
o.Email = ""
o.InviteId = ""
}
func (o *Team) Patch(patch *TeamPatch) {
if patch.DisplayName != nil {
o.DisplayName = *patch.DisplayName
}
if patch.Description != nil {
o.Description = *patch.Description
}
if patch.CompanyName != nil {
o.CompanyName = *patch.CompanyName
}
if patch.AllowedDomains != nil {
o.AllowedDomains = *patch.AllowedDomains
}
if patch.AllowOpenInvite != nil {
o.AllowOpenInvite = *patch.AllowOpenInvite
}
if patch.GroupConstrained != nil {
o.GroupConstrained = patch.GroupConstrained
}
if patch.CloudLimitsArchived != nil {
o.CloudLimitsArchived = *patch.CloudLimitsArchived
}
}
func (o *Team) IsGroupConstrained() bool {
return o.GroupConstrained != nil && *o.GroupConstrained
}
// ShallowCopy returns a shallow copy of team.
func (o *Team) ShallowCopy() *Team {
c := *o
return &c
}
// The following are some GraphQL methods necessary to return the
// data in float64 type. The spec doesn't support 64 bit integers,
// so we have to pass the data in float64. The _ at the end is
// a hack to keep the attribute name same in GraphQL schema.
func (o *Team) CreateAt_() float64 {
return float64(o.UpdateAt)
}
func (o *Team) UpdateAt_() float64 {
return float64(o.UpdateAt)
}
func (o *Team) DeleteAt_() float64 {
return float64(o.DeleteAt)
}
func (o *Team) LastTeamIconUpdate_() float64 {
return float64(o.LastTeamIconUpdate)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"strings"
)
const (
USERNAME = "Username"
)
// This struct's serializer methods are auto-generated. If a new field is added/removed,
// please run make gen-serialized.
//
//msgp:tuple TeamMember
type TeamMember struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
DeleteAt int64 `json:"delete_at"`
SchemeGuest bool `json:"scheme_guest"`
SchemeUser bool `json:"scheme_user"`
SchemeAdmin bool `json:"scheme_admin"`
ExplicitRoles string `json:"explicit_roles"`
CreateAt int64 `json:"-"`
}
func (o *TeamMember) Auditable() map[string]interface{} {
return map[string]interface{}{
"team_id": o.TeamId,
"user_id": o.UserId,
"roles": o.Roles,
"delete_at": o.DeleteAt,
"scheme_guest": o.SchemeGuest,
"scheme_user": o.SchemeUser,
"scheme_admin": o.SchemeAdmin,
"explicit_roles": o.ExplicitRoles,
"create_at": o.CreateAt,
}
}
//msgp:ignore TeamUnread
type TeamUnread struct {
TeamId string `json:"team_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
MentionCountRoot int64 `json:"mention_count_root"`
MsgCountRoot int64 `json:"msg_count_root"`
ThreadCount int64 `json:"thread_count"`
ThreadMentionCount int64 `json:"thread_mention_count"`
ThreadUrgentMentionCount int64 `json:"thread_urgent_mention_count"`
}
//msgp:ignore TeamMemberForExport
type TeamMemberForExport struct {
TeamMember
TeamName string
}
//msgp:ignore TeamMemberWithError
type TeamMemberWithError struct {
UserId string `json:"user_id"`
Member *TeamMember `json:"member"`
Error *AppError `json:"error"`
}
//msgp:ignore EmailInviteWithError
type EmailInviteWithError struct {
Email string `json:"email"`
Error *AppError `json:"error"`
}
//msgp:ignore TeamMembersGetOptions
type TeamMembersGetOptions struct {
// Sort the team members. Accepts "Username", but defaults to "Id".
Sort string
// If true, exclude team members whose corresponding user is deleted.
ExcludeDeletedUsers bool
// Restrict to search in a list of teams and channels
ViewRestrictions *ViewUsersRestrictions
}
//msgp:ignore TeamInviteReminderData
type TeamInviteReminderData struct {
Interval string
}
func EmailInviteWithErrorToEmails(o []*EmailInviteWithError) []string {
var ret []string
for _, o := range o {
if o.Error == nil {
ret = append(ret, o.Email)
}
}
return ret
}
func EmailInviteWithErrorToString(o *EmailInviteWithError) string {
return fmt.Sprintf("%s:%s", o.Email, o.Error.Error())
}
func TeamMembersWithErrorToTeamMembers(o []*TeamMemberWithError) []*TeamMember {
var ret []*TeamMember
for _, o := range o {
if o.Error == nil {
ret = append(ret, o.Member)
}
}
return ret
}
func TeamMemberWithErrorToString(o *TeamMemberWithError) string {
return fmt.Sprintf("%s:%s", o.UserId, o.Error.Error())
}
func (o *TeamMember) IsValid() *AppError {
if !IsValidId(o.TeamId) {
return NewAppError("TeamMember.IsValid", "model.team_member.is_valid.team_id.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(o.UserId) {
return NewAppError("TeamMember.IsValid", "model.team_member.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.Roles) > UserRolesMaxLength {
return NewAppError("TeamMember.IsValid", "model.team_member.is_valid.roles_limit.app_error",
map[string]any{"Limit": UserRolesMaxLength}, "", http.StatusBadRequest)
}
return nil
}
func (o *TeamMember) PreUpdate() {
}
func (o *TeamMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
// DeleteAt_ returns the deleteAt value in float64. This is necessary to work
// with GraphQL since it doesn't support 64 bit integers.
func (o *TeamMember) DeleteAt_() float64 {
return float64(o.DeleteAt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *TeamMember) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0001 uint32
zb0001, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 9 {
err = msgp.ArrayError{Wanted: 9, Got: zb0001}
return
}
z.TeamId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "TeamId")
return
}
z.UserId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
z.Roles, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.DeleteAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
z.SchemeGuest, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "SchemeGuest")
return
}
z.SchemeUser, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "SchemeUser")
return
}
z.SchemeAdmin, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "SchemeAdmin")
return
}
z.ExplicitRoles, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "ExplicitRoles")
return
}
z.CreateAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *TeamMember) EncodeMsg(en *msgp.Writer) (err error) {
// array header, size 9
err = en.Append(0x99)
if err != nil {
return
}
err = en.WriteString(z.TeamId)
if err != nil {
err = msgp.WrapError(err, "TeamId")
return
}
err = en.WriteString(z.UserId)
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
err = en.WriteString(z.Roles)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
err = en.WriteInt64(z.DeleteAt)
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
err = en.WriteBool(z.SchemeGuest)
if err != nil {
err = msgp.WrapError(err, "SchemeGuest")
return
}
err = en.WriteBool(z.SchemeUser)
if err != nil {
err = msgp.WrapError(err, "SchemeUser")
return
}
err = en.WriteBool(z.SchemeAdmin)
if err != nil {
err = msgp.WrapError(err, "SchemeAdmin")
return
}
err = en.WriteString(z.ExplicitRoles)
if err != nil {
err = msgp.WrapError(err, "ExplicitRoles")
return
}
err = en.WriteInt64(z.CreateAt)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *TeamMember) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// array header, size 9
o = append(o, 0x99)
o = msgp.AppendString(o, z.TeamId)
o = msgp.AppendString(o, z.UserId)
o = msgp.AppendString(o, z.Roles)
o = msgp.AppendInt64(o, z.DeleteAt)
o = msgp.AppendBool(o, z.SchemeGuest)
o = msgp.AppendBool(o, z.SchemeUser)
o = msgp.AppendBool(o, z.SchemeAdmin)
o = msgp.AppendString(o, z.ExplicitRoles)
o = msgp.AppendInt64(o, z.CreateAt)
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *TeamMember) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0001 uint32
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 9 {
err = msgp.ArrayError{Wanted: 9, Got: zb0001}
return
}
z.TeamId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "TeamId")
return
}
z.UserId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "UserId")
return
}
z.Roles, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.DeleteAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
z.SchemeGuest, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "SchemeGuest")
return
}
z.SchemeUser, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "SchemeUser")
return
}
z.SchemeAdmin, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "SchemeAdmin")
return
}
z.ExplicitRoles, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "ExplicitRoles")
return
}
z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *TeamMember) Msgsize() (s int) {
s = 1 + msgp.StringPrefixSize + len(z.TeamId) + msgp.StringPrefixSize + len(z.UserId) + msgp.StringPrefixSize + len(z.Roles) + msgp.Int64Size + msgp.BoolSize + msgp.BoolSize + msgp.BoolSize + msgp.StringPrefixSize + len(z.ExplicitRoles) + msgp.Int64Size
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type TeamSearch struct {
Term string `json:"term"`
Page *int `json:"page,omitempty"`
PerPage *int `json:"per_page,omitempty"`
AllowOpenInvite *bool `json:"allow_open_invite,omitempty"`
GroupConstrained *bool `json:"group_constrained,omitempty"`
IncludeGroupConstrained *bool `json:"include_group_constrained,omitempty"`
PolicyID *string `json:"policy_id,omitempty"`
ExcludePolicyConstrained *bool `json:"exclude_policy_constrained,omitempty"`
IncludePolicyID *bool `json:"-"`
IncludeDeleted *bool `json:"-"`
TeamType *string `json:"-"`
}
func (t *TeamSearch) IsPaginated() bool {
return t.Page != nil && t.PerPage != nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
"unicode/utf8"
)
type TermsOfService struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UserId string `json:"user_id"`
Text string `json:"text"`
}
func (t *TermsOfService) IsValid() *AppError {
if !IsValidId(t.Id) {
return InvalidTermsOfServiceError("id", "")
}
if t.CreateAt == 0 {
return InvalidTermsOfServiceError("create_at", t.Id)
}
if !IsValidId(t.UserId) {
return InvalidTermsOfServiceError("user_id", t.Id)
}
if utf8.RuneCountInString(t.Text) > PostMessageMaxRunesV2 {
return InvalidTermsOfServiceError("text", t.Id)
}
return nil
}
func InvalidTermsOfServiceError(fieldName string, termsOfServiceId string) *AppError {
id := fmt.Sprintf("model.terms_of_service.is_valid.%s.app_error", fieldName)
details := ""
if termsOfServiceId != "" {
details = "terms_of_service_id=" + termsOfServiceId
}
return NewAppError("TermsOfService.IsValid", id, map[string]any{"MaxLength": PostMessageMaxRunesV2}, details, http.StatusBadRequest)
}
func (t *TermsOfService) PreSave() {
if t.Id == "" {
t.Id = NewId()
}
t.CreateAt = GetMillis()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Thread tracks the metadata associated with a root post and its reply posts.
//
// Note that Thread metadata does not exist until the first reply to a root post.
type Thread struct {
// PostId is the root post of the thread.
PostId string `json:"id"`
// ChannelId is the channel in which the thread was posted.
ChannelId string `json:"channel_id"`
// ReplyCount is the number of replies to the thread (excluding deleted posts).
ReplyCount int64 `json:"reply_count"`
// LastReplyAt is the timestamp of the most recent post to the thread.
LastReplyAt int64 `json:"last_reply_at"`
// Participants is a list of user ids that have replied to the thread, sorted by the oldest
// to newest. Note that the root post author is not included in this list until they reply.
Participants StringArray `json:"participants"`
// DeleteAt is a denormalized copy of the root posts's DeleteAt. In the database, it's
// named ThreadDeleteAt to avoid introducing a query conflict with older server versions.
DeleteAt int64 `json:"delete_at"`
// TeamId is a denormalized copy of the Channel's teamId. In the database, it's
// named ThreadTeamId to avoid introducing a query conflict with older server versions.
TeamId string `json:"team_id"`
}
type ThreadResponse struct {
PostId string `json:"id"`
ReplyCount int64 `json:"reply_count"`
LastReplyAt int64 `json:"last_reply_at"`
LastViewedAt int64 `json:"last_viewed_at"`
Participants []*User `json:"participants"`
Post *Post `json:"post"`
UnreadReplies int64 `json:"unread_replies"`
UnreadMentions int64 `json:"unread_mentions"`
IsUrgent bool `json:"is_urgent"`
DeleteAt int64 `json:"delete_at"`
}
type Threads struct {
Total int64 `json:"total"`
TotalUnreadThreads int64 `json:"total_unread_threads"`
TotalUnreadMentions int64 `json:"total_unread_mentions"`
TotalUnreadUrgentMentions int64 `json:"total_unread_urgent_mentions"`
Threads []*ThreadResponse `json:"threads"`
}
type GetUserThreadsOpts struct {
// PageSize specifies the size of the returned chunk of results. Default = 30
PageSize uint64
// Extended will enrich the response with participant details. Default = false
Extended bool
// Deleted will specify that even deleted threads should be returned (For mobile sync). Default = false
Deleted bool
// Since filters the threads based on their LastUpdateAt timestamp.
Since uint64
// Before specifies thread id as a cursor for pagination and will return `PageSize` threads before the cursor
Before string
// After specifies thread id as a cursor for pagination and will return `PageSize` threads after the cursor
After string
// Unread will make sure that only threads with unread replies are returned
Unread bool
// TotalsOnly will not fetch any threads and just fetch the total counts
TotalsOnly bool
// ThreadsOnly will fetch threads but not calculate totals and will return 0
ThreadsOnly bool
// TeamOnly will only fetch threads and unreads for the specified team and excludes DMs/GMs
TeamOnly bool
// IncludeIsUrgent will return IsUrgent field as well to assert is the thread is urgent or not
IncludeIsUrgent bool
}
func (o *Thread) Etag() string {
return Etag(o.PostId, o.LastReplyAt)
}
// ThreadMembership models the relationship between a user and a thread of posts, with a similar
// data structure as ChannelMembership.
type ThreadMembership struct {
// PostId is the root post id of the thread in question.
PostId string `json:"post_id"`
// UserId is the user whose membership in the thread is being tracked.
UserId string `json:"user_id"`
// Following tracks whether the user is following the given thread. This defaults to true
// when a ThreadMembership record is created (a record doesn't exist until the user first
// starts following the thread), but the user can stop following or resume following at
// will.
Following bool `json:"following"`
// LastUpdated is either the creation time of the membership record, or the last time the
// membership record was changed (e.g. started/stopped following, viewed thread, mention
// count change).
//
// This field is used to constrain queries of thread memberships to those updated after
// a given timestamp (e.g. on websocket reconnect). It's also used as the time column for
// deletion decisions during any configured retention policy.
LastUpdated int64 `json:"last_update_at"`
// LastViewed is the last time the user viewed this thread. It is the thread analogue to
// the ChannelMembership's LastViewedAt and is used to decide when there are new replies
// for the user and where the user should start reading.
LastViewed int64 `json:"last_view_at"`
// UnreadMentions is the number of unseen at-mentions for the user in the given thread. It
// is the thread analogue to the ChannelMembership's MentionCount, and is used to highlight
// threads with the mention count.
UnreadMentions int64 `json:"unread_mentions"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
const (
TokenSize = 64
MaxTokenExipryTime = 1000 * 60 * 60 * 48 // 48 hour
TokenTypeOAuth = "oauth"
)
type Token struct {
Token string
CreateAt int64
Type string
Extra string
}
func NewToken(tokentype, extra string) *Token {
return &Token{
Token: NewRandomString(TokenSize),
CreateAt: GetMillis(),
Type: tokentype,
Extra: extra,
}
}
func (t *Token) IsValid() *AppError {
if len(t.Token) != TokenSize {
return NewAppError("Token.IsValid", "model.token.is_valid.size", nil, "", http.StatusInternalServerError)
}
if t.CreateAt == 0 {
return NewAppError("Token.IsValid", "model.token.is_valid.expiry", nil, "", http.StatusInternalServerError)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "strings"
type TrueUpReviewProfile struct {
ServerId string `json:"server_id"`
ServerVersion string `json:"server_version"`
ServerInstallationType string `json:"server_installation_type"`
LicenseId string `json:"license_id"`
LicensedSeats int `json:"licensed_seats"`
LicensePlan string `json:"license_plan"`
CustomerName string `json:"customer_name"`
ActiveUsers int64 `json:"active_users"`
AuthenticationFeatures []string `json:"authentication_features"`
Plugins TrueUpReviewPlugins `json:"plugins"`
TotalIncomingWebhooks int64 `json:"incoming_webhooks_count"`
TotalOutgoingWebhooks int64 `json:"outgoing_webhooks_count"`
}
type TrueUpReviewPlugins struct {
TotalPlugins int `json:"total_plugins"`
PluginNames []string `json:"plugin_names"`
}
func (t *TrueUpReviewPlugins) ToMap() map[string]interface{} {
return map[string]interface{}{
"total_plugins": t.TotalPlugins,
"plugin_names": strings.Join(t.PluginNames, ","),
}
}
type TrueUpReviewStatus struct {
Completed bool `json:"complete"`
DueDate int64 `json:"due_date"`
}
func (t *TrueUpReviewStatus) ToSlice() []interface{} {
return []interface{}{
t.DueDate,
t.Completed,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
)
// UploadType defines the type of an upload.
type UploadType string
const (
UploadTypeAttachment UploadType = "attachment"
UploadTypeImport UploadType = "import"
IncompleteUploadSuffix = ".tmp"
)
// UploadNoUserID is a "fake" user id used by the API layer when in local mode.
const UploadNoUserID = "nouser"
// UploadSession contains information used to keep track of a file upload.
type UploadSession struct {
// The unique identifier for the session.
Id string `json:"id"`
// The type of the upload.
Type UploadType `json:"type"`
// The timestamp of creation.
CreateAt int64 `json:"create_at"`
// The id of the user performing the upload.
UserId string `json:"user_id"`
// The id of the channel to upload to.
ChannelId string `json:"channel_id,omitempty"`
// The name of the file to upload.
Filename string `json:"filename"`
// The path where the file is stored.
Path string `json:"-"`
// The size of the file to upload.
FileSize int64 `json:"file_size"`
// The amount of received data in bytes. If equal to FileSize it means the
// upload has finished.
FileOffset int64 `json:"file_offset"`
// Id of remote cluster if uploading for shared channel
RemoteId string `json:"remote_id"`
// Requested file id if uploading for shared channel
ReqFileId string `json:"req_file_id"`
}
func (us *UploadSession) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": us.Id,
"type": us.Type,
"user_id": us.UserId,
"channel_id": us.ChannelId,
"filename": us.Filename,
"file_size": us.FileSize,
"remote_id": us.RemoteId,
"ReqFileId": us.ReqFileId,
}
}
// PreSave is a utility function used to fill required information.
func (us *UploadSession) PreSave() {
if us.Id == "" {
us.Id = NewId()
}
if us.CreateAt == 0 {
us.CreateAt = GetMillis()
}
}
// IsValid validates an UploadType. It returns an error in case of
// failure.
func (t UploadType) IsValid() error {
switch t {
case UploadTypeAttachment:
return nil
case UploadTypeImport:
return nil
default:
}
return fmt.Errorf("invalid UploadType %s", t)
}
// IsValid validates an UploadSession. It returns an error in case of
// failure.
func (us *UploadSession) IsValid() *AppError {
if !IsValidId(us.Id) {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if err := us.Type.IsValid(); err != nil {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.type.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if !IsValidId(us.UserId) && us.UserId != UploadNoUserID {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.user_id.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.Type == UploadTypeAttachment && !IsValidId(us.ChannelId) {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.channel_id.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.CreateAt == 0 {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.create_at.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.Filename == "" {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.filename.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.FileSize <= 0 {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.file_size.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.FileOffset < 0 || us.FileOffset > us.FileSize {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.file_offset.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
if us.Path == "" {
return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.path.app_error", nil, "id="+us.Id, http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"golang.org/x/crypto/bcrypt"
"golang.org/x/text/language"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
Me = "me"
UserNotifyAll = "all"
UserNotifyHere = "here"
UserNotifyMention = "mention"
UserNotifyNone = "none"
DesktopNotifyProp = "desktop"
DesktopSoundNotifyProp = "desktop_sound"
MarkUnreadNotifyProp = "mark_unread"
PushNotifyProp = "push"
PushStatusNotifyProp = "push_status"
EmailNotifyProp = "email"
ChannelMentionsNotifyProp = "channel"
CommentsNotifyProp = "comments"
MentionKeysNotifyProp = "mention_keys"
CommentsNotifyNever = "never"
CommentsNotifyRoot = "root"
CommentsNotifyAny = "any"
CommentsNotifyCRT = "crt"
FirstNameNotifyProp = "first_name"
AutoResponderActiveNotifyProp = "auto_responder_active"
AutoResponderMessageNotifyProp = "auto_responder_message"
DesktopThreadsNotifyProp = "desktop_threads"
PushThreadsNotifyProp = "push_threads"
EmailThreadsNotifyProp = "email_threads"
DefaultLocale = "en"
UserAuthServiceEmail = "email"
UserEmailMaxLength = 128
UserNicknameMaxRunes = 64
UserPositionMaxRunes = 128
UserFirstNameMaxRunes = 64
UserLastNameMaxRunes = 64
UserAuthDataMaxLength = 128
UserNameMaxLength = 64
UserNameMinLength = 1
UserPasswordMaxLength = 72
UserLocaleMaxLength = 5
UserTimezoneMaxRunes = 256
UserRolesMaxLength = 256
)
//msgp:tuple User
// User contains the details about the user.
// This struct's serializer methods are auto-generated. If a new field is added/removed,
// please run make gen-serialized.
type User struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at,omitempty"`
UpdateAt int64 `json:"update_at,omitempty"`
DeleteAt int64 `json:"delete_at"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
AuthData *string `json:"auth_data,omitempty"`
AuthService string `json:"auth_service"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified,omitempty"`
Nickname string `json:"nickname"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Roles string `json:"roles"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"`
Locale string `json:"locale"`
Timezone StringMap `json:"timezone"`
MfaActive bool `json:"mfa_active,omitempty"`
MfaSecret string `json:"mfa_secret,omitempty"`
RemoteId *string `json:"remote_id,omitempty"`
LastActivityAt int64 `json:"last_activity_at,omitempty"`
IsBot bool `json:"is_bot,omitempty"`
BotDescription string `json:"bot_description,omitempty"`
BotLastIconUpdate int64 `json:"bot_last_icon_update,omitempty"`
TermsOfServiceId string `json:"terms_of_service_id,omitempty"`
TermsOfServiceCreateAt int64 `json:"terms_of_service_create_at,omitempty"`
DisableWelcomeEmail bool `json:"disable_welcome_email"`
}
func (u *User) Auditable() map[string]interface{} {
return map[string]interface{}{
"id": u.Id,
"create_at": u.CreateAt,
"update_at": u.UpdateAt,
"delete_at": u.DeleteAt,
"username": u.Username,
"auth_service": u.AuthService,
"email": u.Email,
"email_verified": u.EmailVerified,
"position": u.Position,
"roles": u.Roles,
"allow_marketing": u.AllowMarketing,
"props": u.Props,
"notify_props": u.NotifyProps,
"last_password_update": u.LastPasswordUpdate,
"last_picture_update": u.LastPictureUpdate,
"failed_attempts": u.FailedAttempts,
"locale": u.Locale,
"timezone": u.Timezone,
"mfa_active": u.MfaActive,
"remote_id": u.RemoteId,
"last_activity_at": u.LastActivityAt,
"is_bot": u.IsBot,
"bot_description": u.BotDescription,
"bot_last_icon_update": u.BotLastIconUpdate,
"terms_of_service_id": u.TermsOfServiceId,
"terms_of_service_create_at": u.TermsOfServiceCreateAt,
"disable_welcome_email": u.DisableWelcomeEmail,
}
}
//msgp UserMap
// UserMap is a map from a userId to a user object.
// It is used to generate methods which can be used for fast serialization/de-serialization.
type UserMap map[string]*User
//msgp:ignore UserUpdate
type UserUpdate struct {
Old *User
New *User
}
//msgp:ignore UserPatch
type UserPatch struct {
Username *string `json:"username"`
Password *string `json:"password,omitempty"`
Nickname *string `json:"nickname"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Position *string `json:"position"`
Email *string `json:"email"`
Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"`
Locale *string `json:"locale"`
Timezone StringMap `json:"timezone"`
RemoteId *string `json:"remote_id"`
}
func (u *UserPatch) Auditable() map[string]interface{} {
return map[string]interface{}{
"username": u.Username,
"nickname": u.Nickname,
"first_name": u.FirstName,
"last_name": u.LastName,
"position": u.Position,
"email": u.Email,
"props": u.Props,
"notify_props": u.NotifyProps,
"locale": u.Locale,
"timezone": u.Timezone,
"remote_id": u.RemoteId,
}
}
//msgp:ignore UserAuth
type UserAuth struct {
Password string `json:"password,omitempty"` // DEPRECATED: It is not used.
AuthData *string `json:"auth_data,omitempty"`
AuthService string `json:"auth_service,omitempty"`
}
func (u *UserAuth) Auditable() map[string]interface{} {
return map[string]interface{}{
"auth_service": u.AuthService,
}
}
//msgp:ignore UserForIndexing
type UserForIndexing struct {
Id string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Roles string `json:"roles"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
TeamsIds []string `json:"team_id"`
ChannelsIds []string `json:"channel_id"`
}
//msgp:ignore ViewUsersRestrictions
type ViewUsersRestrictions struct {
Teams []string
Channels []string
}
func (r *ViewUsersRestrictions) Hash() string {
if r == nil {
return ""
}
ids := append(r.Teams, r.Channels...)
sort.Strings(ids)
hash := sha256.New()
hash.Write([]byte(strings.Join(ids, "")))
return fmt.Sprintf("%x", hash.Sum(nil))
}
//msgp:ignore UserSlice
type UserSlice []*User
func (u UserSlice) Usernames() []string {
usernames := []string{}
for _, user := range u {
usernames = append(usernames, user.Username)
}
sort.Strings(usernames)
return usernames
}
func (u UserSlice) IDs() []string {
ids := []string{}
for _, user := range u {
ids = append(ids, user.Id)
}
return ids
}
func (u UserSlice) FilterWithoutBots() UserSlice {
var matches []*User
for _, user := range u {
if !user.IsBot {
matches = append(matches, user)
}
}
return UserSlice(matches)
}
func (u UserSlice) FilterByActive(active bool) UserSlice {
var matches []*User
for _, user := range u {
if user.DeleteAt == 0 && active {
matches = append(matches, user)
} else if user.DeleteAt != 0 && !active {
matches = append(matches, user)
}
}
return UserSlice(matches)
}
func (u UserSlice) FilterByID(ids []string) UserSlice {
var matches []*User
for _, user := range u {
for _, id := range ids {
if id == user.Id {
matches = append(matches, user)
}
}
}
return UserSlice(matches)
}
func (u UserSlice) FilterWithoutID(ids []string) UserSlice {
var keep []*User
for _, user := range u {
present := false
for _, id := range ids {
if id == user.Id {
present = true
}
}
if !present {
keep = append(keep, user)
}
}
return UserSlice(keep)
}
func (u *User) DeepCopy() *User {
copyUser := *u
if u.AuthData != nil {
copyUser.AuthData = NewString(*u.AuthData)
}
if u.Props != nil {
copyUser.Props = CopyStringMap(u.Props)
}
if u.NotifyProps != nil {
copyUser.NotifyProps = CopyStringMap(u.NotifyProps)
}
if u.Timezone != nil {
copyUser.Timezone = CopyStringMap(u.Timezone)
}
return ©User
}
// IsValid validates the user and returns an error if it isn't configured
// correctly.
func (u *User) IsValid() *AppError {
if !IsValidId(u.Id) {
return InvalidUserError("id", "", u.Id)
}
if u.CreateAt == 0 {
return InvalidUserError("create_at", u.Id, u.CreateAt)
}
if u.UpdateAt == 0 {
return InvalidUserError("update_at", u.Id, u.UpdateAt)
}
if u.IsRemote() {
if !IsValidUsernameAllowRemote(u.Username) {
return InvalidUserError("username", u.Id, u.Username)
}
} else {
if !IsValidUsername(u.Username) {
return InvalidUserError("username", u.Id, u.Username)
}
}
if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) {
return InvalidUserError("email", u.Id, u.Email)
}
if utf8.RuneCountInString(u.Nickname) > UserNicknameMaxRunes {
return InvalidUserError("nickname", u.Id, u.Nickname)
}
if utf8.RuneCountInString(u.Position) > UserPositionMaxRunes {
return InvalidUserError("position", u.Id, u.Position)
}
if utf8.RuneCountInString(u.FirstName) > UserFirstNameMaxRunes {
return InvalidUserError("first_name", u.Id, u.FirstName)
}
if utf8.RuneCountInString(u.LastName) > UserLastNameMaxRunes {
return InvalidUserError("last_name", u.Id, u.LastName)
}
if u.AuthData != nil && len(*u.AuthData) > UserAuthDataMaxLength {
return InvalidUserError("auth_data", u.Id, u.AuthData)
}
if u.AuthData != nil && *u.AuthData != "" && u.AuthService == "" {
return InvalidUserError("auth_data_type", u.Id, *u.AuthData+" "+u.AuthService)
}
if u.Password != "" && u.AuthData != nil && *u.AuthData != "" {
return InvalidUserError("auth_data_pwd", u.Id, *u.AuthData)
}
if len(u.Password) > UserPasswordMaxLength {
return InvalidUserError("password_limit", u.Id, "")
}
if !IsValidLocale(u.Locale) {
return InvalidUserError("locale", u.Id, u.Locale)
}
if len(u.Timezone) > 0 {
if tzJSON, err := json.Marshal(u.Timezone); err != nil {
return NewAppError("User.IsValid", "model.user.is_valid.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if utf8.RuneCount(tzJSON) > UserTimezoneMaxRunes {
return InvalidUserError("timezone_limit", u.Id, u.Timezone)
}
}
if len(u.Roles) > UserRolesMaxLength {
return NewAppError("User.IsValid", "model.user.is_valid.roles_limit.app_error",
map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest)
}
return nil
}
func InvalidUserError(fieldName, userId string, fieldValue any) *AppError {
id := fmt.Sprintf("model.user.is_valid.%s.app_error", fieldName)
details := ""
if userId != "" {
details = "user_id=" + userId
}
details += fmt.Sprintf(" %s=%v", fieldName, fieldValue)
return NewAppError("User.IsValid", id, nil, details, http.StatusBadRequest)
}
func NormalizeUsername(username string) string {
return strings.ToLower(username)
}
func NormalizeEmail(email string) string {
return strings.ToLower(email)
}
// PreSave will set the Id and Username if missing. It will also fill
// in the CreateAt, UpdateAt times. It will also hash the password. It should
// be run before saving the user to the db.
func (u *User) PreSave() {
if u.Id == "" {
u.Id = NewId()
}
if u.Username == "" {
u.Username = NewId()
}
if u.AuthData != nil && *u.AuthData == "" {
u.AuthData = nil
}
u.Username = SanitizeUnicode(u.Username)
u.FirstName = SanitizeUnicode(u.FirstName)
u.LastName = SanitizeUnicode(u.LastName)
u.Nickname = SanitizeUnicode(u.Nickname)
u.Username = NormalizeUsername(u.Username)
u.Email = NormalizeEmail(u.Email)
if u.CreateAt == 0 {
u.CreateAt = GetMillis()
}
u.UpdateAt = u.CreateAt
u.LastPasswordUpdate = u.CreateAt
u.MfaActive = false
if u.Locale == "" {
u.Locale = DefaultLocale
}
if u.Props == nil {
u.Props = make(map[string]string)
}
if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
u.SetDefaultNotifications()
}
if u.Timezone == nil {
u.Timezone = timezones.DefaultUserTimezone()
}
if u.Password != "" {
u.Password = HashPassword(u.Password)
}
}
// The following are some GraphQL methods necessary to return the
// data in float64 type. The spec doesn't support 64 bit integers,
// so we have to pass the data in float64. The _ at the end is
// a hack to keep the attribute name same in GraphQL schema.
func (u *User) CreateAt_() float64 {
return float64(u.CreateAt)
}
func (u *User) DeleteAt_() float64 {
return float64(u.DeleteAt)
}
func (u *User) UpdateAt_() float64 {
return float64(u.UpdateAt)
}
func (u *User) LastPictureUpdate_() float64 {
return float64(u.LastPictureUpdate)
}
func (u *User) LastPasswordUpdate_() float64 {
return float64(u.LastPasswordUpdate)
}
func (u *User) FailedAttempts_() float64 {
return float64(u.FailedAttempts)
}
func (u *User) LastActivityAt_() float64 {
return float64(u.LastActivityAt)
}
func (u *User) BotLastIconUpdate_() float64 {
return float64(u.BotLastIconUpdate)
}
func (u *User) TermsOfServiceCreateAt_() float64 {
return float64(u.TermsOfServiceCreateAt)
}
// PreUpdate should be run before updating the user in the db.
func (u *User) PreUpdate() {
u.Username = SanitizeUnicode(u.Username)
u.FirstName = SanitizeUnicode(u.FirstName)
u.LastName = SanitizeUnicode(u.LastName)
u.Nickname = SanitizeUnicode(u.Nickname)
u.BotDescription = SanitizeUnicode(u.BotDescription)
u.Username = NormalizeUsername(u.Username)
u.Email = NormalizeEmail(u.Email)
u.UpdateAt = GetMillis()
u.FirstName = SanitizeUnicode(u.FirstName)
u.LastName = SanitizeUnicode(u.LastName)
u.Nickname = SanitizeUnicode(u.Nickname)
u.BotDescription = SanitizeUnicode(u.BotDescription)
if u.AuthData != nil && *u.AuthData == "" {
u.AuthData = nil
}
if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
u.SetDefaultNotifications()
} else if _, ok := u.NotifyProps[MentionKeysNotifyProp]; ok {
// Remove any blank mention keys
splitKeys := strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",")
goodKeys := []string{}
for _, key := range splitKeys {
if key != "" {
goodKeys = append(goodKeys, strings.ToLower(key))
}
}
u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",")
}
}
func (u *User) SetDefaultNotifications() {
u.NotifyProps = make(map[string]string)
u.NotifyProps[EmailNotifyProp] = "true"
u.NotifyProps[PushNotifyProp] = UserNotifyMention
u.NotifyProps[DesktopNotifyProp] = UserNotifyMention
u.NotifyProps[DesktopSoundNotifyProp] = "true"
u.NotifyProps[MentionKeysNotifyProp] = ""
u.NotifyProps[ChannelMentionsNotifyProp] = "true"
u.NotifyProps[PushStatusNotifyProp] = StatusAway
u.NotifyProps[CommentsNotifyProp] = CommentsNotifyNever
u.NotifyProps[FirstNameNotifyProp] = "false"
u.NotifyProps[DesktopThreadsNotifyProp] = UserNotifyAll
u.NotifyProps[EmailThreadsNotifyProp] = UserNotifyAll
u.NotifyProps[PushThreadsNotifyProp] = UserNotifyAll
}
func (u *User) UpdateMentionKeysFromUsername(oldUsername string) {
nonUsernameKeys := []string{}
for _, key := range u.GetMentionKeys() {
if key != oldUsername && key != "@"+oldUsername {
nonUsernameKeys = append(nonUsernameKeys, key)
}
}
u.NotifyProps[MentionKeysNotifyProp] = ""
if len(nonUsernameKeys) > 0 {
u.NotifyProps[MentionKeysNotifyProp] += "," + strings.Join(nonUsernameKeys, ",")
}
}
func (u *User) GetMentionKeys() []string {
var keys []string
for _, key := range strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",") {
trimmedKey := strings.TrimSpace(key)
if trimmedKey == "" {
continue
}
keys = append(keys, trimmedKey)
}
return keys
}
func (u *User) Patch(patch *UserPatch) {
if patch.Username != nil {
u.Username = *patch.Username
}
if patch.Nickname != nil {
u.Nickname = *patch.Nickname
}
if patch.FirstName != nil {
u.FirstName = *patch.FirstName
}
if patch.LastName != nil {
u.LastName = *patch.LastName
}
if patch.Position != nil {
u.Position = *patch.Position
}
if patch.Email != nil {
u.Email = *patch.Email
}
if patch.Props != nil {
u.Props = patch.Props
}
if patch.NotifyProps != nil {
u.NotifyProps = patch.NotifyProps
}
if patch.Locale != nil {
u.Locale = *patch.Locale
}
if patch.Timezone != nil {
u.Timezone = patch.Timezone
}
if patch.RemoteId != nil {
u.RemoteId = patch.RemoteId
}
}
// Generate a valid strong etag so the browser can cache the results
func (u *User) Etag(showFullName, showEmail bool) string {
return Etag(u.Id, u.UpdateAt, u.TermsOfServiceId, u.TermsOfServiceCreateAt, showFullName, showEmail, u.BotLastIconUpdate)
}
// Remove any private data from the user object
func (u *User) Sanitize(options map[string]bool) {
u.Password = ""
u.AuthData = NewString("")
u.MfaSecret = ""
if len(options) != 0 && !options["email"] {
u.Email = ""
}
if len(options) != 0 && !options["fullname"] {
u.FirstName = ""
u.LastName = ""
}
if len(options) != 0 && !options["passwordupdate"] {
u.LastPasswordUpdate = 0
}
if len(options) != 0 && !options["authservice"] {
u.AuthService = ""
}
}
// Remove any input data from the user object that is not user controlled
func (u *User) SanitizeInput(isAdmin bool) {
if !isAdmin {
u.AuthData = NewString("")
u.AuthService = ""
u.EmailVerified = false
}
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
u.MfaActive = false
u.MfaSecret = ""
u.Email = strings.TrimSpace(u.Email)
}
func (u *User) ClearNonProfileFields() {
u.Password = ""
u.AuthData = NewString("")
u.MfaSecret = ""
u.EmailVerified = false
u.AllowMarketing = false
u.NotifyProps = StringMap{}
u.LastPasswordUpdate = 0
u.FailedAttempts = 0
}
func (u *User) SanitizeProfile(options map[string]bool) {
u.ClearNonProfileFields()
u.Sanitize(options)
}
func (u *User) MakeNonNil() {
if u.Props == nil {
u.Props = make(map[string]string)
}
if u.NotifyProps == nil {
u.NotifyProps = make(map[string]string)
}
}
func (u *User) AddNotifyProp(key string, value string) {
u.MakeNonNil()
u.NotifyProps[key] = value
}
func (u *User) SetCustomStatus(cs *CustomStatus) error {
u.MakeNonNil()
statusJSON, jsonErr := json.Marshal(cs)
if jsonErr != nil {
return jsonErr
}
u.Props[UserPropsKeyCustomStatus] = string(statusJSON)
return nil
}
func (u *User) GetCustomStatus() *CustomStatus {
var o *CustomStatus
data := u.Props[UserPropsKeyCustomStatus]
_ = json.Unmarshal([]byte(data), &o)
return o
}
func (u *User) CustomStatus() *CustomStatus {
var o *CustomStatus
data := u.Props[UserPropsKeyCustomStatus]
_ = json.Unmarshal([]byte(data), &o)
return o
}
func (u *User) ClearCustomStatus() {
u.MakeNonNil()
u.Props[UserPropsKeyCustomStatus] = ""
}
func (u *User) GetFullName() string {
if u.FirstName != "" && u.LastName != "" {
return u.FirstName + " " + u.LastName
} else if u.FirstName != "" {
return u.FirstName
} else if u.LastName != "" {
return u.LastName
} else {
return ""
}
}
func (u *User) getDisplayName(baseName, nameFormat string) string {
displayName := baseName
if nameFormat == ShowNicknameFullName {
if u.Nickname != "" {
displayName = u.Nickname
} else if fullName := u.GetFullName(); fullName != "" {
displayName = fullName
}
} else if nameFormat == ShowFullName {
if fullName := u.GetFullName(); fullName != "" {
displayName = fullName
}
}
return displayName
}
func (u *User) GetDisplayName(nameFormat string) string {
displayName := u.Username
return u.getDisplayName(displayName, nameFormat)
}
func (u *User) GetDisplayNameWithPrefix(nameFormat, prefix string) string {
displayName := prefix + u.Username
return u.getDisplayName(displayName, nameFormat)
}
func (u *User) GetRoles() []string {
return strings.Fields(u.Roles)
}
func (u *User) GetRawRoles() string {
return u.Roles
}
func IsValidUserRoles(userRoles string) bool {
roles := strings.Fields(userRoles)
for _, r := range roles {
if !IsValidRoleName(r) {
return false
}
}
// Exclude just the system_admin role explicitly to prevent mistakes
if len(roles) == 1 && roles[0] == "system_admin" {
return false
}
return true
}
// Make sure you actually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsGuest() bool {
return IsInRole(u.Roles, SystemGuestRoleId)
}
func (u *User) IsSystemAdmin() bool {
return IsInRole(u.Roles, SystemAdminRoleId)
}
// Make sure you actually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsInRole(inRole string) bool {
return IsInRole(u.Roles, inRole)
}
// Make sure you actually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func IsInRole(userRoles string, inRole string) bool {
roles := strings.Split(userRoles, " ")
for _, r := range roles {
if r == inRole {
return true
}
}
return false
}
func (u *User) IsSSOUser() bool {
return u.AuthService != "" && u.AuthService != UserAuthServiceEmail
}
func (u *User) IsOAuthUser() bool {
return u.AuthService == ServiceGitlab ||
u.AuthService == ServiceGoogle ||
u.AuthService == ServiceOffice365 ||
u.AuthService == ServiceOpenid
}
func (u *User) IsLDAPUser() bool {
return u.AuthService == UserAuthServiceLdap
}
func (u *User) IsSAMLUser() bool {
return u.AuthService == UserAuthServiceSaml
}
func (u *User) GetPreferredTimezone() string {
return GetPreferredTimezone(u.Timezone)
}
func (u *User) GetTimezoneLocation() *time.Location {
loc, _ := time.LoadLocation(u.GetPreferredTimezone())
if loc == nil {
loc = time.Now().UTC().Location()
}
return loc
}
// IsRemote returns true if the user belongs to a remote cluster (has RemoteId).
func (u *User) IsRemote() bool {
return u.RemoteId != nil && *u.RemoteId != ""
}
// GetRemoteID returns the remote id for this user or "" if not a remote user.
func (u *User) GetRemoteID() string {
if u.RemoteId != nil {
return *u.RemoteId
}
return ""
}
// GetProp fetches a prop value by name.
func (u *User) GetProp(name string) (string, bool) {
val, ok := u.Props[name]
return val, ok
}
// SetProp sets a prop value by name, creating the map if nil.
// Not thread safe.
func (u *User) SetProp(name string, value string) {
if u.Props == nil {
u.Props = make(map[string]string)
}
u.Props[name] = value
}
func (u *User) ToPatch() *UserPatch {
return &UserPatch{
Username: &u.Username, Password: &u.Password,
Nickname: &u.Nickname, FirstName: &u.FirstName, LastName: &u.LastName,
Position: &u.Position, Email: &u.Email,
Props: u.Props, NotifyProps: u.NotifyProps,
Locale: &u.Locale, Timezone: u.Timezone,
}
}
func (u *UserPatch) SetField(fieldName string, fieldValue string) {
switch fieldName {
case "FirstName":
u.FirstName = &fieldValue
case "LastName":
u.LastName = &fieldValue
case "Nickname":
u.Nickname = &fieldValue
case "Email":
u.Email = &fieldValue
case "Position":
u.Position = &fieldValue
case "Username":
u.Username = &fieldValue
}
}
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
return string(hash)
}
var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
var validUsernameCharsForRemote = regexp.MustCompile(`^[a-z0-9\.\-_:]+$`)
var restrictedUsernames = map[string]struct{}{
"all": {},
"channel": {},
"matterbot": {},
"system": {},
}
func IsValidUsername(s string) bool {
if len(s) < UserNameMinLength || len(s) > UserNameMaxLength {
return false
}
if !validUsernameChars.MatchString(s) {
return false
}
_, found := restrictedUsernames[s]
return !found
}
func IsValidUsernameAllowRemote(s string) bool {
if len(s) < UserNameMinLength || len(s) > UserNameMaxLength {
return false
}
if !validUsernameCharsForRemote.MatchString(s) {
return false
}
_, found := restrictedUsernames[s]
return !found
}
func CleanUsername(username string) string {
s := NormalizeUsername(strings.Replace(username, " ", "-", -1))
for _, value := range reservedName {
if s == value {
s = strings.Replace(s, value, "", -1)
}
}
s = strings.TrimSpace(s)
for _, c := range s {
char := fmt.Sprintf("%c", c)
if !validUsernameChars.MatchString(char) {
s = strings.Replace(s, char, "-", -1)
}
}
s = strings.Trim(s, "-")
if !IsValidUsername(s) {
s = "a" + NewId()
mlog.Warn("Generating new username since provided username was invalid",
mlog.String("provided_username", username), mlog.String("new_username", s))
}
return s
}
func IsValidLocale(locale string) bool {
if locale != "" {
if len(locale) > UserLocaleMaxLength {
return false
} else if _, err := language.Parse(locale); err != nil {
return false
}
}
return true
}
//msgp:ignore UserWithGroups
type UserWithGroups struct {
User
GroupIDs *string `json:"-"`
Groups []*Group `json:"groups"`
SchemeGuest bool `json:"scheme_guest"`
SchemeUser bool `json:"scheme_user"`
SchemeAdmin bool `json:"scheme_admin"`
}
func (u *UserWithGroups) GetGroupIDs() []string {
if u.GroupIDs == nil {
return nil
}
trimmed := strings.TrimSpace(*u.GroupIDs)
if trimmed == "" {
return nil
}
return strings.Split(trimmed, ",")
}
//msgp:ignore UsersWithGroupsAndCount
type UsersWithGroupsAndCount struct {
Users []*UserWithGroups `json:"users"`
Count int64 `json:"total_count"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
)
type UserAccessToken struct {
Id string `json:"id"`
Token string `json:"token,omitempty"`
UserId string `json:"user_id"`
Description string `json:"description"`
IsActive bool `json:"is_active"`
}
func (t *UserAccessToken) IsValid() *AppError {
if !IsValidId(t.Id) {
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if len(t.Token) != 26 {
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.token.app_error", nil, "", http.StatusBadRequest)
}
if !IsValidId(t.UserId) {
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(t.Description) > 255 {
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.description.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (t *UserAccessToken) PreSave() {
t.Id = NewId()
t.IsActive = true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0001 uint32
zb0001, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 33 {
err = msgp.ArrayError{Wanted: 33, Got: zb0001}
return
}
z.Id, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
z.CreateAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
z.UpdateAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "UpdateAt")
return
}
z.DeleteAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
z.Username, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Username")
return
}
z.Password, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Password")
return
}
if dc.IsNil() {
err = dc.ReadNil()
if err != nil {
err = msgp.WrapError(err, "AuthData")
return
}
z.AuthData = nil
} else {
if z.AuthData == nil {
z.AuthData = new(string)
}
*z.AuthData, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "AuthData")
return
}
}
z.AuthService, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "AuthService")
return
}
z.Email, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Email")
return
}
z.EmailVerified, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "EmailVerified")
return
}
z.Nickname, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Nickname")
return
}
z.FirstName, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "FirstName")
return
}
z.LastName, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "LastName")
return
}
z.Position, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Position")
return
}
z.Roles, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.AllowMarketing, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "AllowMarketing")
return
}
err = z.Props.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
err = z.NotifyProps.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "NotifyProps")
return
}
z.LastPasswordUpdate, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "LastPasswordUpdate")
return
}
z.LastPictureUpdate, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "LastPictureUpdate")
return
}
z.FailedAttempts, err = dc.ReadInt()
if err != nil {
err = msgp.WrapError(err, "FailedAttempts")
return
}
z.Locale, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Locale")
return
}
err = z.Timezone.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "Timezone")
return
}
z.MfaActive, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "MfaActive")
return
}
z.MfaSecret, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "MfaSecret")
return
}
if dc.IsNil() {
err = dc.ReadNil()
if err != nil {
err = msgp.WrapError(err, "RemoteId")
return
}
z.RemoteId = nil
} else {
if z.RemoteId == nil {
z.RemoteId = new(string)
}
*z.RemoteId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "RemoteId")
return
}
}
z.LastActivityAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
z.IsBot, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "IsBot")
return
}
z.BotDescription, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "BotDescription")
return
}
z.BotLastIconUpdate, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "BotLastIconUpdate")
return
}
z.TermsOfServiceId, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceId")
return
}
z.TermsOfServiceCreateAt, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceCreateAt")
return
}
z.DisableWelcomeEmail, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "DisableWelcomeEmail")
return
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
// array header, size 33
err = en.Append(0xdc, 0x0, 0x21)
if err != nil {
return
}
err = en.WriteString(z.Id)
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
err = en.WriteInt64(z.CreateAt)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
err = en.WriteInt64(z.UpdateAt)
if err != nil {
err = msgp.WrapError(err, "UpdateAt")
return
}
err = en.WriteInt64(z.DeleteAt)
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
err = en.WriteString(z.Username)
if err != nil {
err = msgp.WrapError(err, "Username")
return
}
err = en.WriteString(z.Password)
if err != nil {
err = msgp.WrapError(err, "Password")
return
}
if z.AuthData == nil {
err = en.WriteNil()
if err != nil {
return
}
} else {
err = en.WriteString(*z.AuthData)
if err != nil {
err = msgp.WrapError(err, "AuthData")
return
}
}
err = en.WriteString(z.AuthService)
if err != nil {
err = msgp.WrapError(err, "AuthService")
return
}
err = en.WriteString(z.Email)
if err != nil {
err = msgp.WrapError(err, "Email")
return
}
err = en.WriteBool(z.EmailVerified)
if err != nil {
err = msgp.WrapError(err, "EmailVerified")
return
}
err = en.WriteString(z.Nickname)
if err != nil {
err = msgp.WrapError(err, "Nickname")
return
}
err = en.WriteString(z.FirstName)
if err != nil {
err = msgp.WrapError(err, "FirstName")
return
}
err = en.WriteString(z.LastName)
if err != nil {
err = msgp.WrapError(err, "LastName")
return
}
err = en.WriteString(z.Position)
if err != nil {
err = msgp.WrapError(err, "Position")
return
}
err = en.WriteString(z.Roles)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
err = en.WriteBool(z.AllowMarketing)
if err != nil {
err = msgp.WrapError(err, "AllowMarketing")
return
}
err = z.Props.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
err = z.NotifyProps.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "NotifyProps")
return
}
err = en.WriteInt64(z.LastPasswordUpdate)
if err != nil {
err = msgp.WrapError(err, "LastPasswordUpdate")
return
}
err = en.WriteInt64(z.LastPictureUpdate)
if err != nil {
err = msgp.WrapError(err, "LastPictureUpdate")
return
}
err = en.WriteInt(z.FailedAttempts)
if err != nil {
err = msgp.WrapError(err, "FailedAttempts")
return
}
err = en.WriteString(z.Locale)
if err != nil {
err = msgp.WrapError(err, "Locale")
return
}
err = z.Timezone.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "Timezone")
return
}
err = en.WriteBool(z.MfaActive)
if err != nil {
err = msgp.WrapError(err, "MfaActive")
return
}
err = en.WriteString(z.MfaSecret)
if err != nil {
err = msgp.WrapError(err, "MfaSecret")
return
}
if z.RemoteId == nil {
err = en.WriteNil()
if err != nil {
return
}
} else {
err = en.WriteString(*z.RemoteId)
if err != nil {
err = msgp.WrapError(err, "RemoteId")
return
}
}
err = en.WriteInt64(z.LastActivityAt)
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
err = en.WriteBool(z.IsBot)
if err != nil {
err = msgp.WrapError(err, "IsBot")
return
}
err = en.WriteString(z.BotDescription)
if err != nil {
err = msgp.WrapError(err, "BotDescription")
return
}
err = en.WriteInt64(z.BotLastIconUpdate)
if err != nil {
err = msgp.WrapError(err, "BotLastIconUpdate")
return
}
err = en.WriteString(z.TermsOfServiceId)
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceId")
return
}
err = en.WriteInt64(z.TermsOfServiceCreateAt)
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceCreateAt")
return
}
err = en.WriteBool(z.DisableWelcomeEmail)
if err != nil {
err = msgp.WrapError(err, "DisableWelcomeEmail")
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *User) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// array header, size 33
o = append(o, 0xdc, 0x0, 0x21)
o = msgp.AppendString(o, z.Id)
o = msgp.AppendInt64(o, z.CreateAt)
o = msgp.AppendInt64(o, z.UpdateAt)
o = msgp.AppendInt64(o, z.DeleteAt)
o = msgp.AppendString(o, z.Username)
o = msgp.AppendString(o, z.Password)
if z.AuthData == nil {
o = msgp.AppendNil(o)
} else {
o = msgp.AppendString(o, *z.AuthData)
}
o = msgp.AppendString(o, z.AuthService)
o = msgp.AppendString(o, z.Email)
o = msgp.AppendBool(o, z.EmailVerified)
o = msgp.AppendString(o, z.Nickname)
o = msgp.AppendString(o, z.FirstName)
o = msgp.AppendString(o, z.LastName)
o = msgp.AppendString(o, z.Position)
o = msgp.AppendString(o, z.Roles)
o = msgp.AppendBool(o, z.AllowMarketing)
o, err = z.Props.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
o, err = z.NotifyProps.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "NotifyProps")
return
}
o = msgp.AppendInt64(o, z.LastPasswordUpdate)
o = msgp.AppendInt64(o, z.LastPictureUpdate)
o = msgp.AppendInt(o, z.FailedAttempts)
o = msgp.AppendString(o, z.Locale)
o, err = z.Timezone.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "Timezone")
return
}
o = msgp.AppendBool(o, z.MfaActive)
o = msgp.AppendString(o, z.MfaSecret)
if z.RemoteId == nil {
o = msgp.AppendNil(o)
} else {
o = msgp.AppendString(o, *z.RemoteId)
}
o = msgp.AppendInt64(o, z.LastActivityAt)
o = msgp.AppendBool(o, z.IsBot)
o = msgp.AppendString(o, z.BotDescription)
o = msgp.AppendInt64(o, z.BotLastIconUpdate)
o = msgp.AppendString(o, z.TermsOfServiceId)
o = msgp.AppendInt64(o, z.TermsOfServiceCreateAt)
o = msgp.AppendBool(o, z.DisableWelcomeEmail)
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0001 uint32
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0001 != 33 {
err = msgp.ArrayError{Wanted: 33, Got: zb0001}
return
}
z.Id, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Id")
return
}
z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "CreateAt")
return
}
z.UpdateAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "UpdateAt")
return
}
z.DeleteAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "DeleteAt")
return
}
z.Username, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Username")
return
}
z.Password, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Password")
return
}
if msgp.IsNil(bts) {
bts, err = msgp.ReadNilBytes(bts)
if err != nil {
return
}
z.AuthData = nil
} else {
if z.AuthData == nil {
z.AuthData = new(string)
}
*z.AuthData, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AuthData")
return
}
}
z.AuthService, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AuthService")
return
}
z.Email, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Email")
return
}
z.EmailVerified, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "EmailVerified")
return
}
z.Nickname, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Nickname")
return
}
z.FirstName, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "FirstName")
return
}
z.LastName, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastName")
return
}
z.Position, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Position")
return
}
z.Roles, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Roles")
return
}
z.AllowMarketing, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AllowMarketing")
return
}
bts, err = z.Props.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "Props")
return
}
bts, err = z.NotifyProps.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "NotifyProps")
return
}
z.LastPasswordUpdate, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastPasswordUpdate")
return
}
z.LastPictureUpdate, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastPictureUpdate")
return
}
z.FailedAttempts, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
err = msgp.WrapError(err, "FailedAttempts")
return
}
z.Locale, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Locale")
return
}
bts, err = z.Timezone.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "Timezone")
return
}
z.MfaActive, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "MfaActive")
return
}
z.MfaSecret, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "MfaSecret")
return
}
if msgp.IsNil(bts) {
bts, err = msgp.ReadNilBytes(bts)
if err != nil {
return
}
z.RemoteId = nil
} else {
if z.RemoteId == nil {
z.RemoteId = new(string)
}
*z.RemoteId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "RemoteId")
return
}
}
z.LastActivityAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "LastActivityAt")
return
}
z.IsBot, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "IsBot")
return
}
z.BotDescription, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "BotDescription")
return
}
z.BotLastIconUpdate, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "BotLastIconUpdate")
return
}
z.TermsOfServiceId, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceId")
return
}
z.TermsOfServiceCreateAt, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "TermsOfServiceCreateAt")
return
}
z.DisableWelcomeEmail, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "DisableWelcomeEmail")
return
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *User) Msgsize() (s int) {
s = 3 + msgp.StringPrefixSize + len(z.Id) + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size + msgp.StringPrefixSize + len(z.Username) + msgp.StringPrefixSize + len(z.Password)
if z.AuthData == nil {
s += msgp.NilSize
} else {
s += msgp.StringPrefixSize + len(*z.AuthData)
}
s += msgp.StringPrefixSize + len(z.AuthService) + msgp.StringPrefixSize + len(z.Email) + msgp.BoolSize + msgp.StringPrefixSize + len(z.Nickname) + msgp.StringPrefixSize + len(z.FirstName) + msgp.StringPrefixSize + len(z.LastName) + msgp.StringPrefixSize + len(z.Position) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + z.Props.Msgsize() + z.NotifyProps.Msgsize() + msgp.Int64Size + msgp.Int64Size + msgp.IntSize + msgp.StringPrefixSize + len(z.Locale) + z.Timezone.Msgsize() + msgp.BoolSize + msgp.StringPrefixSize + len(z.MfaSecret)
if z.RemoteId == nil {
s += msgp.NilSize
} else {
s += msgp.StringPrefixSize + len(*z.RemoteId)
}
s += msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size + msgp.BoolSize
return
}
// DecodeMsg implements msgp.Decodable
func (z *UserMap) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0003 uint32
zb0003, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if (*z) == nil {
(*z) = make(UserMap, zb0003)
} else if len((*z)) > 0 {
for key := range *z {
delete((*z), key)
}
}
for zb0003 > 0 {
zb0003--
var zb0001 string
var zb0002 *User
zb0001, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err)
return
}
if dc.IsNil() {
err = dc.ReadNil()
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
zb0002 = nil
} else {
if zb0002 == nil {
zb0002 = new(User)
}
err = zb0002.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
(*z)[zb0001] = zb0002
}
return
}
// EncodeMsg implements msgp.Encodable
func (z UserMap) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteMapHeader(uint32(len(z)))
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0004, zb0005 := range z {
err = en.WriteString(zb0004)
if err != nil {
err = msgp.WrapError(err)
return
}
if zb0005 == nil {
err = en.WriteNil()
if err != nil {
return
}
} else {
err = zb0005.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, zb0004)
return
}
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z UserMap) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendMapHeader(o, uint32(len(z)))
for zb0004, zb0005 := range z {
o = msgp.AppendString(o, zb0004)
if zb0005 == nil {
o = msgp.AppendNil(o)
} else {
o, err = zb0005.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, zb0004)
return
}
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *UserMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0003 uint32
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if (*z) == nil {
(*z) = make(UserMap, zb0003)
} else if len((*z)) > 0 {
for key := range *z {
delete((*z), key)
}
}
for zb0003 > 0 {
var zb0001 string
var zb0002 *User
zb0003--
zb0001, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if msgp.IsNil(bts) {
bts, err = msgp.ReadNilBytes(bts)
if err != nil {
return
}
zb0002 = nil
} else {
if zb0002 == nil {
zb0002 = new(User)
}
bts, err = zb0002.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
(*z)[zb0001] = zb0002
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z UserMap) Msgsize() (s int) {
s = msgp.MapHeaderSize
if z != nil {
for zb0004, zb0005 := range z {
_ = zb0005
s += msgp.StringPrefixSize + len(zb0004)
if zb0005 == nil {
s += msgp.NilSize
} else {
s += zb0005.Msgsize()
}
}
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
)
type UserTermsOfService struct {
UserId string `json:"user_id"`
TermsOfServiceId string `json:"terms_of_service_id"`
CreateAt int64 `json:"create_at"`
}
func (ut *UserTermsOfService) IsValid() *AppError {
if !IsValidId(ut.UserId) {
return InvalidUserTermsOfServiceError("user_id", ut.UserId)
}
if !IsValidId(ut.TermsOfServiceId) {
return InvalidUserTermsOfServiceError("terms_of_service_id", ut.UserId)
}
if ut.CreateAt == 0 {
return InvalidUserTermsOfServiceError("create_at", ut.UserId)
}
return nil
}
func (ut *UserTermsOfService) PreSave() {
if ut.UserId == "" {
ut.UserId = NewId()
}
ut.CreateAt = GetMillis()
}
func InvalidUserTermsOfServiceError(fieldName string, userTermsOfServiceId string) *AppError {
id := fmt.Sprintf("model.user_terms_of_service.is_valid.%s.app_error", fieldName)
details := ""
if userTermsOfServiceId != "" {
details = "user_terms_of_service_user_id=" + userTermsOfServiceId
}
return NewAppError("UserTermsOfService.IsValid", id, nil, details, http.StatusBadRequest)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"crypto/rand"
"database/sql/driver"
"encoding/base32"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/mail"
"net/url"
"os"
"regexp"
"sort"
"strings"
"sync"
"time"
"unicode"
"github.com/pborman/uuid"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
LowercaseLetters = "abcdefghijklmnopqrstuvwxyz"
UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMBERS = "0123456789"
SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~"
BinaryParamKey = "MM_BINARY_PARAMETERS"
NoTranslation = "<untranslated>"
maxPropSizeBytes = 1024 * 1024
)
var ErrMaxPropSizeExceeded = fmt.Errorf("max prop size of %d exceeded", maxPropSizeBytes)
type StringInterface map[string]any
type StringArray []string
func (sa StringArray) Remove(input string) StringArray {
for index := range sa {
if sa[index] == input {
ret := make(StringArray, 0, len(sa)-1)
ret = append(ret, sa[:index]...)
return append(ret, sa[index+1:]...)
}
}
return sa
}
func (sa StringArray) Contains(input string) bool {
for index := range sa {
if sa[index] == input {
return true
}
}
return false
}
func (sa StringArray) Equals(input StringArray) bool {
if len(sa) != len(input) {
return false
}
for index := range sa {
if sa[index] != input[index] {
return false
}
}
return true
}
// Value converts StringArray to database value
func (sa StringArray) Value() (driver.Value, error) {
sz := 0
for i := range sa {
sz += len(sa[i])
if sz > maxPropSizeBytes {
return nil, ErrMaxPropSizeExceeded
}
}
j, err := json.Marshal(sa)
if err != nil {
return nil, err
}
// non utf8 characters are not supported https://mattermost.atlassian.net/browse/MM-41066
return string(j), err
}
// Scan converts database column value to StringArray
func (sa *StringArray) Scan(value any) error {
if value == nil {
return nil
}
buf, ok := value.([]byte)
if ok {
return json.Unmarshal(buf, sa)
}
str, ok := value.(string)
if ok {
return json.Unmarshal([]byte(str), sa)
}
return errors.New("received value is neither a byte slice nor string")
}
// Scan converts database column value to StringMap
func (m *StringMap) Scan(value any) error {
if value == nil {
return nil
}
buf, ok := value.([]byte)
if ok {
return json.Unmarshal(buf, m)
}
str, ok := value.(string)
if ok {
return json.Unmarshal([]byte(str), m)
}
return errors.New("received value is neither a byte slice nor string")
}
// Value converts StringMap to database value
func (m StringMap) Value() (driver.Value, error) {
ok := m[BinaryParamKey]
delete(m, BinaryParamKey)
sz := 0
for k := range m {
sz += len(k) + len(m[k])
if sz > maxPropSizeBytes {
return nil, ErrMaxPropSizeExceeded
}
}
buf, err := json.Marshal(m)
if err != nil {
return nil, err
}
if ok == "true" {
return append([]byte{0x01}, buf...), nil
} else if ok == "false" {
return buf, nil
}
// Key wasn't found. We fall back to the default case.
return string(buf), nil
}
func (StringMap) ImplementsGraphQLType(name string) bool {
return name == "StringMap"
}
func (m StringMap) MarshalJSON() ([]byte, error) {
return json.Marshal((map[string]string)(m))
}
func (m *StringMap) UnmarshalGraphQL(input any) error {
json, ok := input.(map[string]string)
if !ok {
return errors.New("wrong type")
}
*m = json
return nil
}
func (si *StringInterface) Scan(value any) error {
if value == nil {
return nil
}
buf, ok := value.([]byte)
if ok {
return json.Unmarshal(buf, si)
}
str, ok := value.(string)
if ok {
return json.Unmarshal([]byte(str), si)
}
return errors.New("received value is neither a byte slice nor string")
}
// Value converts StringInterface to database value
func (si StringInterface) Value() (driver.Value, error) {
j, err := json.Marshal(si)
if err != nil {
return nil, err
}
if len(j) > maxPropSizeBytes {
return nil, ErrMaxPropSizeExceeded
}
// non utf8 characters are not supported https://mattermost.atlassian.net/browse/MM-41066
return string(j), err
}
func (StringInterface) ImplementsGraphQLType(name string) bool {
return name == "StringInterface"
}
func (si StringInterface) MarshalJSON() ([]byte, error) {
return json.Marshal((map[string]any)(si))
}
func (si *StringInterface) UnmarshalGraphQL(input any) error {
json, ok := input.(map[string]any)
if !ok {
return errors.New("wrong type")
}
*si = json
return nil
}
var translateFunc i18n.TranslateFunc
var translateFuncOnce sync.Once
func AppErrorInit(t i18n.TranslateFunc) {
translateFuncOnce.Do(func() {
translateFunc = t
})
}
type AppError struct {
Id string `json:"id"`
Message string `json:"message"` // Message to be display to the end user without debugging information
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header
StatusCode int `json:"status_code,omitempty"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific
params map[string]any
wrapped error
}
func (er *AppError) Error() string {
var sb strings.Builder
// render the error information
sb.WriteString(er.Where)
sb.WriteString(": ")
if er.Message != NoTranslation {
sb.WriteString(er.Message)
}
// only render the detailed error when it's present
if er.DetailedError != "" {
if er.Message != NoTranslation {
sb.WriteString(", ")
}
sb.WriteString(er.DetailedError)
}
// render the wrapped error
err := er.wrapped
if err != nil {
sb.WriteString(", ")
sb.WriteString(err.Error())
}
return sb.String()
}
func (er *AppError) Translate(T i18n.TranslateFunc) {
if T == nil {
er.Message = er.Id
return
}
if er.params == nil {
er.Message = T(er.Id)
} else {
er.Message = T(er.Id, er.params)
}
}
func (er *AppError) SystemMessage(T i18n.TranslateFunc) string {
if er.params == nil {
return T(er.Id)
}
return T(er.Id, er.params)
}
func (er *AppError) ToJSON() string {
// turn the wrapped error into a detailed message
detailed := er.DetailedError
defer func() {
er.DetailedError = detailed
}()
er.wrappedToDetailed()
b, _ := json.Marshal(er)
return string(b)
}
func (er *AppError) wrappedToDetailed() {
if er.wrapped == nil {
return
}
if er.DetailedError != "" {
er.DetailedError += ", "
}
er.DetailedError += er.wrapped.Error()
}
func (er *AppError) Unwrap() error {
return er.wrapped
}
func (er *AppError) Wrap(err error) *AppError {
er.wrapped = err
return er
}
// AppErrorFromJSON will decode the input and return an AppError
func AppErrorFromJSON(data io.Reader) *AppError {
str := ""
bytes, rerr := io.ReadAll(data)
if rerr != nil {
str = rerr.Error()
} else {
str = string(bytes)
}
decoder := json.NewDecoder(strings.NewReader(str))
var er AppError
err := decoder.Decode(&er)
if err != nil {
return NewAppError("AppErrorFromJSON", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError).Wrap(err)
}
return &er
}
func NewAppError(where string, id string, params map[string]any, details string, status int) *AppError {
ap := &AppError{
Id: id,
params: params,
Message: id,
Where: where,
DetailedError: details,
StatusCode: status,
IsOAuth: false,
}
ap.Translate(translateFunc)
return ap
}
var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769").WithPadding(base32.NoPadding)
// NewId is a globally unique identifier. It is a [A-Z0-9] string 26
// characters long. It is a UUID version 4 Guid that is zbased32 encoded
// without the padding.
func NewId() string {
return encoding.EncodeToString(uuid.NewRandom())
}
// NewRandomTeamName is a NewId that will be a valid team name.
func NewRandomTeamName() string {
teamName := NewId()
for IsReservedTeamName(teamName) {
teamName = NewId()
}
return teamName
}
// NewRandomString returns a random string of the given length.
// The resulting entropy will be (5 * length) bits.
func NewRandomString(length int) string {
data := make([]byte, 1+(length*5/8))
rand.Read(data)
return encoding.EncodeToString(data)[:length]
}
// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
return thisTime.UnixNano() / int64(time.Millisecond)
}
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
func GetTimeForMillis(millis int64) time.Time {
return time.Unix(0, millis*int64(time.Millisecond))
}
// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format
func PadDateStringZeros(dateString string) string {
parts := strings.Split(dateString, "-")
for index, part := range parts {
if len(part) == 1 {
parts[index] = "0" + part
}
}
dateString = strings.Join(parts[:], "-")
return dateString
}
// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day
func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 {
localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset)
resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone)
return GetMillisForTime(resultTime)
}
// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day
func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 {
localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset)
resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone)
return GetMillisForTime(resultTime)
}
func CopyStringMap(originalMap map[string]string) map[string]string {
copyMap := make(map[string]string, len(originalMap))
for k, v := range originalMap {
copyMap[k] = v
}
return copyMap
}
// MapToJSON converts a map to a json string
func MapToJSON(objmap map[string]string) string {
b, _ := json.Marshal(objmap)
return string(b)
}
// MapBoolToJSON converts a map to a json string
func MapBoolToJSON(objmap map[string]bool) string {
b, _ := json.Marshal(objmap)
return string(b)
}
// MapFromJSON will decode the key/value pair map
func MapFromJSON(data io.Reader) map[string]string {
var objmap map[string]string
json.NewDecoder(data).Decode(&objmap)
if objmap == nil {
return make(map[string]string)
}
return objmap
}
// MapFromJSON will decode the key/value pair map
func MapBoolFromJSON(data io.Reader) map[string]bool {
var objmap map[string]bool
json.NewDecoder(data).Decode(&objmap)
if objmap == nil {
return make(map[string]bool)
}
return objmap
}
func ArrayToJSON(objmap []string) string {
b, _ := json.Marshal(objmap)
return string(b)
}
func ArrayFromJSON(data io.Reader) []string {
var objmap []string
json.NewDecoder(data).Decode(&objmap)
if objmap == nil {
return make([]string, 0)
}
return objmap
}
func ArrayFromInterface(data any) []string {
stringArray := []string{}
dataArray, ok := data.([]any)
if !ok {
return stringArray
}
for _, v := range dataArray {
if str, ok := v.(string); ok {
stringArray = append(stringArray, str)
}
}
return stringArray
}
func StringInterfaceToJSON(objmap map[string]any) string {
b, _ := json.Marshal(objmap)
return string(b)
}
func StringInterfaceFromJSON(data io.Reader) map[string]any {
var objmap map[string]any
json.NewDecoder(data).Decode(&objmap)
if objmap == nil {
return make(map[string]any)
}
return objmap
}
// ToJSON serializes an arbitrary data type to JSON, discarding the error.
func ToJSON(v any) []byte {
b, _ := json.Marshal(v)
return b
}
func GetServerIPAddress(iface string) string {
var addrs []net.Addr
if iface == "" {
var err error
addrs, err = net.InterfaceAddrs()
if err != nil {
return ""
}
} else {
interfaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, i := range interfaces {
if i.Name == iface {
addrs, err = i.Addrs()
if err != nil {
return ""
}
break
}
}
}
for _, addr := range addrs {
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() {
if ip.IP.To4() != nil {
return ip.IP.String()
}
}
}
return ""
}
func isLower(s string) bool {
return strings.ToLower(s) == s
}
func IsValidEmail(email string) bool {
if !isLower(email) {
return false
}
if addr, err := mail.ParseAddress(email); err != nil {
return false
} else if addr.Name != "" {
// mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow
return false
}
return true
}
var reservedName = []string{
"admin",
"api",
"channel",
"claim",
"error",
"files",
"help",
"landing",
"login",
"mfa",
"oauth",
"plug",
"plugins",
"post",
"signup",
"boards",
"playbooks",
}
func IsValidChannelIdentifier(s string) bool {
return validSimpleAlphaNum.MatchString(s) && len(s) >= ChannelNameMinLength
}
var (
validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`)
validAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`)
validSimpleAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]*$`)
validSimpleAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
validSimpleAlphaNumHyphenUnderscorePlus = regexp.MustCompile(`^[a-zA-Z0-9+_-]+$`)
)
func isValidAlphaNum(s string) bool {
return validAlphaNum.MatchString(s)
}
func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool {
if withFormat {
return validAlphaNumHyphenUnderscore.MatchString(s)
}
return validSimpleAlphaNumHyphenUnderscore.MatchString(s)
}
func IsValidAlphaNumHyphenUnderscorePlus(s string) bool {
return validSimpleAlphaNumHyphenUnderscorePlus.MatchString(s)
}
func Etag(parts ...any) string {
etag := CurrentVersion
for _, part := range parts {
etag += fmt.Sprintf(".%v", part)
}
return etag
}
var (
validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`)
puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`)
hashtagStart = regexp.MustCompile(`^#{2,}`)
puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`)
)
func ParseHashtags(text string) (string, string) {
words := strings.Fields(text)
hashtagString := ""
plainString := ""
for _, word := range words {
// trim off surrounding punctuation
word = puncStart.ReplaceAllString(word, "")
word = puncEnd.ReplaceAllString(word, "")
// and remove extra pound #s
word = hashtagStart.ReplaceAllString(word, "#")
if validHashtag.MatchString(word) {
hashtagString += " " + word
} else {
plainString += " " + word
}
}
if len(hashtagString) > 1000 {
hashtagString = hashtagString[:999]
lastSpace := strings.LastIndex(hashtagString, " ")
if lastSpace > -1 {
hashtagString = hashtagString[:lastSpace]
} else {
hashtagString = ""
}
}
return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString)
}
func ClearMentionTags(post string) string {
post = strings.Replace(post, "<mention>", "", -1)
post = strings.Replace(post, "</mention>", "", -1)
return post
}
func IsValidHTTPURL(rawURL string) bool {
if strings.Index(rawURL, "http://") != 0 && strings.Index(rawURL, "https://") != 0 {
return false
}
if u, err := url.ParseRequestURI(rawURL); err != nil || u.Scheme == "" || u.Host == "" {
return false
}
return true
}
func IsValidId(value string) bool {
if len(value) != 26 {
return false
}
for _, r := range value {
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
return false
}
}
return true
}
// RemoveDuplicateStrings does an in-place removal of duplicate strings
// from the input slice. The original slice gets modified.
func RemoveDuplicateStrings(in []string) []string {
// In-place de-dup.
// Copied from https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
if len(in) == 0 {
return in
}
sort.Strings(in)
j := 0
for i := 1; i < len(in); i++ {
if in[j] == in[i] {
continue
}
j++
in[j] = in[i]
}
return in[:j+1]
}
func GetPreferredTimezone(timezone StringMap) string {
if timezone["useAutomaticTimezone"] == "true" {
return timezone["automaticTimezone"]
}
return timezone["manualTimezone"]
}
// SanitizeUnicode will remove undesirable Unicode characters from a string.
func SanitizeUnicode(s string) string {
return strings.Map(filterBlocklist, s)
}
// filterBlocklist returns `r` if it is not in the blocklist, otherwise drop (-1).
// Blocklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist
func filterBlocklist(r rune) rune {
const drop = -1
switch r {
case '\u0340', '\u0341': // clones of grave and acute; deprecated in Unicode
return drop
case '\u17A3', '\u17D3': // obsolete characters for Khmer; deprecated in Unicode
return drop
case '\u2028', '\u2029': // line and paragraph separator
return drop
case '\u202A', '\u202B', '\u202C', '\u202D', '\u202E': // BIDI embedding controls
return drop
case '\u206A', '\u206B': // activate/inhibit symmetric swapping; deprecated in Unicode
return drop
case '\u206C', '\u206D': // activate/inhibit Arabic form shaping; deprecated in Unicode
return drop
case '\u206E', '\u206F': // activate/inhibit national digit shapes; deprecated in Unicode
return drop
case '\uFFF9', '\uFFFA', '\uFFFB': // interlinear annotation characters
return drop
case '\uFEFF': // byte order mark
return drop
case '\uFFFC': // object replacement character
return drop
}
// Scoping for musical notation
if r >= 0x0001D173 && r <= 0x0001D17A {
return drop
}
// Language tag code points
if r >= 0x000E0000 && r <= 0x000E007F {
return drop
}
return r
}
func IsCloud() bool {
return os.Getenv("MM_CLOUD_INSTALLATION_ID") != ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"strconv"
"strings"
)
// This is a list of all the current versions including any patches.
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
"7.10.0",
"7.9.0",
"7.8.0",
"7.7.0",
"7.6.0",
"7.5.0",
"7.4.0",
"7.3.0",
"7.2.0",
"7.1.0",
"7.0.0",
"6.7.0",
"6.6.0",
"6.5.0",
"6.4.0",
"6.3.0",
"6.2.0",
"6.1.0",
"6.0.0",
"5.39.0",
"5.38.0",
"5.37.0",
"5.36.0",
"5.35.0",
"5.34.0",
"5.33.0",
"5.32.0",
"5.31.0",
"5.30.0",
"5.29.0",
"5.28.0",
"5.27.0",
"5.26.0",
"5.25.0",
"5.24.0",
"5.23.0",
"5.22.0",
"5.21.0",
"5.20.0",
"5.19.0",
"5.18.0",
"5.17.0",
"5.16.0",
"5.15.0",
"5.14.0",
"5.13.0",
"5.12.0",
"5.11.0",
"5.10.0",
"5.9.0",
"5.8.0",
"5.7.0",
"5.6.0",
"5.5.0",
"5.4.0",
"5.3.0",
"5.2.0",
"5.1.0",
"5.0.0",
"4.10.0",
"4.9.0",
"4.8.1",
"4.8.0",
"4.7.2",
"4.7.1",
"4.7.0",
"4.6.0",
"4.5.0",
"4.4.0",
"4.3.0",
"4.2.0",
"4.1.0",
"4.0.0",
"3.10.0",
"3.9.0",
"3.8.0",
"3.7.0",
"3.6.0",
"3.5.0",
"3.4.0",
"3.3.0",
"3.2.0",
"3.1.0",
"3.0.0",
"2.2.0",
"2.1.0",
"2.0.0",
"1.4.0",
"1.3.0",
"1.2.1",
"1.2.0",
"1.1.0",
"1.0.0",
"0.7.1",
"0.7.0",
"0.6.0",
"0.5.0",
}
var CurrentVersion string = versions[0]
var BuildNumber string
var BuildDate string
var BuildHash string
var BuildHashEnterprise string
var BuildEnterpriseReady string
var BuildHashBoards string
var BuildBoards string
var BuildHashPlaybooks string
var versionsWithoutHotFixes []string
func init() {
versionsWithoutHotFixes = make([]string, 0, len(versions))
seen := make(map[string]string)
for _, version := range versions {
maj, min, _ := SplitVersion(version)
verStr := fmt.Sprintf("%v.%v.0", maj, min)
if seen[verStr] == "" {
versionsWithoutHotFixes = append(versionsWithoutHotFixes, verStr)
seen[verStr] = verStr
}
}
}
func SplitVersion(version string) (int64, int64, int64) {
parts := strings.Split(version, ".")
major := int64(0)
minor := int64(0)
patch := int64(0)
if len(parts) > 0 {
major, _ = strconv.ParseInt(parts[0], 10, 64)
}
if len(parts) > 1 {
minor, _ = strconv.ParseInt(parts[1], 10, 64)
}
if len(parts) > 2 {
patch, _ = strconv.ParseInt(parts[2], 10, 64)
}
return major, minor, patch
}
func GetPreviousVersion(version string) string {
verMajor, verMinor, _ := SplitVersion(version)
verStr := fmt.Sprintf("%v.%v.0", verMajor, verMinor)
for index, v := range versionsWithoutHotFixes {
if v == verStr && len(versionsWithoutHotFixes) > index+1 {
return versionsWithoutHotFixes[index+1]
}
}
return ""
}
func IsCurrentVersion(versionToCheck string) bool {
currentMajor, currentMinor, _ := SplitVersion(CurrentVersion)
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
if toCheckMajor == currentMajor && toCheckMinor == currentMinor {
return true
}
return false
}
func IsPreviousVersionsSupported(versionToCheck string) bool {
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
versionToCheckStr := fmt.Sprintf("%v.%v.0", toCheckMajor, toCheckMinor)
// Current Supported
if versionsWithoutHotFixes[0] == versionToCheckStr {
return true
}
// Current - 1 Supported
if versionsWithoutHotFixes[1] == versionToCheckStr {
return true
}
// Current - 2 Supported
if versionsWithoutHotFixes[2] == versionToCheckStr {
return true
}
// Current - 3 Supported
if versionsWithoutHotFixes[3] == versionToCheckStr {
return true
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/gorilla/websocket"
"github.com/vmihailenco/msgpack/v5"
)
const (
SocketMaxMessageSizeKb = 8 * 1024 // 8KB
PingTimeoutBufferSeconds = 5
)
type msgType int
const (
msgTypeJSON msgType = iota + 1
msgTypePong
msgTypeBinary
)
type writeMessage struct {
msgType msgType
data any
}
const avgReadMsgSizeBytes = 1024
// WebSocketClient stores the necessary information required to
// communicate with a WebSocket endpoint.
// A client must read from PingTimeoutChannel, EventChannel and ResponseChannel to prevent
// deadlocks from occurring in the program.
type WebSocketClient struct {
URL string // The location of the server like "ws://localhost:8065"
APIURL string // The API location of the server like "ws://localhost:8065/api/v3"
ConnectURL string // The WebSocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket"
Conn *websocket.Conn // The WebSocket connection
AuthToken string // The token used to open the WebSocket connection
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
PingTimeoutChannel chan bool // The channel used to signal ping timeouts
EventChannel chan *WebSocketEvent // The channel used to receive various events pushed from the server. For example: typing, posted
ResponseChannel chan *WebSocketResponse // The channel used to receive responses for requests made to the server
ListenError *AppError // A field that is set if there was an abnormal closure of the WebSocket connection
writeChan chan writeMessage
pingTimeoutTimer *time.Timer
quitPingWatchdog chan struct{}
quitWriterChan chan struct{}
resetTimerChan chan struct{}
closed int32
}
// NewWebSocketClient constructs a new WebSocket client with convenience
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, error) {
return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken)
}
func NewReliableWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken, connID string, seqNo int, withAuthHeader bool) (*WebSocketClient, error) {
connectURL := url + APIURLSuffix + "/websocket" + fmt.Sprintf("?connection_id=%s&sequence_number=%d", connID, seqNo)
var header http.Header
if withAuthHeader {
header = http.Header{
"Authorization": []string{"Bearer " + authToken},
}
}
return makeClient(dialer, url, connectURL, authToken, header)
}
// NewWebSocketClientWithDialer constructs a new WebSocket client with convenience
// methods for talking to the server using a custom dialer.
func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, error) {
return makeClient(dialer, url, url+APIURLSuffix+"/websocket", authToken, nil)
}
func makeClient(dialer *websocket.Dialer, url, connectURL, authToken string, header http.Header) (*WebSocketClient, error) {
conn, _, err := dialer.Dial(connectURL, header)
if err != nil {
return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
client := &WebSocketClient{
URL: url,
APIURL: url + APIURLSuffix,
ConnectURL: connectURL,
Conn: conn,
AuthToken: authToken,
Sequence: 1,
PingTimeoutChannel: make(chan bool, 1),
EventChannel: make(chan *WebSocketEvent, 100),
ResponseChannel: make(chan *WebSocketResponse, 100),
writeChan: make(chan writeMessage),
quitPingWatchdog: make(chan struct{}),
quitWriterChan: make(chan struct{}),
resetTimerChan: make(chan struct{}),
}
client.configurePingHandling()
go client.writer()
client.SendMessage(WebsocketAuthenticationChallenge, map[string]any{"token": authToken})
return client, nil
}
// NewWebSocketClient4 constructs a new WebSocket client with convenience
// methods for talking to the server. Uses the v4 endpoint.
func NewWebSocketClient4(url, authToken string) (*WebSocketClient, error) {
return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken)
}
// NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience
// methods for talking to the server using a custom dialer. Uses the v4 endpoint.
func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, error) {
return NewWebSocketClientWithDialer(dialer, url, authToken)
}
// Connect creates a websocket connection with the given ConnectURL.
// This is racy and error-prone should not be used. Use any of the New* functions to create a websocket.
func (wsc *WebSocketClient) Connect() *AppError {
return wsc.ConnectWithDialer(websocket.DefaultDialer)
}
// ConnectWithDialer creates a websocket connection with the given ConnectURL using the dialer.
// This is racy and error-prone and should not be used. Use any of the New* functions to create a websocket.
func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError {
var err error
wsc.Conn, _, err = dialer.Dial(wsc.ConnectURL, nil)
if err != nil {
return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Super racy and should not be done anyways.
// All of this needs to be redesigned for v6.
wsc.configurePingHandling()
// If it has been closed before, we just restart the writer.
if atomic.CompareAndSwapInt32(&wsc.closed, 1, 0) {
wsc.writeChan = make(chan writeMessage)
wsc.quitWriterChan = make(chan struct{})
go wsc.writer()
wsc.resetTimerChan = make(chan struct{})
wsc.quitPingWatchdog = make(chan struct{})
}
wsc.EventChannel = make(chan *WebSocketEvent, 100)
wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
wsc.SendMessage(WebsocketAuthenticationChallenge, map[string]any{"token": wsc.AuthToken})
return nil
}
// Close closes the websocket client. It is recommended that a closed client should not be
// reused again. Rather a new client should be created anew.
func (wsc *WebSocketClient) Close() {
// CAS to 1 and proceed. Return if already 1.
if !atomic.CompareAndSwapInt32(&wsc.closed, 0, 1) {
return
}
wsc.quitWriterChan <- struct{}{}
close(wsc.writeChan)
// We close the connection, which breaks the reader loop.
// Then we let the defer block in the reader do further cleanup.
wsc.Conn.Close()
}
// TODO: un-export the Conn so that Write methods go through the writer
func (wsc *WebSocketClient) writer() {
for {
select {
case msg := <-wsc.writeChan:
switch msg.msgType {
case msgTypeJSON:
wsc.Conn.WriteJSON(msg.data)
case msgTypeBinary:
if data, ok := msg.data.([]byte); ok {
wsc.Conn.WriteMessage(websocket.BinaryMessage, data)
}
case msgTypePong:
wsc.Conn.WriteMessage(websocket.PongMessage, []byte{})
}
case <-wsc.quitWriterChan:
return
}
}
}
// Listen starts the read loop of the websocket client.
func (wsc *WebSocketClient) Listen() {
// This loop can exit in 2 conditions:
// 1. Either the connection breaks naturally.
// 2. Close was explicitly called, which closes the connection manually.
//
// Due to the way the API is written, there is a requirement that a client may NOT
// call Listen at all and can still call Close and Connect.
// Therefore, we let the cleanup of the reader stuff rely on closing the connection
// and then we do the cleanup in the defer block.
//
// First, we close some channels and then CAS to 1 and proceed to close the writer chan also.
// This is needed because then the defer clause does not double-close the writer when (2) happens.
// But if (1) happens, we set the closed bit, and close the rest of the stuff.
go func() {
defer func() {
close(wsc.EventChannel)
close(wsc.ResponseChannel)
close(wsc.quitPingWatchdog)
close(wsc.resetTimerChan)
// We CAS to 1 and proceed.
if !atomic.CompareAndSwapInt32(&wsc.closed, 0, 1) {
return
}
wsc.quitWriterChan <- struct{}{}
close(wsc.writeChan)
wsc.Conn.Close() // This can most likely be removed. Needs to be checked.
}()
var buf bytes.Buffer
buf.Grow(avgReadMsgSizeBytes)
for {
// Reset buffer.
buf.Reset()
_, r, err := wsc.Conn.NextReader()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
// Use pre-allocated buffer.
_, err = buf.ReadFrom(r)
if err != nil {
// This should use a different error ID, but en.json is not imported anyways.
// It's a different bug altogether but we let it be for now.
// See MM-24520.
wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
event, jsonErr := WebSocketEventFromJSON(bytes.NewReader(buf.Bytes()))
if jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
continue
}
if event.IsValid() {
wsc.EventChannel <- event
continue
}
var response WebSocketResponse
if err := json.Unmarshal(buf.Bytes(), &response); err == nil && response.IsValid() {
wsc.ResponseChannel <- &response
continue
}
}
}()
}
func (wsc *WebSocketClient) SendMessage(action string, data map[string]any) {
req := &WebSocketRequest{}
req.Seq = wsc.Sequence
req.Action = action
req.Data = data
wsc.Sequence++
wsc.writeChan <- writeMessage{
msgType: msgTypeJSON,
data: req,
}
}
func (wsc *WebSocketClient) SendBinaryMessage(action string, data map[string]any) error {
req := &WebSocketRequest{}
req.Seq = wsc.Sequence
req.Action = action
req.Data = data
binaryData, err := msgpack.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request to msgpack: %w", err)
}
wsc.Sequence++
wsc.writeChan <- writeMessage{
msgType: msgTypeBinary,
data: binaryData,
}
return nil
}
// UserTyping will push a user_typing event out to all connected users
// who are in the specified channel
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
data := map[string]any{
"channel_id": channelId,
"parent_id": parentId,
}
wsc.SendMessage("user_typing", data)
}
// GetStatuses will return a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}
// GetStatusesByIds will fetch certain user statuses based on ids and return
// a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
data := map[string]any{
"user_ids": userIds,
}
wsc.SendMessage("get_statuses_by_ids", data)
}
func (wsc *WebSocketClient) configurePingHandling() {
wsc.Conn.SetPingHandler(wsc.pingHandler)
wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PingTimeoutBufferSeconds))
go wsc.pingWatchdog()
}
func (wsc *WebSocketClient) pingHandler(appData string) error {
if atomic.LoadInt32(&wsc.closed) == 1 {
return nil
}
wsc.resetTimerChan <- struct{}{}
wsc.writeChan <- writeMessage{
msgType: msgTypePong,
}
return nil
}
// pingWatchdog is used to send values to the PingTimeoutChannel whenever a timeout occurs.
// We use the resetTimerChan from the pingHandler to pass the signal, and then reset the timer
// after draining it. And if the timer naturally expires, we also extend it to prevent it from
// being deadlocked when the resetTimerChan case runs. Because timer.Stop would return false,
// and the code would be forever stuck trying to read from C.
func (wsc *WebSocketClient) pingWatchdog() {
for {
select {
case <-wsc.resetTimerChan:
if !wsc.pingTimeoutTimer.Stop() {
<-wsc.pingTimeoutTimer.C
}
wsc.pingTimeoutTimer.Reset(time.Second * (60 + PingTimeoutBufferSeconds))
case <-wsc.pingTimeoutTimer.C:
wsc.PingTimeoutChannel <- true
wsc.pingTimeoutTimer.Reset(time.Second * (60 + PingTimeoutBufferSeconds))
case <-wsc.quitPingWatchdog:
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"strconv"
)
const (
WebsocketEventTyping = "typing"
WebsocketEventPosted = "posted"
WebsocketEventPostEdited = "post_edited"
WebsocketEventPostDeleted = "post_deleted"
WebsocketEventPostUnread = "post_unread"
WebsocketEventChannelConverted = "channel_converted"
WebsocketEventChannelCreated = "channel_created"
WebsocketEventChannelDeleted = "channel_deleted"
WebsocketEventChannelRestored = "channel_restored"
WebsocketEventChannelUpdated = "channel_updated"
WebsocketEventChannelMemberUpdated = "channel_member_updated"
WebsocketEventChannelSchemeUpdated = "channel_scheme_updated"
WebsocketEventDirectAdded = "direct_added"
WebsocketEventGroupAdded = "group_added"
WebsocketEventNewUser = "new_user"
WebsocketEventAddedToTeam = "added_to_team"
WebsocketEventLeaveTeam = "leave_team"
WebsocketEventUpdateTeam = "update_team"
WebsocketEventDeleteTeam = "delete_team"
WebsocketEventRestoreTeam = "restore_team"
WebsocketEventUpdateTeamScheme = "update_team_scheme"
WebsocketEventUserAdded = "user_added"
WebsocketEventUserUpdated = "user_updated"
WebsocketEventUserRoleUpdated = "user_role_updated"
WebsocketEventMemberroleUpdated = "memberrole_updated"
WebsocketEventUserRemoved = "user_removed"
WebsocketEventPreferenceChanged = "preference_changed"
WebsocketEventPreferencesChanged = "preferences_changed"
WebsocketEventPreferencesDeleted = "preferences_deleted"
WebsocketEventEphemeralMessage = "ephemeral_message"
WebsocketEventStatusChange = "status_change"
WebsocketEventHello = "hello"
WebsocketAuthenticationChallenge = "authentication_challenge"
WebsocketEventReactionAdded = "reaction_added"
WebsocketEventReactionRemoved = "reaction_removed"
WebsocketEventResponse = "response"
WebsocketEventEmojiAdded = "emoji_added"
WebsocketEventChannelViewed = "channel_viewed"
WebsocketEventPluginStatusesChanged = "plugin_statuses_changed"
WebsocketEventPluginEnabled = "plugin_enabled"
WebsocketEventPluginDisabled = "plugin_disabled"
WebsocketEventRoleUpdated = "role_updated"
WebsocketEventLicenseChanged = "license_changed"
WebsocketEventConfigChanged = "config_changed"
WebsocketEventOpenDialog = "open_dialog"
WebsocketEventGuestsDeactivated = "guests_deactivated"
WebsocketEventUserActivationStatusChange = "user_activation_status_change"
WebsocketEventReceivedGroup = "received_group"
WebsocketEventReceivedGroupAssociatedToTeam = "received_group_associated_to_team"
WebsocketEventReceivedGroupNotAssociatedToTeam = "received_group_not_associated_to_team"
WebsocketEventReceivedGroupAssociatedToChannel = "received_group_associated_to_channel"
WebsocketEventReceivedGroupNotAssociatedToChannel = "received_group_not_associated_to_channel"
WebsocketEventGroupMemberDelete = "group_member_deleted"
WebsocketEventGroupMemberAdd = "group_member_add"
WebsocketEventSidebarCategoryCreated = "sidebar_category_created"
WebsocketEventSidebarCategoryUpdated = "sidebar_category_updated"
WebsocketEventSidebarCategoryDeleted = "sidebar_category_deleted"
WebsocketEventSidebarCategoryOrderUpdated = "sidebar_category_order_updated"
WebsocketWarnMetricStatusReceived = "warn_metric_status_received"
WebsocketWarnMetricStatusRemoved = "warn_metric_status_removed"
WebsocketEventCloudPaymentStatusUpdated = "cloud_payment_status_updated"
WebsocketEventCloudSubscriptionChanged = "cloud_subscription_changed"
WebsocketEventThreadUpdated = "thread_updated"
WebsocketEventThreadFollowChanged = "thread_follow_changed"
WebsocketEventThreadReadChanged = "thread_read_changed"
WebsocketFirstAdminVisitMarketplaceStatusReceived = "first_admin_visit_marketplace_status_received"
WebsocketEventDraftCreated = "draft_created"
WebsocketEventDraftUpdated = "draft_updated"
WebsocketEventDraftDeleted = "draft_deleted"
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
WebsocketEventHostedCustomerSignupProgressUpdated = "hosted_customer_signup_progress_updated"
)
type WebSocketMessage interface {
ToJSON() ([]byte, error)
IsValid() bool
EventType() string
}
type WebsocketBroadcast struct {
OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here
UserId string `json:"user_id"` // broadcast only occurs for this user
ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel
TeamId string `json:"team_id"` // broadcast only occurs for users in this team
ConnectionId string `json:"connection_id"` // broadcast only occurs for this connection
OmitConnectionId string `json:"omit_connection_id"` // broadcast is omitted for this connection
ContainsSanitizedData bool `json:"contains_sanitized_data,omitempty"` // broadcast only occurs for non-sysadmins
ContainsSensitiveData bool `json:"contains_sensitive_data,omitempty"` // broadcast only occurs for sysadmins
// ReliableClusterSend indicates whether or not the message should
// be sent through the cluster using the reliable, TCP backed channel.
ReliableClusterSend bool `json:"-"`
}
func (wb *WebsocketBroadcast) copy() *WebsocketBroadcast {
if wb == nil {
return nil
}
var c WebsocketBroadcast
if wb.OmitUsers != nil {
c.OmitUsers = make(map[string]bool, len(wb.OmitUsers))
for k, v := range wb.OmitUsers {
c.OmitUsers[k] = v
}
}
c.UserId = wb.UserId
c.ChannelId = wb.ChannelId
c.TeamId = wb.TeamId
c.OmitConnectionId = wb.OmitConnectionId
c.ContainsSanitizedData = wb.ContainsSanitizedData
c.ContainsSensitiveData = wb.ContainsSensitiveData
return &c
}
type precomputedWebSocketEventJSON struct {
Event json.RawMessage
Data json.RawMessage
Broadcast json.RawMessage
}
func (p *precomputedWebSocketEventJSON) copy() *precomputedWebSocketEventJSON {
if p == nil {
return nil
}
var c precomputedWebSocketEventJSON
if p.Event != nil {
c.Event = make([]byte, len(p.Event))
copy(c.Event, p.Event)
}
if p.Data != nil {
c.Data = make([]byte, len(p.Data))
copy(c.Data, p.Data)
}
if p.Broadcast != nil {
c.Broadcast = make([]byte, len(p.Broadcast))
copy(c.Broadcast, p.Broadcast)
}
return &c
}
// webSocketEventJSON mirrors WebSocketEvent to make some of its unexported fields serializable
type webSocketEventJSON struct {
Event string `json:"event"`
Data map[string]any `json:"data"`
Broadcast *WebsocketBroadcast `json:"broadcast"`
Sequence int64 `json:"seq"`
}
type WebSocketEvent struct {
event string
data map[string]any
broadcast *WebsocketBroadcast
sequence int64
precomputedJSON *precomputedWebSocketEventJSON
}
// PrecomputeJSON precomputes and stores the serialized JSON for all fields other than Sequence.
// This makes ToJSON much more efficient when sending the same event to multiple connections.
func (ev *WebSocketEvent) PrecomputeJSON() *WebSocketEvent {
copy := ev.Copy()
event, _ := json.Marshal(copy.event)
data, _ := json.Marshal(copy.data)
broadcast, _ := json.Marshal(copy.broadcast)
copy.precomputedJSON = &precomputedWebSocketEventJSON{
Event: json.RawMessage(event),
Data: json.RawMessage(data),
Broadcast: json.RawMessage(broadcast),
}
return copy
}
func (ev *WebSocketEvent) Add(key string, value any) {
ev.data[key] = value
}
func NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool, omitConnectionId string) *WebSocketEvent {
return &WebSocketEvent{
event: event,
data: make(map[string]any),
broadcast: &WebsocketBroadcast{
TeamId: teamId,
ChannelId: channelId,
UserId: userId,
OmitUsers: omitUsers,
OmitConnectionId: omitConnectionId},
}
}
func (ev *WebSocketEvent) Copy() *WebSocketEvent {
copy := &WebSocketEvent{
event: ev.event,
data: ev.data,
broadcast: ev.broadcast,
sequence: ev.sequence,
precomputedJSON: ev.precomputedJSON,
}
return copy
}
func (ev *WebSocketEvent) DeepCopy() *WebSocketEvent {
var dataCopy map[string]any
if ev.data != nil {
dataCopy = make(map[string]any, len(ev.data))
for k, v := range ev.data {
dataCopy[k] = v
}
}
copy := &WebSocketEvent{
event: ev.event,
data: dataCopy,
broadcast: ev.broadcast.copy(),
sequence: ev.sequence,
precomputedJSON: ev.precomputedJSON.copy(),
}
return copy
}
func (ev *WebSocketEvent) GetData() map[string]any {
return ev.data
}
func (ev *WebSocketEvent) GetBroadcast() *WebsocketBroadcast {
return ev.broadcast
}
func (ev *WebSocketEvent) GetSequence() int64 {
return ev.sequence
}
func (ev *WebSocketEvent) SetEvent(event string) *WebSocketEvent {
copy := ev.Copy()
copy.event = event
return copy
}
func (ev *WebSocketEvent) SetData(data map[string]any) *WebSocketEvent {
copy := ev.Copy()
copy.data = data
return copy
}
func (ev *WebSocketEvent) SetBroadcast(broadcast *WebsocketBroadcast) *WebSocketEvent {
copy := ev.Copy()
copy.broadcast = broadcast
return copy
}
func (ev *WebSocketEvent) SetSequence(seq int64) *WebSocketEvent {
copy := ev.Copy()
copy.sequence = seq
return copy
}
func (ev *WebSocketEvent) IsValid() bool {
return ev.event != ""
}
func (ev *WebSocketEvent) EventType() string {
return ev.event
}
func (ev *WebSocketEvent) ToJSON() ([]byte, error) {
if ev.precomputedJSON != nil {
return ev.precomputedJSONBuf(), nil
}
return json.Marshal(webSocketEventJSON{
ev.event,
ev.data,
ev.broadcast,
ev.sequence,
})
}
// Encode encodes the event to the given encoder.
func (ev *WebSocketEvent) Encode(enc *json.Encoder) error {
if ev.precomputedJSON != nil {
return enc.Encode(json.RawMessage(ev.precomputedJSONBuf()))
}
return enc.Encode(webSocketEventJSON{
ev.event,
ev.data,
ev.broadcast,
ev.sequence,
})
}
// We write optimal code here sacrificing readability for
// performance.
func (ev *WebSocketEvent) precomputedJSONBuf() []byte {
return []byte(`{"event": ` +
string(ev.precomputedJSON.Event) +
`, "data": ` +
string(ev.precomputedJSON.Data) +
`, "broadcast": ` +
string(ev.precomputedJSON.Broadcast) +
`, "seq": ` +
strconv.Itoa(int(ev.sequence)) +
`}`)
}
func WebSocketEventFromJSON(data io.Reader) (*WebSocketEvent, error) {
var ev WebSocketEvent
var o webSocketEventJSON
if err := json.NewDecoder(data).Decode(&o); err != nil {
return nil, err
}
ev.event = o.Event
if u, ok := o.Data["user"]; ok {
// We need to convert to and from JSON again
// because the user is in the form of a map[string]any.
buf, err := json.Marshal(u)
if err != nil {
return nil, err
}
var user User
if err = json.Unmarshal(buf, &user); err != nil {
return nil, err
}
o.Data["user"] = &user
}
ev.data = o.Data
ev.broadcast = o.Broadcast
ev.sequence = o.Sequence
return &ev, nil
}
// WebSocketResponse represents a response received through the WebSocket
// for a request made to the server. This is available through the ResponseChannel
// channel in WebSocketClient.
type WebSocketResponse struct {
Status string `json:"status"` // The status of the response. For example: OK, FAIL.
SeqReply int64 `json:"seq_reply,omitempty"` // A counter which is incremented for every response sent.
Data map[string]any `json:"data,omitempty"` // The data contained in the response.
Error *AppError `json:"error,omitempty"` // A field that is set if any error has occurred.
}
func (m *WebSocketResponse) Add(key string, value any) {
m.Data[key] = value
}
func NewWebSocketResponse(status string, seqReply int64, data map[string]any) *WebSocketResponse {
return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data}
}
func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse {
return &WebSocketResponse{Status: StatusFail, SeqReply: seqReply, Error: err}
}
func (m *WebSocketResponse) IsValid() bool {
return m.Status != ""
}
func (m *WebSocketResponse) EventType() string {
return WebsocketEventResponse
}
func (m *WebSocketResponse) ToJSON() ([]byte, error) {
return json.Marshal(m)
}
func WebSocketResponseFromJSON(data io.Reader) (*WebSocketResponse, error) {
var o *WebSocketResponse
return o, json.NewDecoder(data).Decode(&o)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/vmihailenco/msgpack/v5"
)
// WebSocketRequest represents a request made to the server through a websocket.
type WebSocketRequest struct {
// Client-provided fields
Seq int64 `json:"seq" msgpack:"seq"` // A counter which is incremented for every request made.
Action string `json:"action" msgpack:"action"` // The action to perform for a request. For example: get_statuses, user_typing.
Data map[string]any `json:"data" msgpack:"data"` // The metadata for an action.
// Server-provided fields
Session Session `json:"-" msgpack:"-"`
T i18n.TranslateFunc `json:"-" msgpack:"-"`
Locale string `json:"-" msgpack:"-"`
}
func (o *WebSocketRequest) Clone() (*WebSocketRequest, error) {
buf, err := msgpack.Marshal(o)
if err != nil {
return nil, err
}
var ret WebSocketRequest
err = msgpack.Unmarshal(buf, &ret)
if err != nil {
return nil, err
}
return &ret, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
import (
"io"
"net/http"
timePkg "time"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/model"
)
type apiTimerLayer struct {
pluginID string
apiImpl API
metrics einterfaces.MetricsInterface
}
func (api *apiTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
if api.metrics != nil {
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
api.metrics.ObservePluginAPIDuration(api.pluginID, name, success, elapsedTime)
}
}
func (api *apiTimerLayer) LoadPluginConfiguration(dest any) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.LoadPluginConfiguration(dest)
api.recordTime(startTime, "LoadPluginConfiguration", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RegisterCommand(command *model.Command) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RegisterCommand(command)
api.recordTime(startTime, "RegisterCommand", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) UnregisterCommand(teamID, trigger string) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.UnregisterCommand(teamID, trigger)
api.recordTime(startTime, "UnregisterCommand", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ExecuteSlashCommand(commandArgs)
api.recordTime(startTime, "ExecuteSlashCommand", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetConfig() *model.Config {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetConfig()
api.recordTime(startTime, "GetConfig", true)
return _returnsA
}
func (api *apiTimerLayer) GetUnsanitizedConfig() *model.Config {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetUnsanitizedConfig()
api.recordTime(startTime, "GetUnsanitizedConfig", true)
return _returnsA
}
func (api *apiTimerLayer) SaveConfig(config *model.Config) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SaveConfig(config)
api.recordTime(startTime, "SaveConfig", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetPluginConfig() map[string]any {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetPluginConfig()
api.recordTime(startTime, "GetPluginConfig", true)
return _returnsA
}
func (api *apiTimerLayer) SavePluginConfig(config map[string]any) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SavePluginConfig(config)
api.recordTime(startTime, "SavePluginConfig", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetBundlePath() (string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetBundlePath()
api.recordTime(startTime, "GetBundlePath", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetLicense() *model.License {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetLicense()
api.recordTime(startTime, "GetLicense", true)
return _returnsA
}
func (api *apiTimerLayer) IsEnterpriseReady() bool {
startTime := timePkg.Now()
_returnsA := api.apiImpl.IsEnterpriseReady()
api.recordTime(startTime, "IsEnterpriseReady", true)
return _returnsA
}
func (api *apiTimerLayer) GetServerVersion() string {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetServerVersion()
api.recordTime(startTime, "GetServerVersion", true)
return _returnsA
}
func (api *apiTimerLayer) GetSystemInstallDate() (int64, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetSystemInstallDate()
api.recordTime(startTime, "GetSystemInstallDate", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetDiagnosticId() string {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetDiagnosticId()
api.recordTime(startTime, "GetDiagnosticId", true)
return _returnsA
}
func (api *apiTimerLayer) GetTelemetryId() string {
startTime := timePkg.Now()
_returnsA := api.apiImpl.GetTelemetryId()
api.recordTime(startTime, "GetTelemetryId", true)
return _returnsA
}
func (api *apiTimerLayer) CreateUser(user *model.User) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateUser(user)
api.recordTime(startTime, "CreateUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteUser(userID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteUser(userID)
api.recordTime(startTime, "DeleteUser", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUsers(options)
api.recordTime(startTime, "GetUsers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUser(userID string) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUser(userID)
api.recordTime(startTime, "GetUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUserByEmail(email string) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUserByEmail(email)
api.recordTime(startTime, "GetUserByEmail", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUserByUsername(name string) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUserByUsername(name)
api.recordTime(startTime, "GetUserByUsername", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUsersByUsernames(usernames)
api.recordTime(startTime, "GetUsersByUsernames", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUsersInTeam(teamID, page, perPage)
api.recordTime(startTime, "GetUsersInTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPreferencesForUser(userID)
api.recordTime(startTime, "GetPreferencesForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.UpdatePreferencesForUser(userID, preferences)
api.recordTime(startTime, "UpdatePreferencesForUser", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeletePreferencesForUser(userID, preferences)
api.recordTime(startTime, "DeletePreferencesForUser", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetSession(sessionID string) (*model.Session, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetSession(sessionID)
api.recordTime(startTime, "GetSession", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateSession(session)
api.recordTime(startTime, "CreateSession", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ExtendSessionExpiry(sessionID string, newExpiry int64) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.ExtendSessionExpiry(sessionID, newExpiry)
api.recordTime(startTime, "ExtendSessionExpiry", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RevokeSession(sessionID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RevokeSession(sessionID)
api.recordTime(startTime, "RevokeSession", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateUserAccessToken(token)
api.recordTime(startTime, "CreateUserAccessToken", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) RevokeUserAccessToken(tokenID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RevokeUserAccessToken(tokenID)
api.recordTime(startTime, "RevokeUserAccessToken", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetTeamIcon(teamID string) ([]byte, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamIcon(teamID)
api.recordTime(startTime, "GetTeamIcon", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SetTeamIcon(teamID string, data []byte) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SetTeamIcon(teamID, data)
api.recordTime(startTime, "SetTeamIcon", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RemoveTeamIcon(teamID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RemoveTeamIcon(teamID)
api.recordTime(startTime, "RemoveTeamIcon", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) UpdateUser(user *model.User) (*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateUser(user)
api.recordTime(startTime, "UpdateUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUserStatus(userID string) (*model.Status, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUserStatus(userID)
api.recordTime(startTime, "GetUserStatus", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUserStatusesByIds(userIds)
api.recordTime(startTime, "GetUserStatusesByIds", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateUserStatus(userID, status)
api.recordTime(startTime, "UpdateUserStatus", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SetUserStatusTimedDND(userId string, endtime int64) (*model.Status, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SetUserStatusTimedDND(userId, endtime)
api.recordTime(startTime, "SetUserStatusTimedDND", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateUserActive(userID string, active bool) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.UpdateUserActive(userID, active)
api.recordTime(startTime, "UpdateUserActive", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.UpdateUserCustomStatus(userID, customStatus)
api.recordTime(startTime, "UpdateUserCustomStatus", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RemoveUserCustomStatus(userID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RemoveUserCustomStatus(userID)
api.recordTime(startTime, "RemoveUserCustomStatus", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUsersInChannel(channelID, sortBy, page, perPage)
api.recordTime(startTime, "GetUsersInChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetLDAPUserAttributes(userID, attributes)
api.recordTime(startTime, "GetLDAPUserAttributes", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateTeam(team)
api.recordTime(startTime, "CreateTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteTeam(teamID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteTeam(teamID)
api.recordTime(startTime, "DeleteTeam", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetTeams() ([]*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeams()
api.recordTime(startTime, "GetTeams", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeam(teamID string) (*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeam(teamID)
api.recordTime(startTime, "GetTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamByName(name string) (*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamByName(name)
api.recordTime(startTime, "GetTeamByName", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamsUnreadForUser(userID)
api.recordTime(startTime, "GetTeamsUnreadForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateTeam(team)
api.recordTime(startTime, "UpdateTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SearchTeams(term string) ([]*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SearchTeams(term)
api.recordTime(startTime, "SearchTeams", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamsForUser(userID)
api.recordTime(startTime, "GetTeamsForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateTeamMember(teamID, userID)
api.recordTime(startTime, "CreateTeamMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateTeamMembers(teamID, userIds, requestorId)
api.recordTime(startTime, "CreateTeamMembers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateTeamMembersGracefully(teamID, userIds, requestorId)
api.recordTime(startTime, "CreateTeamMembersGracefully", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteTeamMember(teamID, userID, requestorId)
api.recordTime(startTime, "DeleteTeamMember", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamMembers(teamID, page, perPage)
api.recordTime(startTime, "GetTeamMembers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamMember(teamID, userID)
api.recordTime(startTime, "GetTeamMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamMembersForUser(userID, page, perPage)
api.recordTime(startTime, "GetTeamMembersForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateTeamMemberRoles(teamID, userID, newRoles)
api.recordTime(startTime, "UpdateTeamMemberRoles", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateChannel(channel)
api.recordTime(startTime, "CreateChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteChannel(channelId string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteChannel(channelId)
api.recordTime(startTime, "DeleteChannel", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPublicChannelsForTeam(teamID, page, perPage)
api.recordTime(startTime, "GetPublicChannelsForTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannel(channelId string) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannel(channelId)
api.recordTime(startTime, "GetChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelByName(teamID, name, includeDeleted)
api.recordTime(startTime, "GetChannelByName", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelByNameForTeamName(teamName, channelName, includeDeleted)
api.recordTime(startTime, "GetChannelByNameForTeamName", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelsForTeamForUser(teamID, userID, includeDeleted)
api.recordTime(startTime, "GetChannelsForTeamForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelStats(channelId string) (*model.ChannelStats, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelStats(channelId)
api.recordTime(startTime, "GetChannelStats", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetDirectChannel(userId1, userId2)
api.recordTime(startTime, "GetDirectChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupChannel(userIds)
api.recordTime(startTime, "GetGroupChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateChannel(channel)
api.recordTime(startTime, "UpdateChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SearchChannels(teamID, term)
api.recordTime(startTime, "SearchChannels", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateChannelSidebarCategory(userID, teamID, newCategory)
api.recordTime(startTime, "CreateChannelSidebarCategory", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelSidebarCategories(userID, teamID)
api.recordTime(startTime, "GetChannelSidebarCategories", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateChannelSidebarCategories(userID, teamID, categories)
api.recordTime(startTime, "UpdateChannelSidebarCategories", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SearchUsers(search)
api.recordTime(startTime, "SearchUsers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SearchPostsInTeam(teamID, paramsList)
api.recordTime(startTime, "SearchPostsInTeam", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.SearchPostsInTeamForUser(teamID, userID, searchParams)
api.recordTime(startTime, "SearchPostsInTeamForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.AddChannelMember(channelId, userID)
api.recordTime(startTime, "AddChannelMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.AddUserToChannel(channelId, userID, asUserId)
api.recordTime(startTime, "AddUserToChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelMember(channelId, userID)
api.recordTime(startTime, "GetChannelMember", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelMembers(channelId, page, perPage)
api.recordTime(startTime, "GetChannelMembers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelMembersByIds(channelId, userIds)
api.recordTime(startTime, "GetChannelMembersByIds", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetChannelMembersForUser(teamID, userID, page, perPage)
api.recordTime(startTime, "GetChannelMembersForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateChannelMemberRoles(channelId, userID, newRoles)
api.recordTime(startTime, "UpdateChannelMemberRoles", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateChannelMemberNotifications(channelId, userID, notifications)
api.recordTime(startTime, "UpdateChannelMemberNotifications", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroup(groupId string) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroup(groupId)
api.recordTime(startTime, "GetGroup", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupByName(name string) (*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupByName(name)
api.recordTime(startTime, "GetGroupByName", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupMemberUsers(groupID, page, perPage)
api.recordTime(startTime, "GetGroupMemberUsers", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupsBySource(groupSource)
api.recordTime(startTime, "GetGroupsBySource", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetGroupsForUser(userID)
api.recordTime(startTime, "GetGroupsForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteChannelMember(channelId, userID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteChannelMember(channelId, userID)
api.recordTime(startTime, "DeleteChannelMember", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreatePost(post)
api.recordTime(startTime, "CreatePost", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.AddReaction(reaction)
api.recordTime(startTime, "AddReaction", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) RemoveReaction(reaction *model.Reaction) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RemoveReaction(reaction)
api.recordTime(startTime, "RemoveReaction", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetReactions(postId string) ([]*model.Reaction, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetReactions(postId)
api.recordTime(startTime, "GetReactions", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SendEphemeralPost(userID string, post *model.Post) *model.Post {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SendEphemeralPost(userID, post)
api.recordTime(startTime, "SendEphemeralPost", true)
return _returnsA
}
func (api *apiTimerLayer) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
startTime := timePkg.Now()
_returnsA := api.apiImpl.UpdateEphemeralPost(userID, post)
api.recordTime(startTime, "UpdateEphemeralPost", true)
return _returnsA
}
func (api *apiTimerLayer) DeleteEphemeralPost(userID, postId string) {
startTime := timePkg.Now()
api.apiImpl.DeleteEphemeralPost(userID, postId)
api.recordTime(startTime, "DeleteEphemeralPost", true)
}
func (api *apiTimerLayer) DeletePost(postId string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeletePost(postId)
api.recordTime(startTime, "DeletePost", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetPostThread(postId string) (*model.PostList, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPostThread(postId)
api.recordTime(startTime, "GetPostThread", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPost(postId string) (*model.Post, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPost(postId)
api.recordTime(startTime, "GetPost", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPostsSince(channelId, time)
api.recordTime(startTime, "GetPostsSince", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPostsAfter(channelId, postId, page, perPage)
api.recordTime(startTime, "GetPostsAfter", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPostsBefore(channelId, postId, page, perPage)
api.recordTime(startTime, "GetPostsBefore", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPostsForChannel(channelId, page, perPage)
api.recordTime(startTime, "GetPostsForChannel", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetTeamStats(teamID)
api.recordTime(startTime, "GetTeamStats", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdatePost(post)
api.recordTime(startTime, "UpdatePost", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetProfileImage(userID string) ([]byte, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetProfileImage(userID)
api.recordTime(startTime, "GetProfileImage", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) SetProfileImage(userID string, data []byte) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SetProfileImage(userID, data)
api.recordTime(startTime, "SetProfileImage", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetEmojiList(sortBy, page, perPage)
api.recordTime(startTime, "GetEmojiList", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetEmojiByName(name string) (*model.Emoji, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetEmojiByName(name)
api.recordTime(startTime, "GetEmojiByName", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetEmoji(emojiId)
api.recordTime(startTime, "GetEmoji", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CopyFileInfos(userID, fileIds)
api.recordTime(startTime, "CopyFileInfos", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetFileInfo(fileId)
api.recordTime(startTime, "GetFileInfo", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetFileInfos(page, perPage, opt)
api.recordTime(startTime, "GetFileInfos", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetFile(fileId string) ([]byte, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetFile(fileId)
api.recordTime(startTime, "GetFile", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetFileLink(fileId string) (string, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetFileLink(fileId)
api.recordTime(startTime, "GetFileLink", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ReadFile(path string) ([]byte, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ReadFile(path)
api.recordTime(startTime, "ReadFile", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB, _returnsC := api.apiImpl.GetEmojiImage(emojiId)
api.recordTime(startTime, "GetEmojiImage", _returnsC == nil)
return _returnsA, _returnsB, _returnsC
}
func (api *apiTimerLayer) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UploadFile(data, channelId, filename)
api.recordTime(startTime, "UploadFile", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.OpenInteractiveDialog(dialog)
api.recordTime(startTime, "OpenInteractiveDialog", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetPlugins() ([]*model.Manifest, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPlugins()
api.recordTime(startTime, "GetPlugins", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) EnablePlugin(id string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.EnablePlugin(id)
api.recordTime(startTime, "EnablePlugin", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) DisablePlugin(id string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DisablePlugin(id)
api.recordTime(startTime, "DisablePlugin", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RemovePlugin(id string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RemovePlugin(id)
api.recordTime(startTime, "RemovePlugin", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetPluginStatus(id)
api.recordTime(startTime, "GetPluginStatus", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.InstallPlugin(file, replace)
api.recordTime(startTime, "InstallPlugin", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) KVSet(key string, value []byte) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.KVSet(key, value)
api.recordTime(startTime, "KVSet", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.KVCompareAndSet(key, oldValue, newValue)
api.recordTime(startTime, "KVCompareAndSet", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.KVCompareAndDelete(key, oldValue)
api.recordTime(startTime, "KVCompareAndDelete", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.KVSetWithOptions(key, value, options)
api.recordTime(startTime, "KVSetWithOptions", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.KVSetWithExpiry(key, value, expireInSeconds)
api.recordTime(startTime, "KVSetWithExpiry", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) KVGet(key string) ([]byte, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.KVGet(key)
api.recordTime(startTime, "KVGet", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) KVDelete(key string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.KVDelete(key)
api.recordTime(startTime, "KVDelete", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) KVDeleteAll() *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.KVDeleteAll()
api.recordTime(startTime, "KVDeleteAll", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) KVList(page, perPage int) ([]string, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.KVList(page, perPage)
api.recordTime(startTime, "KVList", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
startTime := timePkg.Now()
api.apiImpl.PublishWebSocketEvent(event, payload, broadcast)
api.recordTime(startTime, "PublishWebSocketEvent", true)
}
func (api *apiTimerLayer) HasPermissionTo(userID string, permission *model.Permission) bool {
startTime := timePkg.Now()
_returnsA := api.apiImpl.HasPermissionTo(userID, permission)
api.recordTime(startTime, "HasPermissionTo", true)
return _returnsA
}
func (api *apiTimerLayer) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool {
startTime := timePkg.Now()
_returnsA := api.apiImpl.HasPermissionToTeam(userID, teamID, permission)
api.recordTime(startTime, "HasPermissionToTeam", true)
return _returnsA
}
func (api *apiTimerLayer) HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool {
startTime := timePkg.Now()
_returnsA := api.apiImpl.HasPermissionToChannel(userID, channelId, permission)
api.recordTime(startTime, "HasPermissionToChannel", true)
return _returnsA
}
func (api *apiTimerLayer) RolesGrantPermission(roleNames []string, permissionId string) bool {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RolesGrantPermission(roleNames, permissionId)
api.recordTime(startTime, "RolesGrantPermission", true)
return _returnsA
}
func (api *apiTimerLayer) LogDebug(msg string, keyValuePairs ...any) {
startTime := timePkg.Now()
api.apiImpl.LogDebug(msg, keyValuePairs...)
api.recordTime(startTime, "LogDebug", true)
}
func (api *apiTimerLayer) LogInfo(msg string, keyValuePairs ...any) {
startTime := timePkg.Now()
api.apiImpl.LogInfo(msg, keyValuePairs...)
api.recordTime(startTime, "LogInfo", true)
}
func (api *apiTimerLayer) LogError(msg string, keyValuePairs ...any) {
startTime := timePkg.Now()
api.apiImpl.LogError(msg, keyValuePairs...)
api.recordTime(startTime, "LogError", true)
}
func (api *apiTimerLayer) LogWarn(msg string, keyValuePairs ...any) {
startTime := timePkg.Now()
api.apiImpl.LogWarn(msg, keyValuePairs...)
api.recordTime(startTime, "LogWarn", true)
}
func (api *apiTimerLayer) SendMail(to, subject, htmlBody string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.SendMail(to, subject, htmlBody)
api.recordTime(startTime, "SendMail", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateBot(bot)
api.recordTime(startTime, "CreateBot", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.PatchBot(botUserId, botPatch)
api.recordTime(startTime, "PatchBot", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetBot(botUserId, includeDeleted)
api.recordTime(startTime, "GetBot", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetBots(options)
api.recordTime(startTime, "GetBots", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateBotActive(botUserId, active)
api.recordTime(startTime, "UpdateBotActive", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) PermanentDeleteBot(botUserId string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.PermanentDeleteBot(botUserId)
api.recordTime(startTime, "PermanentDeleteBot", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) PluginHTTP(request *http.Request) *http.Response {
startTime := timePkg.Now()
_returnsA := api.apiImpl.PluginHTTP(request)
api.recordTime(startTime, "PluginHTTP", true)
return _returnsA
}
func (api *apiTimerLayer) PublishUserTyping(userID, channelId, parentId string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.PublishUserTyping(userID, channelId, parentId)
api.recordTime(startTime, "PublishUserTyping", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreateCommand(cmd *model.Command) (*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateCommand(cmd)
api.recordTime(startTime, "CreateCommand", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ListCommands(teamID string) ([]*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ListCommands(teamID)
api.recordTime(startTime, "ListCommands", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ListCustomCommands(teamID string) ([]*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ListCustomCommands(teamID)
api.recordTime(startTime, "ListCustomCommands", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ListPluginCommands(teamID string) ([]*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ListPluginCommands(teamID)
api.recordTime(startTime, "ListPluginCommands", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) ListBuiltInCommands() ([]*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.ListBuiltInCommands()
api.recordTime(startTime, "ListBuiltInCommands", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetCommand(commandID string) (*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetCommand(commandID)
api.recordTime(startTime, "GetCommand", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateCommand(commandID, updatedCmd)
api.recordTime(startTime, "UpdateCommand", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteCommand(commandID string) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteCommand(commandID)
api.recordTime(startTime, "DeleteCommand", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateOAuthApp(app)
api.recordTime(startTime, "CreateOAuthApp", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetOAuthApp(appID)
api.recordTime(startTime, "GetOAuthApp", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UpdateOAuthApp(app)
api.recordTime(startTime, "UpdateOAuthApp", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) DeleteOAuthApp(appID string) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.DeleteOAuthApp(appID)
api.recordTime(startTime, "DeleteOAuthApp", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) PublishPluginClusterEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.PublishPluginClusterEvent(ev, opts)
api.recordTime(startTime, "PublishPluginClusterEvent", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
api.recordTime(startTime, "RequestTrialLicense", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetCloudLimits() (*model.ProductLimits, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetCloudLimits()
api.recordTime(startTime, "GetCloudLimits", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) EnsureBotUser(bot *model.Bot) (string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.EnsureBotUser(bot)
api.recordTime(startTime, "EnsureBotUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) RegisterCollectionAndTopic(collectionType, topicType string) error {
startTime := timePkg.Now()
_returnsA := api.apiImpl.RegisterCollectionAndTopic(collectionType, topicType)
api.recordTime(startTime, "RegisterCollectionAndTopic", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreateUploadSession(us)
api.recordTime(startTime, "CreateUploadSession", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.UploadData(us, rd)
api.recordTime(startTime, "UploadData", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) GetUploadSession(uploadID string) (*model.UploadSession, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetUploadSession(uploadID)
api.recordTime(startTime, "GetUploadSession", _returnsB == nil)
return _returnsA, _returnsB
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"github.com/hashicorp/go-plugin"
)
const (
InternalKeyPrefix = "mmi_"
BotUserKey = InternalKeyPrefix + "botid"
)
// Starts the serving of a Mattermost plugin over net/rpc. gRPC is not yet supported.
//
// Call this when your plugin is ready to start.
func ClientMain(pluginImplementation any) {
if impl, ok := pluginImplementation.(interface {
SetAPI(api API)
SetDriver(driver Driver)
}); !ok {
panic("Plugin implementation given must embed plugin.MattermostPlugin")
} else {
impl.SetAPI(nil)
impl.SetDriver(nil)
}
pluginMap := map[string]plugin.Plugin{
"hooks": &hooksPlugin{hooks: pluginImplementation},
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshake,
Plugins: pluginMap,
})
}
type MattermostPlugin struct {
// API exposes the plugin api, and becomes available just prior to the OnActive hook.
API API
Driver Driver
}
// SetAPI persists the given API interface to the plugin. It is invoked just prior to the
// OnActivate hook, exposing the API for use by the plugin.
func (p *MattermostPlugin) SetAPI(api API) {
p.API = api
}
// SetDriver sets the RPC client implementation to talk with the server.
func (p *MattermostPlugin) SetDriver(driver Driver) {
p.Driver = driver
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate go run interface_generator/main.go
package plugin
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/gob"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/rpc"
"os"
"reflect"
"sync"
"github.com/go-sql-driver/mysql"
"github.com/hashicorp/go-plugin"
"github.com/lib/pq"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var hookNameToId map[string]int = make(map[string]int)
type hooksRPCClient struct {
client *rpc.Client
log *mlog.Logger
muxBroker *plugin.MuxBroker
apiImpl API
driver Driver
implemented [TotalHooksID]bool
doneWg sync.WaitGroup
}
type hooksRPCServer struct {
impl any
muxBroker *plugin.MuxBroker
apiRPCClient *apiRPCClient
}
// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin
type hooksPlugin struct {
hooks any
apiImpl API
driverImpl Driver
log *mlog.Logger
}
func (p *hooksPlugin) Server(b *plugin.MuxBroker) (any, error) {
return &hooksRPCServer{impl: p.hooks, muxBroker: b}, nil
}
func (p *hooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (any, error) {
return &hooksRPCClient{client: client,
log: p.log,
muxBroker: b,
apiImpl: p.apiImpl,
driver: p.driverImpl,
}, nil
}
type apiRPCClient struct {
client *rpc.Client
muxBroker *plugin.MuxBroker
}
type apiRPCServer struct {
impl API
muxBroker *plugin.MuxBroker
}
// ErrorString is a fallback for sending unregistered implementations of the error interface across
// rpc. For example, the errorString type from the github.com/pkg/errors package cannot be
// registered since it is not exported, but this precludes common error handling paradigms.
// ErrorString merely preserves the string description of the error, while satisfying the error
// interface itself to allow other registered types (such as model.AppError) to be sent unmodified.
type ErrorString struct {
Code int // Code to map to various error variables
Err string
}
func (e ErrorString) Error() string {
return e.Err
}
func encodableError(err error) error {
if err == nil {
return nil
}
if _, ok := err.(*model.AppError); ok {
return err
}
if _, ok := err.(*pq.Error); ok {
return err
}
if _, ok := err.(*mysql.MySQLError); ok {
return err
}
ret := &ErrorString{
Err: err.Error(),
}
switch err {
case io.EOF:
ret.Code = 1
case sql.ErrNoRows:
ret.Code = 2
case sql.ErrConnDone:
ret.Code = 3
case sql.ErrTxDone:
ret.Code = 4
case driver.ErrSkip:
ret.Code = 5
case driver.ErrBadConn:
ret.Code = 6
case driver.ErrRemoveArgument:
ret.Code = 7
}
return ret
}
func decodableError(err error) error {
if encErr, ok := err.(*ErrorString); ok {
switch encErr.Code {
case 1:
return io.EOF
case 2:
return sql.ErrNoRows
case 3:
return sql.ErrConnDone
case 4:
return sql.ErrTxDone
case 5:
return driver.ErrSkip
case 6:
return driver.ErrBadConn
case 7:
return driver.ErrRemoveArgument
}
}
return err
}
// Registering some types used by MM for encoding/gob used by rpc
func init() {
gob.Register([]*model.SlackAttachment{})
gob.Register([]any{})
gob.Register(map[string]any{})
gob.Register(&model.AppError{})
gob.Register(&pq.Error{})
gob.Register(&mysql.MySQLError{})
gob.Register(&ErrorString{})
gob.Register(&model.AutocompleteDynamicListArg{})
gob.Register(&model.AutocompleteStaticListArg{})
gob.Register(&model.AutocompleteTextArg{})
gob.Register(&model.PreviewPost{})
}
// These enforce compile time checks to make sure types implement the interface
// If you are getting an error here, you probably need to run `make pluginapi` to
// autogenerate RPC glue code
var _ plugin.Plugin = &hooksPlugin{}
var _ Hooks = &hooksRPCClient{}
//
// Below are special cases for hooks or APIs that can not be auto generated
//
func (g *hooksRPCClient) Implemented() (impl []string, err error) {
err = g.client.Call("Plugin.Implemented", struct{}{}, &impl)
for _, hookName := range impl {
if hookId, ok := hookNameToId[hookName]; ok {
g.implemented[hookId] = true
}
}
return
}
// Implemented replies with the names of the hooks that are implemented.
func (s *hooksRPCServer) Implemented(args struct{}, reply *[]string) error {
ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem()
implType := reflect.TypeOf(s.impl)
selfType := reflect.TypeOf(s)
var methods []string
for i := 0; i < ifaceType.NumMethod(); i++ {
method := ifaceType.Method(i)
if m, ok := implType.MethodByName(method.Name); !ok {
continue
} else if m.Type.NumIn() != method.Type.NumIn()+1 {
continue
} else if m.Type.NumOut() != method.Type.NumOut() {
continue
} else {
match := true
for j := 0; j < method.Type.NumIn(); j++ {
if m.Type.In(j+1) != method.Type.In(j) {
match = false
break
}
}
for j := 0; j < method.Type.NumOut(); j++ {
if m.Type.Out(j) != method.Type.Out(j) {
match = false
break
}
}
if !match {
continue
}
}
if _, ok := selfType.MethodByName(method.Name); !ok {
continue
}
methods = append(methods, method.Name)
}
*reply = methods
return encodableError(nil)
}
type Z_OnActivateArgs struct {
APIMuxId uint32
DriverMuxId uint32
}
type Z_OnActivateReturns struct {
A error
}
func (g *hooksRPCClient) OnActivate() error {
muxId := g.muxBroker.NextId()
g.doneWg.Add(1)
go func() {
defer g.doneWg.Done()
g.muxBroker.AcceptAndServe(muxId, &apiRPCServer{
impl: g.apiImpl,
muxBroker: g.muxBroker,
})
}()
nextID := g.muxBroker.NextId()
g.doneWg.Add(1)
go func() {
defer g.doneWg.Done()
g.muxBroker.AcceptAndServe(nextID, &dbRPCServer{
dbImpl: g.driver,
})
}()
_args := &Z_OnActivateArgs{
APIMuxId: muxId,
DriverMuxId: nextID,
}
_returns := &Z_OnActivateReturns{}
if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil {
g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err))
}
return _returns.A
}
func (s *hooksRPCServer) OnActivate(args *Z_OnActivateArgs, returns *Z_OnActivateReturns) error {
connection, err := s.muxBroker.Dial(args.APIMuxId)
if err != nil {
return err
}
conn2, err := s.muxBroker.Dial(args.DriverMuxId)
if err != nil {
return err
}
s.apiRPCClient = &apiRPCClient{
client: rpc.NewClient(connection),
muxBroker: s.muxBroker,
}
dbClient := &dbRPCClient{
client: rpc.NewClient(conn2),
}
if mmplugin, ok := s.impl.(interface {
SetAPI(api API)
SetDriver(driver Driver)
}); ok {
mmplugin.SetAPI(s.apiRPCClient)
mmplugin.SetDriver(dbClient)
}
if mmplugin, ok := s.impl.(interface {
OnConfigurationChange() error
}); ok {
if err := mmplugin.OnConfigurationChange(); err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] call to OnConfigurationChange failed, error: %v", err.Error())
}
}
// Capture output of standard logger because go-plugin
// redirects it.
log.SetOutput(os.Stderr)
if hook, ok := s.impl.(interface {
OnActivate() error
}); ok {
returns.A = encodableError(hook.OnActivate())
}
return nil
}
type Z_LoadPluginConfigurationArgsArgs struct {
}
type Z_LoadPluginConfigurationArgsReturns struct {
A []byte
}
func (g *apiRPCClient) LoadPluginConfiguration(dest any) error {
_args := &Z_LoadPluginConfigurationArgsArgs{}
_returns := &Z_LoadPluginConfigurationArgsReturns{}
if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil {
log.Printf("RPC call to LoadPluginConfiguration API failed: %s", err.Error())
}
if err := json.Unmarshal(_returns.A, dest); err != nil {
log.Printf("LoadPluginConfiguration API failed to unmarshal: %s", err.Error())
}
return nil
}
func (s *apiRPCServer) LoadPluginConfiguration(args *Z_LoadPluginConfigurationArgsArgs, returns *Z_LoadPluginConfigurationArgsReturns) error {
var config any
if hook, ok := s.impl.(interface {
LoadPluginConfiguration(dest any) error
}); ok {
if err := hook.LoadPluginConfiguration(&config); err != nil {
return err
}
}
b, err := json.Marshal(config)
if err != nil {
return err
}
returns.A = b
return nil
}
func init() {
hookNameToId["ServeHTTP"] = ServeHTTPID
}
type Z_ServeHTTPArgs struct {
ResponseWriterStream uint32
Request *http.Request
Context *Context
RequestBodyStream uint32
}
func (g *hooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
if !g.implemented[ServeHTTPID] {
http.NotFound(w, r)
return
}
serveHTTPStreamId := g.muxBroker.NextId()
go func() {
connection, err := g.muxBroker.Accept(serveHTTPStreamId)
if err != nil {
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err))
return
}
defer connection.Close()
rpcServer := rpc.NewServer()
if err := rpcServer.RegisterName("Plugin", &httpResponseWriterRPCServer{w: w, log: g.log}); err != nil {
g.log.Error("Plugin failed to ServeHTTP, couldn't register RPC name", mlog.Err(err))
return
}
rpcServer.ServeConn(connection)
}()
requestBodyStreamId := uint32(0)
if r.Body != nil {
requestBodyStreamId = g.muxBroker.NextId()
go func() {
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
if err != nil {
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connection", mlog.Err(err))
return
}
defer bodyConnection.Close()
serveIOReader(r.Body, bodyConnection)
}()
}
forwardedRequest := &http.Request{
Method: r.Method,
URL: r.URL,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
Host: r.Host,
RemoteAddr: r.RemoteAddr,
RequestURI: r.RequestURI,
}
if err := g.client.Call("Plugin.ServeHTTP", Z_ServeHTTPArgs{
Context: c,
ResponseWriterStream: serveHTTPStreamId,
Request: forwardedRequest,
RequestBodyStream: requestBodyStreamId,
}, nil); err != nil {
g.log.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err))
http.Error(w, "500 internal server error", http.StatusInternalServerError)
}
}
func (s *hooksRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error {
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
return err
}
w := connectHTTPResponseWriter(connection)
defer w.Close()
r := args.Request
if args.RequestBodyStream != 0 {
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
return err
}
r.Body = connectIOReader(connection)
} else {
r.Body = io.NopCloser(&bytes.Buffer{})
}
defer r.Body.Close()
if hook, ok := s.impl.(interface {
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
}); ok {
hook.ServeHTTP(args.Context, w, r)
} else {
http.NotFound(w, r)
}
return nil
}
type Z_PluginHTTPArgs struct {
Request *http.Request
RequestBody []byte
}
type Z_PluginHTTPReturns struct {
Response *http.Response
ResponseBody []byte
}
func (g *apiRPCClient) PluginHTTP(request *http.Request) *http.Response {
forwardedRequest := &http.Request{
Method: request.Method,
URL: request.URL,
Proto: request.Proto,
ProtoMajor: request.ProtoMajor,
ProtoMinor: request.ProtoMinor,
Header: request.Header,
Host: request.Host,
RemoteAddr: request.RemoteAddr,
RequestURI: request.RequestURI,
}
_args := &Z_PluginHTTPArgs{
Request: forwardedRequest,
}
if request.Body != nil {
requestBody, err := io.ReadAll(request.Body)
if err != nil {
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
return nil
}
request.Body.Close()
request.Body = nil
_args.RequestBody = requestBody
}
_returns := &Z_PluginHTTPReturns{}
if err := g.client.Call("Plugin.PluginHTTP", _args, _returns); err != nil {
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
return nil
}
_returns.Response.Body = io.NopCloser(bytes.NewBuffer(_returns.ResponseBody))
return _returns.Response
}
func (s *apiRPCServer) PluginHTTP(args *Z_PluginHTTPArgs, returns *Z_PluginHTTPReturns) error {
args.Request.Body = io.NopCloser(bytes.NewBuffer(args.RequestBody))
if hook, ok := s.impl.(interface {
PluginHTTP(request *http.Request) *http.Response
}); ok {
response := hook.PluginHTTP(args.Request)
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return encodableError(fmt.Errorf("RPC call to PluginHTTP API failed: %s", err.Error()))
}
response.Body.Close()
response.Body = nil
returns.Response = response
returns.ResponseBody = responseBody
} else {
return encodableError(fmt.Errorf("API PluginHTTP called but not implemented"))
}
return nil
}
func init() {
hookNameToId["FileWillBeUploaded"] = FileWillBeUploadedID
}
type Z_FileWillBeUploadedArgs struct {
A *Context
B *model.FileInfo
UploadedFileStream uint32
ReplacementFileStream uint32
}
type Z_FileWillBeUploadedReturns struct {
A *model.FileInfo
B string
}
func (g *hooksRPCClient) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
if !g.implemented[FileWillBeUploadedID] {
return info, ""
}
uploadedFileStreamId := g.muxBroker.NextId()
go func() {
uploadedFileConnection, err := g.muxBroker.Accept(uploadedFileStreamId)
if err != nil {
g.log.Error("Plugin failed to serve upload file stream. MuxBroker could not Accept connection", mlog.Err(err))
return
}
defer uploadedFileConnection.Close()
serveIOReader(file, uploadedFileConnection)
}()
replacementDone := make(chan bool)
replacementFileStreamId := g.muxBroker.NextId()
go func() {
defer close(replacementDone)
replacementFileConnection, err := g.muxBroker.Accept(replacementFileStreamId)
if err != nil {
g.log.Error("Plugin failed to serve replacement file stream. MuxBroker could not Accept connection", mlog.Err(err))
return
}
defer replacementFileConnection.Close()
if _, err := io.Copy(output, replacementFileConnection); err != nil {
g.log.Error("Error reading replacement file.", mlog.Err(err))
}
}()
_args := &Z_FileWillBeUploadedArgs{c, info, uploadedFileStreamId, replacementFileStreamId}
_returns := &Z_FileWillBeUploadedReturns{A: _args.B}
if err := g.client.Call("Plugin.FileWillBeUploaded", _args, _returns); err != nil {
g.log.Error("RPC call FileWillBeUploaded to plugin failed.", mlog.Err(err))
}
// Ensure the io.Copy from the replacementFileConnection above completes.
<-replacementDone
return _returns.A, _returns.B
}
func (s *hooksRPCServer) FileWillBeUploaded(args *Z_FileWillBeUploadedArgs, returns *Z_FileWillBeUploadedReturns) error {
uploadFileConnection, err := s.muxBroker.Dial(args.UploadedFileStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote upload file stream, error: %v", err.Error())
return err
}
defer uploadFileConnection.Close()
fileReader := connectIOReader(uploadFileConnection)
defer fileReader.Close()
replacementFileConnection, err := s.muxBroker.Dial(args.ReplacementFileStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote replacement file stream, error: %v", err.Error())
return err
}
defer replacementFileConnection.Close()
returnFileWriter := replacementFileConnection
if hook, ok := s.impl.(interface {
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
}); ok {
returns.A, returns.B = hook.FileWillBeUploaded(args.A, args.B, fileReader, returnFileWriter)
} else {
return fmt.Errorf("hook FileWillBeUploaded called but not implemented")
}
return nil
}
// MessageWillBePosted is in this file because of the difficulty of identifying which fields need special behaviour.
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
// of fields by older plugins.
func init() {
hookNameToId["MessageWillBePosted"] = MessageWillBePostedID
}
type Z_MessageWillBePostedArgs struct {
A *Context
B *model.Post
}
type Z_MessageWillBePostedReturns struct {
A *model.Post
B string
}
func (g *hooksRPCClient) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
_args := &Z_MessageWillBePostedArgs{c, post}
_returns := &Z_MessageWillBePostedReturns{A: _args.B}
if g.implemented[MessageWillBePostedID] {
if err := g.client.Call("Plugin.MessageWillBePosted", _args, _returns); err != nil {
g.log.Error("RPC call MessageWillBePosted to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) MessageWillBePosted(args *Z_MessageWillBePostedArgs, returns *Z_MessageWillBePostedReturns) error {
if hook, ok := s.impl.(interface {
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
}); ok {
returns.A, returns.B = hook.MessageWillBePosted(args.A, args.B)
} else {
return encodableError(fmt.Errorf("hook MessageWillBePosted called but not implemented"))
}
return nil
}
// MessageWillBeUpdated is in this file because of the difficulty of identifying which fields need special behaviour.
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
// of fields by older plugins.
func init() {
hookNameToId["MessageWillBeUpdated"] = MessageWillBeUpdatedID
}
type Z_MessageWillBeUpdatedArgs struct {
A *Context
B *model.Post
C *model.Post
}
type Z_MessageWillBeUpdatedReturns struct {
A *model.Post
B string
}
func (g *hooksRPCClient) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
_args := &Z_MessageWillBeUpdatedArgs{c, newPost, oldPost}
_returns := &Z_MessageWillBeUpdatedReturns{A: _args.B}
if g.implemented[MessageWillBeUpdatedID] {
if err := g.client.Call("Plugin.MessageWillBeUpdated", _args, _returns); err != nil {
g.log.Error("RPC call MessageWillBeUpdated to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) MessageWillBeUpdated(args *Z_MessageWillBeUpdatedArgs, returns *Z_MessageWillBeUpdatedReturns) error {
if hook, ok := s.impl.(interface {
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
}); ok {
returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("hook MessageWillBeUpdated called but not implemented"))
}
return nil
}
type Z_LogDebugArgs struct {
A string
B []any
}
type Z_LogDebugReturns struct {
}
func (g *apiRPCClient) LogDebug(msg string, keyValuePairs ...any) {
stringifiedPairs := stringifyToObjects(keyValuePairs)
_args := &Z_LogDebugArgs{msg, stringifiedPairs}
_returns := &Z_LogDebugReturns{}
if err := g.client.Call("Plugin.LogDebug", _args, _returns); err != nil {
log.Printf("RPC call to LogDebug API failed: %s", err.Error())
}
}
func (s *apiRPCServer) LogDebug(args *Z_LogDebugArgs, returns *Z_LogDebugReturns) error {
if hook, ok := s.impl.(interface {
LogDebug(msg string, keyValuePairs ...any)
}); ok {
hook.LogDebug(args.A, args.B...)
} else {
return encodableError(fmt.Errorf("API LogDebug called but not implemented"))
}
return nil
}
type Z_LogInfoArgs struct {
A string
B []any
}
type Z_LogInfoReturns struct {
}
func (g *apiRPCClient) LogInfo(msg string, keyValuePairs ...any) {
stringifiedPairs := stringifyToObjects(keyValuePairs)
_args := &Z_LogInfoArgs{msg, stringifiedPairs}
_returns := &Z_LogInfoReturns{}
if err := g.client.Call("Plugin.LogInfo", _args, _returns); err != nil {
log.Printf("RPC call to LogInfo API failed: %s", err.Error())
}
}
func (s *apiRPCServer) LogInfo(args *Z_LogInfoArgs, returns *Z_LogInfoReturns) error {
if hook, ok := s.impl.(interface {
LogInfo(msg string, keyValuePairs ...any)
}); ok {
hook.LogInfo(args.A, args.B...)
} else {
return encodableError(fmt.Errorf("API LogInfo called but not implemented"))
}
return nil
}
type Z_LogWarnArgs struct {
A string
B []any
}
type Z_LogWarnReturns struct {
}
func (g *apiRPCClient) LogWarn(msg string, keyValuePairs ...any) {
stringifiedPairs := stringifyToObjects(keyValuePairs)
_args := &Z_LogWarnArgs{msg, stringifiedPairs}
_returns := &Z_LogWarnReturns{}
if err := g.client.Call("Plugin.LogWarn", _args, _returns); err != nil {
log.Printf("RPC call to LogWarn API failed: %s", err.Error())
}
}
func (s *apiRPCServer) LogWarn(args *Z_LogWarnArgs, returns *Z_LogWarnReturns) error {
if hook, ok := s.impl.(interface {
LogWarn(msg string, keyValuePairs ...any)
}); ok {
hook.LogWarn(args.A, args.B...)
} else {
return encodableError(fmt.Errorf("API LogWarn called but not implemented"))
}
return nil
}
type Z_LogErrorArgs struct {
A string
B []any
}
type Z_LogErrorReturns struct {
}
func (g *apiRPCClient) LogError(msg string, keyValuePairs ...any) {
stringifiedPairs := stringifyToObjects(keyValuePairs)
_args := &Z_LogErrorArgs{msg, stringifiedPairs}
_returns := &Z_LogErrorReturns{}
if err := g.client.Call("Plugin.LogError", _args, _returns); err != nil {
log.Printf("RPC call to LogError API failed: %s", err.Error())
}
}
func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns) error {
if hook, ok := s.impl.(interface {
LogError(msg string, keyValuePairs ...any)
}); ok {
hook.LogError(args.A, args.B...)
} else {
return encodableError(fmt.Errorf("API LogError called but not implemented"))
}
return nil
}
type Z_InstallPluginArgs struct {
PluginStreamID uint32
B bool
}
type Z_InstallPluginReturns struct {
A *model.Manifest
B *model.AppError
}
func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
pluginStreamID := g.muxBroker.NextId()
go func() {
uploadPluginConnection, err := g.muxBroker.Accept(pluginStreamID)
if err != nil {
log.Print("Plugin failed to upload plugin. MuxBroker could not Accept connection", mlog.Err(err))
return
}
defer uploadPluginConnection.Close()
serveIOReader(file, uploadPluginConnection)
}()
_args := &Z_InstallPluginArgs{pluginStreamID, replace}
_returns := &Z_InstallPluginReturns{}
if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil {
log.Print("RPC call InstallPlugin to plugin failed.", mlog.Err(err))
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error {
hook, ok := s.impl.(interface {
InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError)
})
if !ok {
return encodableError(fmt.Errorf("API InstallPlugin called but not implemented"))
}
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
return err
}
pluginReader := connectIOReader(receivePluginConnection)
defer pluginReader.Close()
returns.A, returns.B = hook.InstallPlugin(pluginReader, args.B)
return nil
}
type Z_UploadDataArgs struct {
A *model.UploadSession
PluginStreamID uint32
}
type Z_UploadDataReturns struct {
A *model.FileInfo
B error
}
func (g *apiRPCClient) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
pluginStreamID := g.muxBroker.NextId()
go func() {
pluginConnection, err := g.muxBroker.Accept(pluginStreamID)
if err != nil {
log.Print("Failed to upload data. MuxBroker could not Accept connection", mlog.Err(err))
return
}
defer pluginConnection.Close()
serveIOReader(rd, pluginConnection)
}()
_args := &Z_UploadDataArgs{us, pluginStreamID}
_returns := &Z_UploadDataReturns{}
if err := g.client.Call("Plugin.UploadData", _args, _returns); err != nil {
log.Print("RPC call UploadData to plugin failed.", mlog.Err(err))
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UploadData(args *Z_UploadDataArgs, returns *Z_UploadDataReturns) error {
hook, ok := s.impl.(interface {
UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error)
})
if !ok {
return encodableError(fmt.Errorf("API UploadData called but not implemented"))
}
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
return err
}
pluginReader := connectIOReader(receivePluginConnection)
defer pluginReader.Close()
returns.A, returns.B = hook.UploadData(args.A, pluginReader)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
import (
"fmt"
"log"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func init() {
hookNameToId["OnDeactivate"] = OnDeactivateID
}
type Z_OnDeactivateArgs struct {
}
type Z_OnDeactivateReturns struct {
A error
}
func (g *hooksRPCClient) OnDeactivate() error {
_args := &Z_OnDeactivateArgs{}
_returns := &Z_OnDeactivateReturns{}
if g.implemented[OnDeactivateID] {
if err := g.client.Call("Plugin.OnDeactivate", _args, _returns); err != nil {
g.log.Error("RPC call OnDeactivate to plugin failed.", mlog.Err(err))
}
}
return _returns.A
}
func (s *hooksRPCServer) OnDeactivate(args *Z_OnDeactivateArgs, returns *Z_OnDeactivateReturns) error {
if hook, ok := s.impl.(interface {
OnDeactivate() error
}); ok {
returns.A = hook.OnDeactivate()
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("Hook OnDeactivate called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnConfigurationChange"] = OnConfigurationChangeID
}
type Z_OnConfigurationChangeArgs struct {
}
type Z_OnConfigurationChangeReturns struct {
A error
}
func (g *hooksRPCClient) OnConfigurationChange() error {
_args := &Z_OnConfigurationChangeArgs{}
_returns := &Z_OnConfigurationChangeReturns{}
if g.implemented[OnConfigurationChangeID] {
if err := g.client.Call("Plugin.OnConfigurationChange", _args, _returns); err != nil {
g.log.Error("RPC call OnConfigurationChange to plugin failed.", mlog.Err(err))
}
}
return _returns.A
}
func (s *hooksRPCServer) OnConfigurationChange(args *Z_OnConfigurationChangeArgs, returns *Z_OnConfigurationChangeReturns) error {
if hook, ok := s.impl.(interface {
OnConfigurationChange() error
}); ok {
returns.A = hook.OnConfigurationChange()
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("Hook OnConfigurationChange called but not implemented."))
}
return nil
}
func init() {
hookNameToId["ExecuteCommand"] = ExecuteCommandID
}
type Z_ExecuteCommandArgs struct {
A *Context
B *model.CommandArgs
}
type Z_ExecuteCommandReturns struct {
A *model.CommandResponse
B *model.AppError
}
func (g *hooksRPCClient) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
_args := &Z_ExecuteCommandArgs{c, args}
_returns := &Z_ExecuteCommandReturns{}
if g.implemented[ExecuteCommandID] {
if err := g.client.Call("Plugin.ExecuteCommand", _args, _returns); err != nil {
g.log.Error("RPC call ExecuteCommand to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) ExecuteCommand(args *Z_ExecuteCommandArgs, returns *Z_ExecuteCommandReturns) error {
if hook, ok := s.impl.(interface {
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
}); ok {
returns.A, returns.B = hook.ExecuteCommand(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook ExecuteCommand called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasBeenCreated"] = UserHasBeenCreatedID
}
type Z_UserHasBeenCreatedArgs struct {
A *Context
B *model.User
}
type Z_UserHasBeenCreatedReturns struct {
}
func (g *hooksRPCClient) UserHasBeenCreated(c *Context, user *model.User) {
_args := &Z_UserHasBeenCreatedArgs{c, user}
_returns := &Z_UserHasBeenCreatedReturns{}
if g.implemented[UserHasBeenCreatedID] {
if err := g.client.Call("Plugin.UserHasBeenCreated", _args, _returns); err != nil {
g.log.Error("RPC call UserHasBeenCreated to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasBeenCreated(args *Z_UserHasBeenCreatedArgs, returns *Z_UserHasBeenCreatedReturns) error {
if hook, ok := s.impl.(interface {
UserHasBeenCreated(c *Context, user *model.User)
}); ok {
hook.UserHasBeenCreated(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook UserHasBeenCreated called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserWillLogIn"] = UserWillLogInID
}
type Z_UserWillLogInArgs struct {
A *Context
B *model.User
}
type Z_UserWillLogInReturns struct {
A string
}
func (g *hooksRPCClient) UserWillLogIn(c *Context, user *model.User) string {
_args := &Z_UserWillLogInArgs{c, user}
_returns := &Z_UserWillLogInReturns{}
if g.implemented[UserWillLogInID] {
if err := g.client.Call("Plugin.UserWillLogIn", _args, _returns); err != nil {
g.log.Error("RPC call UserWillLogIn to plugin failed.", mlog.Err(err))
}
}
return _returns.A
}
func (s *hooksRPCServer) UserWillLogIn(args *Z_UserWillLogInArgs, returns *Z_UserWillLogInReturns) error {
if hook, ok := s.impl.(interface {
UserWillLogIn(c *Context, user *model.User) string
}); ok {
returns.A = hook.UserWillLogIn(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook UserWillLogIn called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasLoggedIn"] = UserHasLoggedInID
}
type Z_UserHasLoggedInArgs struct {
A *Context
B *model.User
}
type Z_UserHasLoggedInReturns struct {
}
func (g *hooksRPCClient) UserHasLoggedIn(c *Context, user *model.User) {
_args := &Z_UserHasLoggedInArgs{c, user}
_returns := &Z_UserHasLoggedInReturns{}
if g.implemented[UserHasLoggedInID] {
if err := g.client.Call("Plugin.UserHasLoggedIn", _args, _returns); err != nil {
g.log.Error("RPC call UserHasLoggedIn to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasLoggedIn(args *Z_UserHasLoggedInArgs, returns *Z_UserHasLoggedInReturns) error {
if hook, ok := s.impl.(interface {
UserHasLoggedIn(c *Context, user *model.User)
}); ok {
hook.UserHasLoggedIn(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook UserHasLoggedIn called but not implemented."))
}
return nil
}
func init() {
hookNameToId["MessageHasBeenPosted"] = MessageHasBeenPostedID
}
type Z_MessageHasBeenPostedArgs struct {
A *Context
B *model.Post
}
type Z_MessageHasBeenPostedReturns struct {
}
func (g *hooksRPCClient) MessageHasBeenPosted(c *Context, post *model.Post) {
_args := &Z_MessageHasBeenPostedArgs{c, post}
_returns := &Z_MessageHasBeenPostedReturns{}
if g.implemented[MessageHasBeenPostedID] {
if err := g.client.Call("Plugin.MessageHasBeenPosted", _args, _returns); err != nil {
g.log.Error("RPC call MessageHasBeenPosted to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) MessageHasBeenPosted(args *Z_MessageHasBeenPostedArgs, returns *Z_MessageHasBeenPostedReturns) error {
if hook, ok := s.impl.(interface {
MessageHasBeenPosted(c *Context, post *model.Post)
}); ok {
hook.MessageHasBeenPosted(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook MessageHasBeenPosted called but not implemented."))
}
return nil
}
func init() {
hookNameToId["MessageHasBeenUpdated"] = MessageHasBeenUpdatedID
}
type Z_MessageHasBeenUpdatedArgs struct {
A *Context
B *model.Post
C *model.Post
}
type Z_MessageHasBeenUpdatedReturns struct {
}
func (g *hooksRPCClient) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
_args := &Z_MessageHasBeenUpdatedArgs{c, newPost, oldPost}
_returns := &Z_MessageHasBeenUpdatedReturns{}
if g.implemented[MessageHasBeenUpdatedID] {
if err := g.client.Call("Plugin.MessageHasBeenUpdated", _args, _returns); err != nil {
g.log.Error("RPC call MessageHasBeenUpdated to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) MessageHasBeenUpdated(args *Z_MessageHasBeenUpdatedArgs, returns *Z_MessageHasBeenUpdatedReturns) error {
if hook, ok := s.impl.(interface {
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
}); ok {
hook.MessageHasBeenUpdated(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook MessageHasBeenUpdated called but not implemented."))
}
return nil
}
func init() {
hookNameToId["ChannelHasBeenCreated"] = ChannelHasBeenCreatedID
}
type Z_ChannelHasBeenCreatedArgs struct {
A *Context
B *model.Channel
}
type Z_ChannelHasBeenCreatedReturns struct {
}
func (g *hooksRPCClient) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
_args := &Z_ChannelHasBeenCreatedArgs{c, channel}
_returns := &Z_ChannelHasBeenCreatedReturns{}
if g.implemented[ChannelHasBeenCreatedID] {
if err := g.client.Call("Plugin.ChannelHasBeenCreated", _args, _returns); err != nil {
g.log.Error("RPC call ChannelHasBeenCreated to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) ChannelHasBeenCreated(args *Z_ChannelHasBeenCreatedArgs, returns *Z_ChannelHasBeenCreatedReturns) error {
if hook, ok := s.impl.(interface {
ChannelHasBeenCreated(c *Context, channel *model.Channel)
}); ok {
hook.ChannelHasBeenCreated(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook ChannelHasBeenCreated called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasJoinedChannel"] = UserHasJoinedChannelID
}
type Z_UserHasJoinedChannelArgs struct {
A *Context
B *model.ChannelMember
C *model.User
}
type Z_UserHasJoinedChannelReturns struct {
}
func (g *hooksRPCClient) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
_args := &Z_UserHasJoinedChannelArgs{c, channelMember, actor}
_returns := &Z_UserHasJoinedChannelReturns{}
if g.implemented[UserHasJoinedChannelID] {
if err := g.client.Call("Plugin.UserHasJoinedChannel", _args, _returns); err != nil {
g.log.Error("RPC call UserHasJoinedChannel to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasJoinedChannel(args *Z_UserHasJoinedChannelArgs, returns *Z_UserHasJoinedChannelReturns) error {
if hook, ok := s.impl.(interface {
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}); ok {
hook.UserHasJoinedChannel(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook UserHasJoinedChannel called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasLeftChannel"] = UserHasLeftChannelID
}
type Z_UserHasLeftChannelArgs struct {
A *Context
B *model.ChannelMember
C *model.User
}
type Z_UserHasLeftChannelReturns struct {
}
func (g *hooksRPCClient) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
_args := &Z_UserHasLeftChannelArgs{c, channelMember, actor}
_returns := &Z_UserHasLeftChannelReturns{}
if g.implemented[UserHasLeftChannelID] {
if err := g.client.Call("Plugin.UserHasLeftChannel", _args, _returns); err != nil {
g.log.Error("RPC call UserHasLeftChannel to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasLeftChannel(args *Z_UserHasLeftChannelArgs, returns *Z_UserHasLeftChannelReturns) error {
if hook, ok := s.impl.(interface {
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}); ok {
hook.UserHasLeftChannel(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook UserHasLeftChannel called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasJoinedTeam"] = UserHasJoinedTeamID
}
type Z_UserHasJoinedTeamArgs struct {
A *Context
B *model.TeamMember
C *model.User
}
type Z_UserHasJoinedTeamReturns struct {
}
func (g *hooksRPCClient) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
_args := &Z_UserHasJoinedTeamArgs{c, teamMember, actor}
_returns := &Z_UserHasJoinedTeamReturns{}
if g.implemented[UserHasJoinedTeamID] {
if err := g.client.Call("Plugin.UserHasJoinedTeam", _args, _returns); err != nil {
g.log.Error("RPC call UserHasJoinedTeam to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasJoinedTeam(args *Z_UserHasJoinedTeamArgs, returns *Z_UserHasJoinedTeamReturns) error {
if hook, ok := s.impl.(interface {
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}); ok {
hook.UserHasJoinedTeam(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook UserHasJoinedTeam called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasLeftTeam"] = UserHasLeftTeamID
}
type Z_UserHasLeftTeamArgs struct {
A *Context
B *model.TeamMember
C *model.User
}
type Z_UserHasLeftTeamReturns struct {
}
func (g *hooksRPCClient) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
_args := &Z_UserHasLeftTeamArgs{c, teamMember, actor}
_returns := &Z_UserHasLeftTeamReturns{}
if g.implemented[UserHasLeftTeamID] {
if err := g.client.Call("Plugin.UserHasLeftTeam", _args, _returns); err != nil {
g.log.Error("RPC call UserHasLeftTeam to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) UserHasLeftTeam(args *Z_UserHasLeftTeamArgs, returns *Z_UserHasLeftTeamReturns) error {
if hook, ok := s.impl.(interface {
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}); ok {
hook.UserHasLeftTeam(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook UserHasLeftTeam called but not implemented."))
}
return nil
}
func init() {
hookNameToId["ReactionHasBeenAdded"] = ReactionHasBeenAddedID
}
type Z_ReactionHasBeenAddedArgs struct {
A *Context
B *model.Reaction
}
type Z_ReactionHasBeenAddedReturns struct {
}
func (g *hooksRPCClient) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
_args := &Z_ReactionHasBeenAddedArgs{c, reaction}
_returns := &Z_ReactionHasBeenAddedReturns{}
if g.implemented[ReactionHasBeenAddedID] {
if err := g.client.Call("Plugin.ReactionHasBeenAdded", _args, _returns); err != nil {
g.log.Error("RPC call ReactionHasBeenAdded to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) ReactionHasBeenAdded(args *Z_ReactionHasBeenAddedArgs, returns *Z_ReactionHasBeenAddedReturns) error {
if hook, ok := s.impl.(interface {
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
}); ok {
hook.ReactionHasBeenAdded(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook ReactionHasBeenAdded called but not implemented."))
}
return nil
}
func init() {
hookNameToId["ReactionHasBeenRemoved"] = ReactionHasBeenRemovedID
}
type Z_ReactionHasBeenRemovedArgs struct {
A *Context
B *model.Reaction
}
type Z_ReactionHasBeenRemovedReturns struct {
}
func (g *hooksRPCClient) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
_args := &Z_ReactionHasBeenRemovedArgs{c, reaction}
_returns := &Z_ReactionHasBeenRemovedReturns{}
if g.implemented[ReactionHasBeenRemovedID] {
if err := g.client.Call("Plugin.ReactionHasBeenRemoved", _args, _returns); err != nil {
g.log.Error("RPC call ReactionHasBeenRemoved to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) ReactionHasBeenRemoved(args *Z_ReactionHasBeenRemovedArgs, returns *Z_ReactionHasBeenRemovedReturns) error {
if hook, ok := s.impl.(interface {
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
}); ok {
hook.ReactionHasBeenRemoved(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook ReactionHasBeenRemoved called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnPluginClusterEvent"] = OnPluginClusterEventID
}
type Z_OnPluginClusterEventArgs struct {
A *Context
B model.PluginClusterEvent
}
type Z_OnPluginClusterEventReturns struct {
}
func (g *hooksRPCClient) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
_args := &Z_OnPluginClusterEventArgs{c, ev}
_returns := &Z_OnPluginClusterEventReturns{}
if g.implemented[OnPluginClusterEventID] {
if err := g.client.Call("Plugin.OnPluginClusterEvent", _args, _returns); err != nil {
g.log.Error("RPC call OnPluginClusterEvent to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnPluginClusterEvent(args *Z_OnPluginClusterEventArgs, returns *Z_OnPluginClusterEventReturns) error {
if hook, ok := s.impl.(interface {
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
}); ok {
hook.OnPluginClusterEvent(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook OnPluginClusterEvent called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnWebSocketConnect"] = OnWebSocketConnectID
}
type Z_OnWebSocketConnectArgs struct {
A string
B string
}
type Z_OnWebSocketConnectReturns struct {
}
func (g *hooksRPCClient) OnWebSocketConnect(webConnID, userID string) {
_args := &Z_OnWebSocketConnectArgs{webConnID, userID}
_returns := &Z_OnWebSocketConnectReturns{}
if g.implemented[OnWebSocketConnectID] {
if err := g.client.Call("Plugin.OnWebSocketConnect", _args, _returns); err != nil {
g.log.Error("RPC call OnWebSocketConnect to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnWebSocketConnect(args *Z_OnWebSocketConnectArgs, returns *Z_OnWebSocketConnectReturns) error {
if hook, ok := s.impl.(interface {
OnWebSocketConnect(webConnID, userID string)
}); ok {
hook.OnWebSocketConnect(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook OnWebSocketConnect called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnWebSocketDisconnect"] = OnWebSocketDisconnectID
}
type Z_OnWebSocketDisconnectArgs struct {
A string
B string
}
type Z_OnWebSocketDisconnectReturns struct {
}
func (g *hooksRPCClient) OnWebSocketDisconnect(webConnID, userID string) {
_args := &Z_OnWebSocketDisconnectArgs{webConnID, userID}
_returns := &Z_OnWebSocketDisconnectReturns{}
if g.implemented[OnWebSocketDisconnectID] {
if err := g.client.Call("Plugin.OnWebSocketDisconnect", _args, _returns); err != nil {
g.log.Error("RPC call OnWebSocketDisconnect to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnWebSocketDisconnect(args *Z_OnWebSocketDisconnectArgs, returns *Z_OnWebSocketDisconnectReturns) error {
if hook, ok := s.impl.(interface {
OnWebSocketDisconnect(webConnID, userID string)
}); ok {
hook.OnWebSocketDisconnect(args.A, args.B)
} else {
return encodableError(fmt.Errorf("Hook OnWebSocketDisconnect called but not implemented."))
}
return nil
}
func init() {
hookNameToId["WebSocketMessageHasBeenPosted"] = WebSocketMessageHasBeenPostedID
}
type Z_WebSocketMessageHasBeenPostedArgs struct {
A string
B string
C *model.WebSocketRequest
}
type Z_WebSocketMessageHasBeenPostedReturns struct {
}
func (g *hooksRPCClient) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
_args := &Z_WebSocketMessageHasBeenPostedArgs{webConnID, userID, req}
_returns := &Z_WebSocketMessageHasBeenPostedReturns{}
if g.implemented[WebSocketMessageHasBeenPostedID] {
if err := g.client.Call("Plugin.WebSocketMessageHasBeenPosted", _args, _returns); err != nil {
g.log.Error("RPC call WebSocketMessageHasBeenPosted to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) WebSocketMessageHasBeenPosted(args *Z_WebSocketMessageHasBeenPostedArgs, returns *Z_WebSocketMessageHasBeenPostedReturns) error {
if hook, ok := s.impl.(interface {
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
}); ok {
hook.WebSocketMessageHasBeenPosted(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("Hook WebSocketMessageHasBeenPosted called but not implemented."))
}
return nil
}
func init() {
hookNameToId["RunDataRetention"] = RunDataRetentionID
}
type Z_RunDataRetentionArgs struct {
A int64
B int64
}
type Z_RunDataRetentionReturns struct {
A int64
B error
}
func (g *hooksRPCClient) RunDataRetention(nowTime, batchSize int64) (int64, error) {
_args := &Z_RunDataRetentionArgs{nowTime, batchSize}
_returns := &Z_RunDataRetentionReturns{}
if g.implemented[RunDataRetentionID] {
if err := g.client.Call("Plugin.RunDataRetention", _args, _returns); err != nil {
g.log.Error("RPC call RunDataRetention to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) RunDataRetention(args *Z_RunDataRetentionArgs, returns *Z_RunDataRetentionReturns) error {
if hook, ok := s.impl.(interface {
RunDataRetention(nowTime, batchSize int64) (int64, error)
}); ok {
returns.A, returns.B = hook.RunDataRetention(args.A, args.B)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook RunDataRetention called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnInstall"] = OnInstallID
}
type Z_OnInstallArgs struct {
A *Context
B model.OnInstallEvent
}
type Z_OnInstallReturns struct {
A error
}
func (g *hooksRPCClient) OnInstall(c *Context, event model.OnInstallEvent) error {
_args := &Z_OnInstallArgs{c, event}
_returns := &Z_OnInstallReturns{}
if g.implemented[OnInstallID] {
if err := g.client.Call("Plugin.OnInstall", _args, _returns); err != nil {
g.log.Error("RPC call OnInstall to plugin failed.", mlog.Err(err))
}
}
return _returns.A
}
func (s *hooksRPCServer) OnInstall(args *Z_OnInstallArgs, returns *Z_OnInstallReturns) error {
if hook, ok := s.impl.(interface {
OnInstall(c *Context, event model.OnInstallEvent) error
}); ok {
returns.A = hook.OnInstall(args.A, args.B)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("Hook OnInstall called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnSendDailyTelemetry"] = OnSendDailyTelemetryID
}
type Z_OnSendDailyTelemetryArgs struct {
}
type Z_OnSendDailyTelemetryReturns struct {
}
func (g *hooksRPCClient) OnSendDailyTelemetry() {
_args := &Z_OnSendDailyTelemetryArgs{}
_returns := &Z_OnSendDailyTelemetryReturns{}
if g.implemented[OnSendDailyTelemetryID] {
if err := g.client.Call("Plugin.OnSendDailyTelemetry", _args, _returns); err != nil {
g.log.Error("RPC call OnSendDailyTelemetry to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnSendDailyTelemetry(args *Z_OnSendDailyTelemetryArgs, returns *Z_OnSendDailyTelemetryReturns) error {
if hook, ok := s.impl.(interface {
OnSendDailyTelemetry()
}); ok {
hook.OnSendDailyTelemetry()
} else {
return encodableError(fmt.Errorf("Hook OnSendDailyTelemetry called but not implemented."))
}
return nil
}
func init() {
hookNameToId["OnCloudLimitsUpdated"] = OnCloudLimitsUpdatedID
}
type Z_OnCloudLimitsUpdatedArgs struct {
A *model.ProductLimits
}
type Z_OnCloudLimitsUpdatedReturns struct {
}
func (g *hooksRPCClient) OnCloudLimitsUpdated(limits *model.ProductLimits) {
_args := &Z_OnCloudLimitsUpdatedArgs{limits}
_returns := &Z_OnCloudLimitsUpdatedReturns{}
if g.implemented[OnCloudLimitsUpdatedID] {
if err := g.client.Call("Plugin.OnCloudLimitsUpdated", _args, _returns); err != nil {
g.log.Error("RPC call OnCloudLimitsUpdated to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnCloudLimitsUpdated(args *Z_OnCloudLimitsUpdatedArgs, returns *Z_OnCloudLimitsUpdatedReturns) error {
if hook, ok := s.impl.(interface {
OnCloudLimitsUpdated(limits *model.ProductLimits)
}); ok {
hook.OnCloudLimitsUpdated(args.A)
} else {
return encodableError(fmt.Errorf("Hook OnCloudLimitsUpdated called but not implemented."))
}
return nil
}
func init() {
hookNameToId["UserHasPermissionToCollection"] = UserHasPermissionToCollectionID
}
type Z_UserHasPermissionToCollectionArgs struct {
A *Context
B string
C string
D string
E *model.Permission
}
type Z_UserHasPermissionToCollectionReturns struct {
A bool
B error
}
func (g *hooksRPCClient) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) {
_args := &Z_UserHasPermissionToCollectionArgs{c, userID, collectionType, collectionId, permission}
_returns := &Z_UserHasPermissionToCollectionReturns{}
if g.implemented[UserHasPermissionToCollectionID] {
if err := g.client.Call("Plugin.UserHasPermissionToCollection", _args, _returns); err != nil {
g.log.Error("RPC call UserHasPermissionToCollection to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) UserHasPermissionToCollection(args *Z_UserHasPermissionToCollectionArgs, returns *Z_UserHasPermissionToCollectionReturns) error {
if hook, ok := s.impl.(interface {
UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error)
}); ok {
returns.A, returns.B = hook.UserHasPermissionToCollection(args.A, args.B, args.C, args.D, args.E)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook UserHasPermissionToCollection called but not implemented."))
}
return nil
}
func init() {
hookNameToId["GetAllCollectionIDsForUser"] = GetAllCollectionIDsForUserID
}
type Z_GetAllCollectionIDsForUserArgs struct {
A *Context
B string
C string
}
type Z_GetAllCollectionIDsForUserReturns struct {
A []string
B error
}
func (g *hooksRPCClient) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) {
_args := &Z_GetAllCollectionIDsForUserArgs{c, userID, collectionType}
_returns := &Z_GetAllCollectionIDsForUserReturns{}
if g.implemented[GetAllCollectionIDsForUserID] {
if err := g.client.Call("Plugin.GetAllCollectionIDsForUser", _args, _returns); err != nil {
g.log.Error("RPC call GetAllCollectionIDsForUser to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GetAllCollectionIDsForUser(args *Z_GetAllCollectionIDsForUserArgs, returns *Z_GetAllCollectionIDsForUserReturns) error {
if hook, ok := s.impl.(interface {
GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error)
}); ok {
returns.A, returns.B = hook.GetAllCollectionIDsForUser(args.A, args.B, args.C)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GetAllCollectionIDsForUser called but not implemented."))
}
return nil
}
func init() {
hookNameToId["GetAllUserIdsForCollection"] = GetAllUserIdsForCollectionID
}
type Z_GetAllUserIdsForCollectionArgs struct {
A *Context
B string
C string
}
type Z_GetAllUserIdsForCollectionReturns struct {
A []string
B error
}
func (g *hooksRPCClient) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) {
_args := &Z_GetAllUserIdsForCollectionArgs{c, collectionType, collectionID}
_returns := &Z_GetAllUserIdsForCollectionReturns{}
if g.implemented[GetAllUserIdsForCollectionID] {
if err := g.client.Call("Plugin.GetAllUserIdsForCollection", _args, _returns); err != nil {
g.log.Error("RPC call GetAllUserIdsForCollection to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GetAllUserIdsForCollection(args *Z_GetAllUserIdsForCollectionArgs, returns *Z_GetAllUserIdsForCollectionReturns) error {
if hook, ok := s.impl.(interface {
GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error)
}); ok {
returns.A, returns.B = hook.GetAllUserIdsForCollection(args.A, args.B, args.C)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GetAllUserIdsForCollection called but not implemented."))
}
return nil
}
func init() {
hookNameToId["GetTopicRedirect"] = GetTopicRedirectID
}
type Z_GetTopicRedirectArgs struct {
A *Context
B string
C string
}
type Z_GetTopicRedirectReturns struct {
A string
B error
}
func (g *hooksRPCClient) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) {
_args := &Z_GetTopicRedirectArgs{c, topicType, topicID}
_returns := &Z_GetTopicRedirectReturns{}
if g.implemented[GetTopicRedirectID] {
if err := g.client.Call("Plugin.GetTopicRedirect", _args, _returns); err != nil {
g.log.Error("RPC call GetTopicRedirect to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GetTopicRedirect(args *Z_GetTopicRedirectArgs, returns *Z_GetTopicRedirectReturns) error {
if hook, ok := s.impl.(interface {
GetTopicRedirect(c *Context, topicType, topicID string) (string, error)
}); ok {
returns.A, returns.B = hook.GetTopicRedirect(args.A, args.B, args.C)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GetTopicRedirect called but not implemented."))
}
return nil
}
func init() {
hookNameToId["GetCollectionMetadataByIds"] = GetCollectionMetadataByIdsID
}
type Z_GetCollectionMetadataByIdsArgs struct {
A *Context
B string
C []string
}
type Z_GetCollectionMetadataByIdsReturns struct {
A map[string]*model.CollectionMetadata
B error
}
func (g *hooksRPCClient) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
_args := &Z_GetCollectionMetadataByIdsArgs{c, collectionType, collectionIds}
_returns := &Z_GetCollectionMetadataByIdsReturns{}
if g.implemented[GetCollectionMetadataByIdsID] {
if err := g.client.Call("Plugin.GetCollectionMetadataByIds", _args, _returns); err != nil {
g.log.Error("RPC call GetCollectionMetadataByIds to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GetCollectionMetadataByIds(args *Z_GetCollectionMetadataByIdsArgs, returns *Z_GetCollectionMetadataByIdsReturns) error {
if hook, ok := s.impl.(interface {
GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error)
}); ok {
returns.A, returns.B = hook.GetCollectionMetadataByIds(args.A, args.B, args.C)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GetCollectionMetadataByIds called but not implemented."))
}
return nil
}
func init() {
hookNameToId["GetTopicMetadataByIds"] = GetTopicMetadataByIdsID
}
type Z_GetTopicMetadataByIdsArgs struct {
A *Context
B string
C []string
}
type Z_GetTopicMetadataByIdsReturns struct {
A map[string]*model.TopicMetadata
B error
}
func (g *hooksRPCClient) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
_args := &Z_GetTopicMetadataByIdsArgs{c, topicType, topicIds}
_returns := &Z_GetTopicMetadataByIdsReturns{}
if g.implemented[GetTopicMetadataByIdsID] {
if err := g.client.Call("Plugin.GetTopicMetadataByIds", _args, _returns); err != nil {
g.log.Error("RPC call GetTopicMetadataByIds to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GetTopicMetadataByIds(args *Z_GetTopicMetadataByIdsArgs, returns *Z_GetTopicMetadataByIdsReturns) error {
if hook, ok := s.impl.(interface {
GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error)
}); ok {
returns.A, returns.B = hook.GetTopicMetadataByIds(args.A, args.B, args.C)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GetTopicMetadataByIds called but not implemented."))
}
return nil
}
type Z_RegisterCommandArgs struct {
A *model.Command
}
type Z_RegisterCommandReturns struct {
A error
}
func (g *apiRPCClient) RegisterCommand(command *model.Command) error {
_args := &Z_RegisterCommandArgs{command}
_returns := &Z_RegisterCommandReturns{}
if err := g.client.Call("Plugin.RegisterCommand", _args, _returns); err != nil {
log.Printf("RPC call to RegisterCommand API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RegisterCommand(args *Z_RegisterCommandArgs, returns *Z_RegisterCommandReturns) error {
if hook, ok := s.impl.(interface {
RegisterCommand(command *model.Command) error
}); ok {
returns.A = hook.RegisterCommand(args.A)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("API RegisterCommand called but not implemented."))
}
return nil
}
type Z_UnregisterCommandArgs struct {
A string
B string
}
type Z_UnregisterCommandReturns struct {
A error
}
func (g *apiRPCClient) UnregisterCommand(teamID, trigger string) error {
_args := &Z_UnregisterCommandArgs{teamID, trigger}
_returns := &Z_UnregisterCommandReturns{}
if err := g.client.Call("Plugin.UnregisterCommand", _args, _returns); err != nil {
log.Printf("RPC call to UnregisterCommand API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UnregisterCommand(args *Z_UnregisterCommandArgs, returns *Z_UnregisterCommandReturns) error {
if hook, ok := s.impl.(interface {
UnregisterCommand(teamID, trigger string) error
}); ok {
returns.A = hook.UnregisterCommand(args.A, args.B)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("API UnregisterCommand called but not implemented."))
}
return nil
}
type Z_ExecuteSlashCommandArgs struct {
A *model.CommandArgs
}
type Z_ExecuteSlashCommandReturns struct {
A *model.CommandResponse
B error
}
func (g *apiRPCClient) ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) {
_args := &Z_ExecuteSlashCommandArgs{commandArgs}
_returns := &Z_ExecuteSlashCommandReturns{}
if err := g.client.Call("Plugin.ExecuteSlashCommand", _args, _returns); err != nil {
log.Printf("RPC call to ExecuteSlashCommand API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ExecuteSlashCommand(args *Z_ExecuteSlashCommandArgs, returns *Z_ExecuteSlashCommandReturns) error {
if hook, ok := s.impl.(interface {
ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error)
}); ok {
returns.A, returns.B = hook.ExecuteSlashCommand(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API ExecuteSlashCommand called but not implemented."))
}
return nil
}
type Z_GetConfigArgs struct {
}
type Z_GetConfigReturns struct {
A *model.Config
}
func (g *apiRPCClient) GetConfig() *model.Config {
_args := &Z_GetConfigArgs{}
_returns := &Z_GetConfigReturns{}
if err := g.client.Call("Plugin.GetConfig", _args, _returns); err != nil {
log.Printf("RPC call to GetConfig API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetConfig(args *Z_GetConfigArgs, returns *Z_GetConfigReturns) error {
if hook, ok := s.impl.(interface {
GetConfig() *model.Config
}); ok {
returns.A = hook.GetConfig()
} else {
return encodableError(fmt.Errorf("API GetConfig called but not implemented."))
}
return nil
}
type Z_GetUnsanitizedConfigArgs struct {
}
type Z_GetUnsanitizedConfigReturns struct {
A *model.Config
}
func (g *apiRPCClient) GetUnsanitizedConfig() *model.Config {
_args := &Z_GetUnsanitizedConfigArgs{}
_returns := &Z_GetUnsanitizedConfigReturns{}
if err := g.client.Call("Plugin.GetUnsanitizedConfig", _args, _returns); err != nil {
log.Printf("RPC call to GetUnsanitizedConfig API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetUnsanitizedConfig(args *Z_GetUnsanitizedConfigArgs, returns *Z_GetUnsanitizedConfigReturns) error {
if hook, ok := s.impl.(interface {
GetUnsanitizedConfig() *model.Config
}); ok {
returns.A = hook.GetUnsanitizedConfig()
} else {
return encodableError(fmt.Errorf("API GetUnsanitizedConfig called but not implemented."))
}
return nil
}
type Z_SaveConfigArgs struct {
A *model.Config
}
type Z_SaveConfigReturns struct {
A *model.AppError
}
func (g *apiRPCClient) SaveConfig(config *model.Config) *model.AppError {
_args := &Z_SaveConfigArgs{config}
_returns := &Z_SaveConfigReturns{}
if err := g.client.Call("Plugin.SaveConfig", _args, _returns); err != nil {
log.Printf("RPC call to SaveConfig API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SaveConfig(args *Z_SaveConfigArgs, returns *Z_SaveConfigReturns) error {
if hook, ok := s.impl.(interface {
SaveConfig(config *model.Config) *model.AppError
}); ok {
returns.A = hook.SaveConfig(args.A)
} else {
return encodableError(fmt.Errorf("API SaveConfig called but not implemented."))
}
return nil
}
type Z_GetPluginConfigArgs struct {
}
type Z_GetPluginConfigReturns struct {
A map[string]any
}
func (g *apiRPCClient) GetPluginConfig() map[string]any {
_args := &Z_GetPluginConfigArgs{}
_returns := &Z_GetPluginConfigReturns{}
if err := g.client.Call("Plugin.GetPluginConfig", _args, _returns); err != nil {
log.Printf("RPC call to GetPluginConfig API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetPluginConfig(args *Z_GetPluginConfigArgs, returns *Z_GetPluginConfigReturns) error {
if hook, ok := s.impl.(interface {
GetPluginConfig() map[string]any
}); ok {
returns.A = hook.GetPluginConfig()
} else {
return encodableError(fmt.Errorf("API GetPluginConfig called but not implemented."))
}
return nil
}
type Z_SavePluginConfigArgs struct {
A map[string]any
}
type Z_SavePluginConfigReturns struct {
A *model.AppError
}
func (g *apiRPCClient) SavePluginConfig(config map[string]any) *model.AppError {
_args := &Z_SavePluginConfigArgs{config}
_returns := &Z_SavePluginConfigReturns{}
if err := g.client.Call("Plugin.SavePluginConfig", _args, _returns); err != nil {
log.Printf("RPC call to SavePluginConfig API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SavePluginConfig(args *Z_SavePluginConfigArgs, returns *Z_SavePluginConfigReturns) error {
if hook, ok := s.impl.(interface {
SavePluginConfig(config map[string]any) *model.AppError
}); ok {
returns.A = hook.SavePluginConfig(args.A)
} else {
return encodableError(fmt.Errorf("API SavePluginConfig called but not implemented."))
}
return nil
}
type Z_GetBundlePathArgs struct {
}
type Z_GetBundlePathReturns struct {
A string
B error
}
func (g *apiRPCClient) GetBundlePath() (string, error) {
_args := &Z_GetBundlePathArgs{}
_returns := &Z_GetBundlePathReturns{}
if err := g.client.Call("Plugin.GetBundlePath", _args, _returns); err != nil {
log.Printf("RPC call to GetBundlePath API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetBundlePath(args *Z_GetBundlePathArgs, returns *Z_GetBundlePathReturns) error {
if hook, ok := s.impl.(interface {
GetBundlePath() (string, error)
}); ok {
returns.A, returns.B = hook.GetBundlePath()
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API GetBundlePath called but not implemented."))
}
return nil
}
type Z_GetLicenseArgs struct {
}
type Z_GetLicenseReturns struct {
A *model.License
}
func (g *apiRPCClient) GetLicense() *model.License {
_args := &Z_GetLicenseArgs{}
_returns := &Z_GetLicenseReturns{}
if err := g.client.Call("Plugin.GetLicense", _args, _returns); err != nil {
log.Printf("RPC call to GetLicense API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetLicense(args *Z_GetLicenseArgs, returns *Z_GetLicenseReturns) error {
if hook, ok := s.impl.(interface {
GetLicense() *model.License
}); ok {
returns.A = hook.GetLicense()
} else {
return encodableError(fmt.Errorf("API GetLicense called but not implemented."))
}
return nil
}
type Z_IsEnterpriseReadyArgs struct {
}
type Z_IsEnterpriseReadyReturns struct {
A bool
}
func (g *apiRPCClient) IsEnterpriseReady() bool {
_args := &Z_IsEnterpriseReadyArgs{}
_returns := &Z_IsEnterpriseReadyReturns{}
if err := g.client.Call("Plugin.IsEnterpriseReady", _args, _returns); err != nil {
log.Printf("RPC call to IsEnterpriseReady API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) IsEnterpriseReady(args *Z_IsEnterpriseReadyArgs, returns *Z_IsEnterpriseReadyReturns) error {
if hook, ok := s.impl.(interface {
IsEnterpriseReady() bool
}); ok {
returns.A = hook.IsEnterpriseReady()
} else {
return encodableError(fmt.Errorf("API IsEnterpriseReady called but not implemented."))
}
return nil
}
type Z_GetServerVersionArgs struct {
}
type Z_GetServerVersionReturns struct {
A string
}
func (g *apiRPCClient) GetServerVersion() string {
_args := &Z_GetServerVersionArgs{}
_returns := &Z_GetServerVersionReturns{}
if err := g.client.Call("Plugin.GetServerVersion", _args, _returns); err != nil {
log.Printf("RPC call to GetServerVersion API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetServerVersion(args *Z_GetServerVersionArgs, returns *Z_GetServerVersionReturns) error {
if hook, ok := s.impl.(interface {
GetServerVersion() string
}); ok {
returns.A = hook.GetServerVersion()
} else {
return encodableError(fmt.Errorf("API GetServerVersion called but not implemented."))
}
return nil
}
type Z_GetSystemInstallDateArgs struct {
}
type Z_GetSystemInstallDateReturns struct {
A int64
B *model.AppError
}
func (g *apiRPCClient) GetSystemInstallDate() (int64, *model.AppError) {
_args := &Z_GetSystemInstallDateArgs{}
_returns := &Z_GetSystemInstallDateReturns{}
if err := g.client.Call("Plugin.GetSystemInstallDate", _args, _returns); err != nil {
log.Printf("RPC call to GetSystemInstallDate API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetSystemInstallDate(args *Z_GetSystemInstallDateArgs, returns *Z_GetSystemInstallDateReturns) error {
if hook, ok := s.impl.(interface {
GetSystemInstallDate() (int64, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetSystemInstallDate()
} else {
return encodableError(fmt.Errorf("API GetSystemInstallDate called but not implemented."))
}
return nil
}
type Z_GetDiagnosticIdArgs struct {
}
type Z_GetDiagnosticIdReturns struct {
A string
}
func (g *apiRPCClient) GetDiagnosticId() string {
_args := &Z_GetDiagnosticIdArgs{}
_returns := &Z_GetDiagnosticIdReturns{}
if err := g.client.Call("Plugin.GetDiagnosticId", _args, _returns); err != nil {
log.Printf("RPC call to GetDiagnosticId API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetDiagnosticId(args *Z_GetDiagnosticIdArgs, returns *Z_GetDiagnosticIdReturns) error {
if hook, ok := s.impl.(interface {
GetDiagnosticId() string
}); ok {
returns.A = hook.GetDiagnosticId()
} else {
return encodableError(fmt.Errorf("API GetDiagnosticId called but not implemented."))
}
return nil
}
type Z_GetTelemetryIdArgs struct {
}
type Z_GetTelemetryIdReturns struct {
A string
}
func (g *apiRPCClient) GetTelemetryId() string {
_args := &Z_GetTelemetryIdArgs{}
_returns := &Z_GetTelemetryIdReturns{}
if err := g.client.Call("Plugin.GetTelemetryId", _args, _returns); err != nil {
log.Printf("RPC call to GetTelemetryId API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) GetTelemetryId(args *Z_GetTelemetryIdArgs, returns *Z_GetTelemetryIdReturns) error {
if hook, ok := s.impl.(interface {
GetTelemetryId() string
}); ok {
returns.A = hook.GetTelemetryId()
} else {
return encodableError(fmt.Errorf("API GetTelemetryId called but not implemented."))
}
return nil
}
type Z_CreateUserArgs struct {
A *model.User
}
type Z_CreateUserReturns struct {
A *model.User
B *model.AppError
}
func (g *apiRPCClient) CreateUser(user *model.User) (*model.User, *model.AppError) {
_args := &Z_CreateUserArgs{user}
_returns := &Z_CreateUserReturns{}
if err := g.client.Call("Plugin.CreateUser", _args, _returns); err != nil {
log.Printf("RPC call to CreateUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateUser(args *Z_CreateUserArgs, returns *Z_CreateUserReturns) error {
if hook, ok := s.impl.(interface {
CreateUser(user *model.User) (*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateUser(args.A)
} else {
return encodableError(fmt.Errorf("API CreateUser called but not implemented."))
}
return nil
}
type Z_DeleteUserArgs struct {
A string
}
type Z_DeleteUserReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteUser(userID string) *model.AppError {
_args := &Z_DeleteUserArgs{userID}
_returns := &Z_DeleteUserReturns{}
if err := g.client.Call("Plugin.DeleteUser", _args, _returns); err != nil {
log.Printf("RPC call to DeleteUser API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteUser(args *Z_DeleteUserArgs, returns *Z_DeleteUserReturns) error {
if hook, ok := s.impl.(interface {
DeleteUser(userID string) *model.AppError
}); ok {
returns.A = hook.DeleteUser(args.A)
} else {
return encodableError(fmt.Errorf("API DeleteUser called but not implemented."))
}
return nil
}
type Z_GetUsersArgs struct {
A *model.UserGetOptions
}
type Z_GetUsersReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
_args := &Z_GetUsersArgs{options}
_returns := &Z_GetUsersReturns{}
if err := g.client.Call("Plugin.GetUsers", _args, _returns); err != nil {
log.Printf("RPC call to GetUsers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUsers(args *Z_GetUsersArgs, returns *Z_GetUsersReturns) error {
if hook, ok := s.impl.(interface {
GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUsers(args.A)
} else {
return encodableError(fmt.Errorf("API GetUsers called but not implemented."))
}
return nil
}
type Z_GetUserArgs struct {
A string
}
type Z_GetUserReturns struct {
A *model.User
B *model.AppError
}
func (g *apiRPCClient) GetUser(userID string) (*model.User, *model.AppError) {
_args := &Z_GetUserArgs{userID}
_returns := &Z_GetUserReturns{}
if err := g.client.Call("Plugin.GetUser", _args, _returns); err != nil {
log.Printf("RPC call to GetUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUser(args *Z_GetUserArgs, returns *Z_GetUserReturns) error {
if hook, ok := s.impl.(interface {
GetUser(userID string) (*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUser(args.A)
} else {
return encodableError(fmt.Errorf("API GetUser called but not implemented."))
}
return nil
}
type Z_GetUserByEmailArgs struct {
A string
}
type Z_GetUserByEmailReturns struct {
A *model.User
B *model.AppError
}
func (g *apiRPCClient) GetUserByEmail(email string) (*model.User, *model.AppError) {
_args := &Z_GetUserByEmailArgs{email}
_returns := &Z_GetUserByEmailReturns{}
if err := g.client.Call("Plugin.GetUserByEmail", _args, _returns); err != nil {
log.Printf("RPC call to GetUserByEmail API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUserByEmail(args *Z_GetUserByEmailArgs, returns *Z_GetUserByEmailReturns) error {
if hook, ok := s.impl.(interface {
GetUserByEmail(email string) (*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUserByEmail(args.A)
} else {
return encodableError(fmt.Errorf("API GetUserByEmail called but not implemented."))
}
return nil
}
type Z_GetUserByUsernameArgs struct {
A string
}
type Z_GetUserByUsernameReturns struct {
A *model.User
B *model.AppError
}
func (g *apiRPCClient) GetUserByUsername(name string) (*model.User, *model.AppError) {
_args := &Z_GetUserByUsernameArgs{name}
_returns := &Z_GetUserByUsernameReturns{}
if err := g.client.Call("Plugin.GetUserByUsername", _args, _returns); err != nil {
log.Printf("RPC call to GetUserByUsername API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUserByUsername(args *Z_GetUserByUsernameArgs, returns *Z_GetUserByUsernameReturns) error {
if hook, ok := s.impl.(interface {
GetUserByUsername(name string) (*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUserByUsername(args.A)
} else {
return encodableError(fmt.Errorf("API GetUserByUsername called but not implemented."))
}
return nil
}
type Z_GetUsersByUsernamesArgs struct {
A []string
}
type Z_GetUsersByUsernamesReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) {
_args := &Z_GetUsersByUsernamesArgs{usernames}
_returns := &Z_GetUsersByUsernamesReturns{}
if err := g.client.Call("Plugin.GetUsersByUsernames", _args, _returns); err != nil {
log.Printf("RPC call to GetUsersByUsernames API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUsersByUsernames(args *Z_GetUsersByUsernamesArgs, returns *Z_GetUsersByUsernamesReturns) error {
if hook, ok := s.impl.(interface {
GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUsersByUsernames(args.A)
} else {
return encodableError(fmt.Errorf("API GetUsersByUsernames called but not implemented."))
}
return nil
}
type Z_GetUsersInTeamArgs struct {
A string
B int
C int
}
type Z_GetUsersInTeamReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) {
_args := &Z_GetUsersInTeamArgs{teamID, page, perPage}
_returns := &Z_GetUsersInTeamReturns{}
if err := g.client.Call("Plugin.GetUsersInTeam", _args, _returns); err != nil {
log.Printf("RPC call to GetUsersInTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUsersInTeam(args *Z_GetUsersInTeamArgs, returns *Z_GetUsersInTeamReturns) error {
if hook, ok := s.impl.(interface {
GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUsersInTeam(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetUsersInTeam called but not implemented."))
}
return nil
}
type Z_GetPreferencesForUserArgs struct {
A string
}
type Z_GetPreferencesForUserReturns struct {
A []model.Preference
B *model.AppError
}
func (g *apiRPCClient) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) {
_args := &Z_GetPreferencesForUserArgs{userID}
_returns := &Z_GetPreferencesForUserReturns{}
if err := g.client.Call("Plugin.GetPreferencesForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetPreferencesForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPreferencesForUser(args *Z_GetPreferencesForUserArgs, returns *Z_GetPreferencesForUserReturns) error {
if hook, ok := s.impl.(interface {
GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPreferencesForUser(args.A)
} else {
return encodableError(fmt.Errorf("API GetPreferencesForUser called but not implemented."))
}
return nil
}
type Z_UpdatePreferencesForUserArgs struct {
A string
B []model.Preference
}
type Z_UpdatePreferencesForUserReturns struct {
A *model.AppError
}
func (g *apiRPCClient) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
_args := &Z_UpdatePreferencesForUserArgs{userID, preferences}
_returns := &Z_UpdatePreferencesForUserReturns{}
if err := g.client.Call("Plugin.UpdatePreferencesForUser", _args, _returns); err != nil {
log.Printf("RPC call to UpdatePreferencesForUser API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UpdatePreferencesForUser(args *Z_UpdatePreferencesForUserArgs, returns *Z_UpdatePreferencesForUserReturns) error {
if hook, ok := s.impl.(interface {
UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError
}); ok {
returns.A = hook.UpdatePreferencesForUser(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdatePreferencesForUser called but not implemented."))
}
return nil
}
type Z_DeletePreferencesForUserArgs struct {
A string
B []model.Preference
}
type Z_DeletePreferencesForUserReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
_args := &Z_DeletePreferencesForUserArgs{userID, preferences}
_returns := &Z_DeletePreferencesForUserReturns{}
if err := g.client.Call("Plugin.DeletePreferencesForUser", _args, _returns); err != nil {
log.Printf("RPC call to DeletePreferencesForUser API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeletePreferencesForUser(args *Z_DeletePreferencesForUserArgs, returns *Z_DeletePreferencesForUserReturns) error {
if hook, ok := s.impl.(interface {
DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError
}); ok {
returns.A = hook.DeletePreferencesForUser(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API DeletePreferencesForUser called but not implemented."))
}
return nil
}
type Z_GetSessionArgs struct {
A string
}
type Z_GetSessionReturns struct {
A *model.Session
B *model.AppError
}
func (g *apiRPCClient) GetSession(sessionID string) (*model.Session, *model.AppError) {
_args := &Z_GetSessionArgs{sessionID}
_returns := &Z_GetSessionReturns{}
if err := g.client.Call("Plugin.GetSession", _args, _returns); err != nil {
log.Printf("RPC call to GetSession API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetSession(args *Z_GetSessionArgs, returns *Z_GetSessionReturns) error {
if hook, ok := s.impl.(interface {
GetSession(sessionID string) (*model.Session, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetSession(args.A)
} else {
return encodableError(fmt.Errorf("API GetSession called but not implemented."))
}
return nil
}
type Z_CreateSessionArgs struct {
A *model.Session
}
type Z_CreateSessionReturns struct {
A *model.Session
B *model.AppError
}
func (g *apiRPCClient) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
_args := &Z_CreateSessionArgs{session}
_returns := &Z_CreateSessionReturns{}
if err := g.client.Call("Plugin.CreateSession", _args, _returns); err != nil {
log.Printf("RPC call to CreateSession API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateSession(args *Z_CreateSessionArgs, returns *Z_CreateSessionReturns) error {
if hook, ok := s.impl.(interface {
CreateSession(session *model.Session) (*model.Session, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateSession(args.A)
} else {
return encodableError(fmt.Errorf("API CreateSession called but not implemented."))
}
return nil
}
type Z_ExtendSessionExpiryArgs struct {
A string
B int64
}
type Z_ExtendSessionExpiryReturns struct {
A *model.AppError
}
func (g *apiRPCClient) ExtendSessionExpiry(sessionID string, newExpiry int64) *model.AppError {
_args := &Z_ExtendSessionExpiryArgs{sessionID, newExpiry}
_returns := &Z_ExtendSessionExpiryReturns{}
if err := g.client.Call("Plugin.ExtendSessionExpiry", _args, _returns); err != nil {
log.Printf("RPC call to ExtendSessionExpiry API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) ExtendSessionExpiry(args *Z_ExtendSessionExpiryArgs, returns *Z_ExtendSessionExpiryReturns) error {
if hook, ok := s.impl.(interface {
ExtendSessionExpiry(sessionID string, newExpiry int64) *model.AppError
}); ok {
returns.A = hook.ExtendSessionExpiry(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API ExtendSessionExpiry called but not implemented."))
}
return nil
}
type Z_RevokeSessionArgs struct {
A string
}
type Z_RevokeSessionReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RevokeSession(sessionID string) *model.AppError {
_args := &Z_RevokeSessionArgs{sessionID}
_returns := &Z_RevokeSessionReturns{}
if err := g.client.Call("Plugin.RevokeSession", _args, _returns); err != nil {
log.Printf("RPC call to RevokeSession API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RevokeSession(args *Z_RevokeSessionArgs, returns *Z_RevokeSessionReturns) error {
if hook, ok := s.impl.(interface {
RevokeSession(sessionID string) *model.AppError
}); ok {
returns.A = hook.RevokeSession(args.A)
} else {
return encodableError(fmt.Errorf("API RevokeSession called but not implemented."))
}
return nil
}
type Z_CreateUserAccessTokenArgs struct {
A *model.UserAccessToken
}
type Z_CreateUserAccessTokenReturns struct {
A *model.UserAccessToken
B *model.AppError
}
func (g *apiRPCClient) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
_args := &Z_CreateUserAccessTokenArgs{token}
_returns := &Z_CreateUserAccessTokenReturns{}
if err := g.client.Call("Plugin.CreateUserAccessToken", _args, _returns); err != nil {
log.Printf("RPC call to CreateUserAccessToken API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateUserAccessToken(args *Z_CreateUserAccessTokenArgs, returns *Z_CreateUserAccessTokenReturns) error {
if hook, ok := s.impl.(interface {
CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateUserAccessToken(args.A)
} else {
return encodableError(fmt.Errorf("API CreateUserAccessToken called but not implemented."))
}
return nil
}
type Z_RevokeUserAccessTokenArgs struct {
A string
}
type Z_RevokeUserAccessTokenReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RevokeUserAccessToken(tokenID string) *model.AppError {
_args := &Z_RevokeUserAccessTokenArgs{tokenID}
_returns := &Z_RevokeUserAccessTokenReturns{}
if err := g.client.Call("Plugin.RevokeUserAccessToken", _args, _returns); err != nil {
log.Printf("RPC call to RevokeUserAccessToken API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RevokeUserAccessToken(args *Z_RevokeUserAccessTokenArgs, returns *Z_RevokeUserAccessTokenReturns) error {
if hook, ok := s.impl.(interface {
RevokeUserAccessToken(tokenID string) *model.AppError
}); ok {
returns.A = hook.RevokeUserAccessToken(args.A)
} else {
return encodableError(fmt.Errorf("API RevokeUserAccessToken called but not implemented."))
}
return nil
}
type Z_GetTeamIconArgs struct {
A string
}
type Z_GetTeamIconReturns struct {
A []byte
B *model.AppError
}
func (g *apiRPCClient) GetTeamIcon(teamID string) ([]byte, *model.AppError) {
_args := &Z_GetTeamIconArgs{teamID}
_returns := &Z_GetTeamIconReturns{}
if err := g.client.Call("Plugin.GetTeamIcon", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamIcon API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamIcon(args *Z_GetTeamIconArgs, returns *Z_GetTeamIconReturns) error {
if hook, ok := s.impl.(interface {
GetTeamIcon(teamID string) ([]byte, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamIcon(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeamIcon called but not implemented."))
}
return nil
}
type Z_SetTeamIconArgs struct {
A string
B []byte
}
type Z_SetTeamIconReturns struct {
A *model.AppError
}
func (g *apiRPCClient) SetTeamIcon(teamID string, data []byte) *model.AppError {
_args := &Z_SetTeamIconArgs{teamID, data}
_returns := &Z_SetTeamIconReturns{}
if err := g.client.Call("Plugin.SetTeamIcon", _args, _returns); err != nil {
log.Printf("RPC call to SetTeamIcon API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SetTeamIcon(args *Z_SetTeamIconArgs, returns *Z_SetTeamIconReturns) error {
if hook, ok := s.impl.(interface {
SetTeamIcon(teamID string, data []byte) *model.AppError
}); ok {
returns.A = hook.SetTeamIcon(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SetTeamIcon called but not implemented."))
}
return nil
}
type Z_RemoveTeamIconArgs struct {
A string
}
type Z_RemoveTeamIconReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RemoveTeamIcon(teamID string) *model.AppError {
_args := &Z_RemoveTeamIconArgs{teamID}
_returns := &Z_RemoveTeamIconReturns{}
if err := g.client.Call("Plugin.RemoveTeamIcon", _args, _returns); err != nil {
log.Printf("RPC call to RemoveTeamIcon API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RemoveTeamIcon(args *Z_RemoveTeamIconArgs, returns *Z_RemoveTeamIconReturns) error {
if hook, ok := s.impl.(interface {
RemoveTeamIcon(teamID string) *model.AppError
}); ok {
returns.A = hook.RemoveTeamIcon(args.A)
} else {
return encodableError(fmt.Errorf("API RemoveTeamIcon called but not implemented."))
}
return nil
}
type Z_UpdateUserArgs struct {
A *model.User
}
type Z_UpdateUserReturns struct {
A *model.User
B *model.AppError
}
func (g *apiRPCClient) UpdateUser(user *model.User) (*model.User, *model.AppError) {
_args := &Z_UpdateUserArgs{user}
_returns := &Z_UpdateUserReturns{}
if err := g.client.Call("Plugin.UpdateUser", _args, _returns); err != nil {
log.Printf("RPC call to UpdateUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateUser(args *Z_UpdateUserArgs, returns *Z_UpdateUserReturns) error {
if hook, ok := s.impl.(interface {
UpdateUser(user *model.User) (*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateUser(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateUser called but not implemented."))
}
return nil
}
type Z_GetUserStatusArgs struct {
A string
}
type Z_GetUserStatusReturns struct {
A *model.Status
B *model.AppError
}
func (g *apiRPCClient) GetUserStatus(userID string) (*model.Status, *model.AppError) {
_args := &Z_GetUserStatusArgs{userID}
_returns := &Z_GetUserStatusReturns{}
if err := g.client.Call("Plugin.GetUserStatus", _args, _returns); err != nil {
log.Printf("RPC call to GetUserStatus API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUserStatus(args *Z_GetUserStatusArgs, returns *Z_GetUserStatusReturns) error {
if hook, ok := s.impl.(interface {
GetUserStatus(userID string) (*model.Status, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUserStatus(args.A)
} else {
return encodableError(fmt.Errorf("API GetUserStatus called but not implemented."))
}
return nil
}
type Z_GetUserStatusesByIdsArgs struct {
A []string
}
type Z_GetUserStatusesByIdsReturns struct {
A []*model.Status
B *model.AppError
}
func (g *apiRPCClient) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) {
_args := &Z_GetUserStatusesByIdsArgs{userIds}
_returns := &Z_GetUserStatusesByIdsReturns{}
if err := g.client.Call("Plugin.GetUserStatusesByIds", _args, _returns); err != nil {
log.Printf("RPC call to GetUserStatusesByIds API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUserStatusesByIds(args *Z_GetUserStatusesByIdsArgs, returns *Z_GetUserStatusesByIdsReturns) error {
if hook, ok := s.impl.(interface {
GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUserStatusesByIds(args.A)
} else {
return encodableError(fmt.Errorf("API GetUserStatusesByIds called but not implemented."))
}
return nil
}
type Z_UpdateUserStatusArgs struct {
A string
B string
}
type Z_UpdateUserStatusReturns struct {
A *model.Status
B *model.AppError
}
func (g *apiRPCClient) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) {
_args := &Z_UpdateUserStatusArgs{userID, status}
_returns := &Z_UpdateUserStatusReturns{}
if err := g.client.Call("Plugin.UpdateUserStatus", _args, _returns); err != nil {
log.Printf("RPC call to UpdateUserStatus API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateUserStatus(args *Z_UpdateUserStatusArgs, returns *Z_UpdateUserStatusReturns) error {
if hook, ok := s.impl.(interface {
UpdateUserStatus(userID, status string) (*model.Status, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateUserStatus(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateUserStatus called but not implemented."))
}
return nil
}
type Z_SetUserStatusTimedDNDArgs struct {
A string
B int64
}
type Z_SetUserStatusTimedDNDReturns struct {
A *model.Status
B *model.AppError
}
func (g *apiRPCClient) SetUserStatusTimedDND(userId string, endtime int64) (*model.Status, *model.AppError) {
_args := &Z_SetUserStatusTimedDNDArgs{userId, endtime}
_returns := &Z_SetUserStatusTimedDNDReturns{}
if err := g.client.Call("Plugin.SetUserStatusTimedDND", _args, _returns); err != nil {
log.Printf("RPC call to SetUserStatusTimedDND API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SetUserStatusTimedDND(args *Z_SetUserStatusTimedDNDArgs, returns *Z_SetUserStatusTimedDNDReturns) error {
if hook, ok := s.impl.(interface {
SetUserStatusTimedDND(userId string, endtime int64) (*model.Status, *model.AppError)
}); ok {
returns.A, returns.B = hook.SetUserStatusTimedDND(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SetUserStatusTimedDND called but not implemented."))
}
return nil
}
type Z_UpdateUserActiveArgs struct {
A string
B bool
}
type Z_UpdateUserActiveReturns struct {
A *model.AppError
}
func (g *apiRPCClient) UpdateUserActive(userID string, active bool) *model.AppError {
_args := &Z_UpdateUserActiveArgs{userID, active}
_returns := &Z_UpdateUserActiveReturns{}
if err := g.client.Call("Plugin.UpdateUserActive", _args, _returns); err != nil {
log.Printf("RPC call to UpdateUserActive API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UpdateUserActive(args *Z_UpdateUserActiveArgs, returns *Z_UpdateUserActiveReturns) error {
if hook, ok := s.impl.(interface {
UpdateUserActive(userID string, active bool) *model.AppError
}); ok {
returns.A = hook.UpdateUserActive(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateUserActive called but not implemented."))
}
return nil
}
type Z_UpdateUserCustomStatusArgs struct {
A string
B *model.CustomStatus
}
type Z_UpdateUserCustomStatusReturns struct {
A *model.AppError
}
func (g *apiRPCClient) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError {
_args := &Z_UpdateUserCustomStatusArgs{userID, customStatus}
_returns := &Z_UpdateUserCustomStatusReturns{}
if err := g.client.Call("Plugin.UpdateUserCustomStatus", _args, _returns); err != nil {
log.Printf("RPC call to UpdateUserCustomStatus API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UpdateUserCustomStatus(args *Z_UpdateUserCustomStatusArgs, returns *Z_UpdateUserCustomStatusReturns) error {
if hook, ok := s.impl.(interface {
UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError
}); ok {
returns.A = hook.UpdateUserCustomStatus(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateUserCustomStatus called but not implemented."))
}
return nil
}
type Z_RemoveUserCustomStatusArgs struct {
A string
}
type Z_RemoveUserCustomStatusReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RemoveUserCustomStatus(userID string) *model.AppError {
_args := &Z_RemoveUserCustomStatusArgs{userID}
_returns := &Z_RemoveUserCustomStatusReturns{}
if err := g.client.Call("Plugin.RemoveUserCustomStatus", _args, _returns); err != nil {
log.Printf("RPC call to RemoveUserCustomStatus API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RemoveUserCustomStatus(args *Z_RemoveUserCustomStatusArgs, returns *Z_RemoveUserCustomStatusReturns) error {
if hook, ok := s.impl.(interface {
RemoveUserCustomStatus(userID string) *model.AppError
}); ok {
returns.A = hook.RemoveUserCustomStatus(args.A)
} else {
return encodableError(fmt.Errorf("API RemoveUserCustomStatus called but not implemented."))
}
return nil
}
type Z_GetUsersInChannelArgs struct {
A string
B string
C int
D int
}
type Z_GetUsersInChannelReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
_args := &Z_GetUsersInChannelArgs{channelID, sortBy, page, perPage}
_returns := &Z_GetUsersInChannelReturns{}
if err := g.client.Call("Plugin.GetUsersInChannel", _args, _returns); err != nil {
log.Printf("RPC call to GetUsersInChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUsersInChannel(args *Z_GetUsersInChannelArgs, returns *Z_GetUsersInChannelReturns) error {
if hook, ok := s.impl.(interface {
GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetUsersInChannel(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API GetUsersInChannel called but not implemented."))
}
return nil
}
type Z_GetLDAPUserAttributesArgs struct {
A string
B []string
}
type Z_GetLDAPUserAttributesReturns struct {
A map[string]string
B *model.AppError
}
func (g *apiRPCClient) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) {
_args := &Z_GetLDAPUserAttributesArgs{userID, attributes}
_returns := &Z_GetLDAPUserAttributesReturns{}
if err := g.client.Call("Plugin.GetLDAPUserAttributes", _args, _returns); err != nil {
log.Printf("RPC call to GetLDAPUserAttributes API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetLDAPUserAttributes(args *Z_GetLDAPUserAttributesArgs, returns *Z_GetLDAPUserAttributesReturns) error {
if hook, ok := s.impl.(interface {
GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetLDAPUserAttributes(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetLDAPUserAttributes called but not implemented."))
}
return nil
}
type Z_CreateTeamArgs struct {
A *model.Team
}
type Z_CreateTeamReturns struct {
A *model.Team
B *model.AppError
}
func (g *apiRPCClient) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
_args := &Z_CreateTeamArgs{team}
_returns := &Z_CreateTeamReturns{}
if err := g.client.Call("Plugin.CreateTeam", _args, _returns); err != nil {
log.Printf("RPC call to CreateTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateTeam(args *Z_CreateTeamArgs, returns *Z_CreateTeamReturns) error {
if hook, ok := s.impl.(interface {
CreateTeam(team *model.Team) (*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateTeam(args.A)
} else {
return encodableError(fmt.Errorf("API CreateTeam called but not implemented."))
}
return nil
}
type Z_DeleteTeamArgs struct {
A string
}
type Z_DeleteTeamReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteTeam(teamID string) *model.AppError {
_args := &Z_DeleteTeamArgs{teamID}
_returns := &Z_DeleteTeamReturns{}
if err := g.client.Call("Plugin.DeleteTeam", _args, _returns); err != nil {
log.Printf("RPC call to DeleteTeam API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteTeam(args *Z_DeleteTeamArgs, returns *Z_DeleteTeamReturns) error {
if hook, ok := s.impl.(interface {
DeleteTeam(teamID string) *model.AppError
}); ok {
returns.A = hook.DeleteTeam(args.A)
} else {
return encodableError(fmt.Errorf("API DeleteTeam called but not implemented."))
}
return nil
}
type Z_GetTeamsArgs struct {
}
type Z_GetTeamsReturns struct {
A []*model.Team
B *model.AppError
}
func (g *apiRPCClient) GetTeams() ([]*model.Team, *model.AppError) {
_args := &Z_GetTeamsArgs{}
_returns := &Z_GetTeamsReturns{}
if err := g.client.Call("Plugin.GetTeams", _args, _returns); err != nil {
log.Printf("RPC call to GetTeams API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeams(args *Z_GetTeamsArgs, returns *Z_GetTeamsReturns) error {
if hook, ok := s.impl.(interface {
GetTeams() ([]*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeams()
} else {
return encodableError(fmt.Errorf("API GetTeams called but not implemented."))
}
return nil
}
type Z_GetTeamArgs struct {
A string
}
type Z_GetTeamReturns struct {
A *model.Team
B *model.AppError
}
func (g *apiRPCClient) GetTeam(teamID string) (*model.Team, *model.AppError) {
_args := &Z_GetTeamArgs{teamID}
_returns := &Z_GetTeamReturns{}
if err := g.client.Call("Plugin.GetTeam", _args, _returns); err != nil {
log.Printf("RPC call to GetTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeam(args *Z_GetTeamArgs, returns *Z_GetTeamReturns) error {
if hook, ok := s.impl.(interface {
GetTeam(teamID string) (*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeam(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeam called but not implemented."))
}
return nil
}
type Z_GetTeamByNameArgs struct {
A string
}
type Z_GetTeamByNameReturns struct {
A *model.Team
B *model.AppError
}
func (g *apiRPCClient) GetTeamByName(name string) (*model.Team, *model.AppError) {
_args := &Z_GetTeamByNameArgs{name}
_returns := &Z_GetTeamByNameReturns{}
if err := g.client.Call("Plugin.GetTeamByName", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamByName API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamByName(args *Z_GetTeamByNameArgs, returns *Z_GetTeamByNameReturns) error {
if hook, ok := s.impl.(interface {
GetTeamByName(name string) (*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamByName(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeamByName called but not implemented."))
}
return nil
}
type Z_GetTeamsUnreadForUserArgs struct {
A string
}
type Z_GetTeamsUnreadForUserReturns struct {
A []*model.TeamUnread
B *model.AppError
}
func (g *apiRPCClient) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) {
_args := &Z_GetTeamsUnreadForUserArgs{userID}
_returns := &Z_GetTeamsUnreadForUserReturns{}
if err := g.client.Call("Plugin.GetTeamsUnreadForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamsUnreadForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamsUnreadForUser(args *Z_GetTeamsUnreadForUserArgs, returns *Z_GetTeamsUnreadForUserReturns) error {
if hook, ok := s.impl.(interface {
GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamsUnreadForUser(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeamsUnreadForUser called but not implemented."))
}
return nil
}
type Z_UpdateTeamArgs struct {
A *model.Team
}
type Z_UpdateTeamReturns struct {
A *model.Team
B *model.AppError
}
func (g *apiRPCClient) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
_args := &Z_UpdateTeamArgs{team}
_returns := &Z_UpdateTeamReturns{}
if err := g.client.Call("Plugin.UpdateTeam", _args, _returns); err != nil {
log.Printf("RPC call to UpdateTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateTeam(args *Z_UpdateTeamArgs, returns *Z_UpdateTeamReturns) error {
if hook, ok := s.impl.(interface {
UpdateTeam(team *model.Team) (*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateTeam(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateTeam called but not implemented."))
}
return nil
}
type Z_SearchTeamsArgs struct {
A string
}
type Z_SearchTeamsReturns struct {
A []*model.Team
B *model.AppError
}
func (g *apiRPCClient) SearchTeams(term string) ([]*model.Team, *model.AppError) {
_args := &Z_SearchTeamsArgs{term}
_returns := &Z_SearchTeamsReturns{}
if err := g.client.Call("Plugin.SearchTeams", _args, _returns); err != nil {
log.Printf("RPC call to SearchTeams API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SearchTeams(args *Z_SearchTeamsArgs, returns *Z_SearchTeamsReturns) error {
if hook, ok := s.impl.(interface {
SearchTeams(term string) ([]*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.SearchTeams(args.A)
} else {
return encodableError(fmt.Errorf("API SearchTeams called but not implemented."))
}
return nil
}
type Z_GetTeamsForUserArgs struct {
A string
}
type Z_GetTeamsForUserReturns struct {
A []*model.Team
B *model.AppError
}
func (g *apiRPCClient) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
_args := &Z_GetTeamsForUserArgs{userID}
_returns := &Z_GetTeamsForUserReturns{}
if err := g.client.Call("Plugin.GetTeamsForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamsForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamsForUser(args *Z_GetTeamsForUserArgs, returns *Z_GetTeamsForUserReturns) error {
if hook, ok := s.impl.(interface {
GetTeamsForUser(userID string) ([]*model.Team, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamsForUser(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeamsForUser called but not implemented."))
}
return nil
}
type Z_CreateTeamMemberArgs struct {
A string
B string
}
type Z_CreateTeamMemberReturns struct {
A *model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
_args := &Z_CreateTeamMemberArgs{teamID, userID}
_returns := &Z_CreateTeamMemberReturns{}
if err := g.client.Call("Plugin.CreateTeamMember", _args, _returns); err != nil {
log.Printf("RPC call to CreateTeamMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateTeamMember(args *Z_CreateTeamMemberArgs, returns *Z_CreateTeamMemberReturns) error {
if hook, ok := s.impl.(interface {
CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateTeamMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API CreateTeamMember called but not implemented."))
}
return nil
}
type Z_CreateTeamMembersArgs struct {
A string
B []string
C string
}
type Z_CreateTeamMembersReturns struct {
A []*model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
_args := &Z_CreateTeamMembersArgs{teamID, userIds, requestorId}
_returns := &Z_CreateTeamMembersReturns{}
if err := g.client.Call("Plugin.CreateTeamMembers", _args, _returns); err != nil {
log.Printf("RPC call to CreateTeamMembers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateTeamMembers(args *Z_CreateTeamMembersArgs, returns *Z_CreateTeamMembersReturns) error {
if hook, ok := s.impl.(interface {
CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateTeamMembers(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API CreateTeamMembers called but not implemented."))
}
return nil
}
type Z_CreateTeamMembersGracefullyArgs struct {
A string
B []string
C string
}
type Z_CreateTeamMembersGracefullyReturns struct {
A []*model.TeamMemberWithError
B *model.AppError
}
func (g *apiRPCClient) CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) {
_args := &Z_CreateTeamMembersGracefullyArgs{teamID, userIds, requestorId}
_returns := &Z_CreateTeamMembersGracefullyReturns{}
if err := g.client.Call("Plugin.CreateTeamMembersGracefully", _args, _returns); err != nil {
log.Printf("RPC call to CreateTeamMembersGracefully API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateTeamMembersGracefully(args *Z_CreateTeamMembersGracefullyArgs, returns *Z_CreateTeamMembersGracefullyReturns) error {
if hook, ok := s.impl.(interface {
CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateTeamMembersGracefully(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API CreateTeamMembersGracefully called but not implemented."))
}
return nil
}
type Z_DeleteTeamMemberArgs struct {
A string
B string
C string
}
type Z_DeleteTeamMemberReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError {
_args := &Z_DeleteTeamMemberArgs{teamID, userID, requestorId}
_returns := &Z_DeleteTeamMemberReturns{}
if err := g.client.Call("Plugin.DeleteTeamMember", _args, _returns); err != nil {
log.Printf("RPC call to DeleteTeamMember API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteTeamMember(args *Z_DeleteTeamMemberArgs, returns *Z_DeleteTeamMemberReturns) error {
if hook, ok := s.impl.(interface {
DeleteTeamMember(teamID, userID, requestorId string) *model.AppError
}); ok {
returns.A = hook.DeleteTeamMember(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API DeleteTeamMember called but not implemented."))
}
return nil
}
type Z_GetTeamMembersArgs struct {
A string
B int
C int
}
type Z_GetTeamMembersReturns struct {
A []*model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
_args := &Z_GetTeamMembersArgs{teamID, page, perPage}
_returns := &Z_GetTeamMembersReturns{}
if err := g.client.Call("Plugin.GetTeamMembers", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamMembers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamMembers(args *Z_GetTeamMembersArgs, returns *Z_GetTeamMembersReturns) error {
if hook, ok := s.impl.(interface {
GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamMembers(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetTeamMembers called but not implemented."))
}
return nil
}
type Z_GetTeamMemberArgs struct {
A string
B string
}
type Z_GetTeamMemberReturns struct {
A *model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
_args := &Z_GetTeamMemberArgs{teamID, userID}
_returns := &Z_GetTeamMemberReturns{}
if err := g.client.Call("Plugin.GetTeamMember", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamMember(args *Z_GetTeamMemberArgs, returns *Z_GetTeamMemberReturns) error {
if hook, ok := s.impl.(interface {
GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetTeamMember called but not implemented."))
}
return nil
}
type Z_GetTeamMembersForUserArgs struct {
A string
B int
C int
}
type Z_GetTeamMembersForUserReturns struct {
A []*model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
_args := &Z_GetTeamMembersForUserArgs{userID, page, perPage}
_returns := &Z_GetTeamMembersForUserReturns{}
if err := g.client.Call("Plugin.GetTeamMembersForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamMembersForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamMembersForUser(args *Z_GetTeamMembersForUserArgs, returns *Z_GetTeamMembersForUserReturns) error {
if hook, ok := s.impl.(interface {
GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamMembersForUser(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetTeamMembersForUser called but not implemented."))
}
return nil
}
type Z_UpdateTeamMemberRolesArgs struct {
A string
B string
C string
}
type Z_UpdateTeamMemberRolesReturns struct {
A *model.TeamMember
B *model.AppError
}
func (g *apiRPCClient) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) {
_args := &Z_UpdateTeamMemberRolesArgs{teamID, userID, newRoles}
_returns := &Z_UpdateTeamMemberRolesReturns{}
if err := g.client.Call("Plugin.UpdateTeamMemberRoles", _args, _returns); err != nil {
log.Printf("RPC call to UpdateTeamMemberRoles API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateTeamMemberRoles(args *Z_UpdateTeamMemberRolesArgs, returns *Z_UpdateTeamMemberRolesReturns) error {
if hook, ok := s.impl.(interface {
UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateTeamMemberRoles(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API UpdateTeamMemberRoles called but not implemented."))
}
return nil
}
type Z_CreateChannelArgs struct {
A *model.Channel
}
type Z_CreateChannelReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
_args := &Z_CreateChannelArgs{channel}
_returns := &Z_CreateChannelReturns{}
if err := g.client.Call("Plugin.CreateChannel", _args, _returns); err != nil {
log.Printf("RPC call to CreateChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateChannel(args *Z_CreateChannelArgs, returns *Z_CreateChannelReturns) error {
if hook, ok := s.impl.(interface {
CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateChannel(args.A)
} else {
return encodableError(fmt.Errorf("API CreateChannel called but not implemented."))
}
return nil
}
type Z_DeleteChannelArgs struct {
A string
}
type Z_DeleteChannelReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteChannel(channelId string) *model.AppError {
_args := &Z_DeleteChannelArgs{channelId}
_returns := &Z_DeleteChannelReturns{}
if err := g.client.Call("Plugin.DeleteChannel", _args, _returns); err != nil {
log.Printf("RPC call to DeleteChannel API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteChannel(args *Z_DeleteChannelArgs, returns *Z_DeleteChannelReturns) error {
if hook, ok := s.impl.(interface {
DeleteChannel(channelId string) *model.AppError
}); ok {
returns.A = hook.DeleteChannel(args.A)
} else {
return encodableError(fmt.Errorf("API DeleteChannel called but not implemented."))
}
return nil
}
type Z_GetPublicChannelsForTeamArgs struct {
A string
B int
C int
}
type Z_GetPublicChannelsForTeamReturns struct {
A []*model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) {
_args := &Z_GetPublicChannelsForTeamArgs{teamID, page, perPage}
_returns := &Z_GetPublicChannelsForTeamReturns{}
if err := g.client.Call("Plugin.GetPublicChannelsForTeam", _args, _returns); err != nil {
log.Printf("RPC call to GetPublicChannelsForTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPublicChannelsForTeam(args *Z_GetPublicChannelsForTeamArgs, returns *Z_GetPublicChannelsForTeamReturns) error {
if hook, ok := s.impl.(interface {
GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPublicChannelsForTeam(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetPublicChannelsForTeam called but not implemented."))
}
return nil
}
type Z_GetChannelArgs struct {
A string
}
type Z_GetChannelReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetChannel(channelId string) (*model.Channel, *model.AppError) {
_args := &Z_GetChannelArgs{channelId}
_returns := &Z_GetChannelReturns{}
if err := g.client.Call("Plugin.GetChannel", _args, _returns); err != nil {
log.Printf("RPC call to GetChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannel(args *Z_GetChannelArgs, returns *Z_GetChannelReturns) error {
if hook, ok := s.impl.(interface {
GetChannel(channelId string) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannel(args.A)
} else {
return encodableError(fmt.Errorf("API GetChannel called but not implemented."))
}
return nil
}
type Z_GetChannelByNameArgs struct {
A string
B string
C bool
}
type Z_GetChannelByNameReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) {
_args := &Z_GetChannelByNameArgs{teamID, name, includeDeleted}
_returns := &Z_GetChannelByNameReturns{}
if err := g.client.Call("Plugin.GetChannelByName", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelByName API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelByName(args *Z_GetChannelByNameArgs, returns *Z_GetChannelByNameReturns) error {
if hook, ok := s.impl.(interface {
GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelByName(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetChannelByName called but not implemented."))
}
return nil
}
type Z_GetChannelByNameForTeamNameArgs struct {
A string
B string
C bool
}
type Z_GetChannelByNameForTeamNameReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) {
_args := &Z_GetChannelByNameForTeamNameArgs{teamName, channelName, includeDeleted}
_returns := &Z_GetChannelByNameForTeamNameReturns{}
if err := g.client.Call("Plugin.GetChannelByNameForTeamName", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelByNameForTeamName API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelByNameForTeamName(args *Z_GetChannelByNameForTeamNameArgs, returns *Z_GetChannelByNameForTeamNameReturns) error {
if hook, ok := s.impl.(interface {
GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelByNameForTeamName(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetChannelByNameForTeamName called but not implemented."))
}
return nil
}
type Z_GetChannelsForTeamForUserArgs struct {
A string
B string
C bool
}
type Z_GetChannelsForTeamForUserReturns struct {
A []*model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
_args := &Z_GetChannelsForTeamForUserArgs{teamID, userID, includeDeleted}
_returns := &Z_GetChannelsForTeamForUserReturns{}
if err := g.client.Call("Plugin.GetChannelsForTeamForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelsForTeamForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelsForTeamForUser(args *Z_GetChannelsForTeamForUserArgs, returns *Z_GetChannelsForTeamForUserReturns) error {
if hook, ok := s.impl.(interface {
GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelsForTeamForUser(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetChannelsForTeamForUser called but not implemented."))
}
return nil
}
type Z_GetChannelStatsArgs struct {
A string
}
type Z_GetChannelStatsReturns struct {
A *model.ChannelStats
B *model.AppError
}
func (g *apiRPCClient) GetChannelStats(channelId string) (*model.ChannelStats, *model.AppError) {
_args := &Z_GetChannelStatsArgs{channelId}
_returns := &Z_GetChannelStatsReturns{}
if err := g.client.Call("Plugin.GetChannelStats", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelStats API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelStats(args *Z_GetChannelStatsArgs, returns *Z_GetChannelStatsReturns) error {
if hook, ok := s.impl.(interface {
GetChannelStats(channelId string) (*model.ChannelStats, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelStats(args.A)
} else {
return encodableError(fmt.Errorf("API GetChannelStats called but not implemented."))
}
return nil
}
type Z_GetDirectChannelArgs struct {
A string
B string
}
type Z_GetDirectChannelReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
_args := &Z_GetDirectChannelArgs{userId1, userId2}
_returns := &Z_GetDirectChannelReturns{}
if err := g.client.Call("Plugin.GetDirectChannel", _args, _returns); err != nil {
log.Printf("RPC call to GetDirectChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetDirectChannel(args *Z_GetDirectChannelArgs, returns *Z_GetDirectChannelReturns) error {
if hook, ok := s.impl.(interface {
GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetDirectChannel(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetDirectChannel called but not implemented."))
}
return nil
}
type Z_GetGroupChannelArgs struct {
A []string
}
type Z_GetGroupChannelReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
_args := &Z_GetGroupChannelArgs{userIds}
_returns := &Z_GetGroupChannelReturns{}
if err := g.client.Call("Plugin.GetGroupChannel", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupChannel(args *Z_GetGroupChannelArgs, returns *Z_GetGroupChannelReturns) error {
if hook, ok := s.impl.(interface {
GetGroupChannel(userIds []string) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupChannel(args.A)
} else {
return encodableError(fmt.Errorf("API GetGroupChannel called but not implemented."))
}
return nil
}
type Z_UpdateChannelArgs struct {
A *model.Channel
}
type Z_UpdateChannelReturns struct {
A *model.Channel
B *model.AppError
}
func (g *apiRPCClient) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
_args := &Z_UpdateChannelArgs{channel}
_returns := &Z_UpdateChannelReturns{}
if err := g.client.Call("Plugin.UpdateChannel", _args, _returns); err != nil {
log.Printf("RPC call to UpdateChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateChannel(args *Z_UpdateChannelArgs, returns *Z_UpdateChannelReturns) error {
if hook, ok := s.impl.(interface {
UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateChannel(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateChannel called but not implemented."))
}
return nil
}
type Z_SearchChannelsArgs struct {
A string
B string
}
type Z_SearchChannelsReturns struct {
A []*model.Channel
B *model.AppError
}
func (g *apiRPCClient) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) {
_args := &Z_SearchChannelsArgs{teamID, term}
_returns := &Z_SearchChannelsReturns{}
if err := g.client.Call("Plugin.SearchChannels", _args, _returns); err != nil {
log.Printf("RPC call to SearchChannels API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SearchChannels(args *Z_SearchChannelsArgs, returns *Z_SearchChannelsReturns) error {
if hook, ok := s.impl.(interface {
SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError)
}); ok {
returns.A, returns.B = hook.SearchChannels(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SearchChannels called but not implemented."))
}
return nil
}
type Z_CreateChannelSidebarCategoryArgs struct {
A string
B string
C *model.SidebarCategoryWithChannels
}
type Z_CreateChannelSidebarCategoryReturns struct {
A *model.SidebarCategoryWithChannels
B *model.AppError
}
func (g *apiRPCClient) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
_args := &Z_CreateChannelSidebarCategoryArgs{userID, teamID, newCategory}
_returns := &Z_CreateChannelSidebarCategoryReturns{}
if err := g.client.Call("Plugin.CreateChannelSidebarCategory", _args, _returns); err != nil {
log.Printf("RPC call to CreateChannelSidebarCategory API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateChannelSidebarCategory(args *Z_CreateChannelSidebarCategoryArgs, returns *Z_CreateChannelSidebarCategoryReturns) error {
if hook, ok := s.impl.(interface {
CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateChannelSidebarCategory(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API CreateChannelSidebarCategory called but not implemented."))
}
return nil
}
type Z_GetChannelSidebarCategoriesArgs struct {
A string
B string
}
type Z_GetChannelSidebarCategoriesReturns struct {
A *model.OrderedSidebarCategories
B *model.AppError
}
func (g *apiRPCClient) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
_args := &Z_GetChannelSidebarCategoriesArgs{userID, teamID}
_returns := &Z_GetChannelSidebarCategoriesReturns{}
if err := g.client.Call("Plugin.GetChannelSidebarCategories", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelSidebarCategories API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelSidebarCategories(args *Z_GetChannelSidebarCategoriesArgs, returns *Z_GetChannelSidebarCategoriesReturns) error {
if hook, ok := s.impl.(interface {
GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelSidebarCategories(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetChannelSidebarCategories called but not implemented."))
}
return nil
}
type Z_UpdateChannelSidebarCategoriesArgs struct {
A string
B string
C []*model.SidebarCategoryWithChannels
}
type Z_UpdateChannelSidebarCategoriesReturns struct {
A []*model.SidebarCategoryWithChannels
B *model.AppError
}
func (g *apiRPCClient) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
_args := &Z_UpdateChannelSidebarCategoriesArgs{userID, teamID, categories}
_returns := &Z_UpdateChannelSidebarCategoriesReturns{}
if err := g.client.Call("Plugin.UpdateChannelSidebarCategories", _args, _returns); err != nil {
log.Printf("RPC call to UpdateChannelSidebarCategories API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateChannelSidebarCategories(args *Z_UpdateChannelSidebarCategoriesArgs, returns *Z_UpdateChannelSidebarCategoriesReturns) error {
if hook, ok := s.impl.(interface {
UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateChannelSidebarCategories(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API UpdateChannelSidebarCategories called but not implemented."))
}
return nil
}
type Z_SearchUsersArgs struct {
A *model.UserSearch
}
type Z_SearchUsersReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) {
_args := &Z_SearchUsersArgs{search}
_returns := &Z_SearchUsersReturns{}
if err := g.client.Call("Plugin.SearchUsers", _args, _returns); err != nil {
log.Printf("RPC call to SearchUsers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SearchUsers(args *Z_SearchUsersArgs, returns *Z_SearchUsersReturns) error {
if hook, ok := s.impl.(interface {
SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.SearchUsers(args.A)
} else {
return encodableError(fmt.Errorf("API SearchUsers called but not implemented."))
}
return nil
}
type Z_SearchPostsInTeamArgs struct {
A string
B []*model.SearchParams
}
type Z_SearchPostsInTeamReturns struct {
A []*model.Post
B *model.AppError
}
func (g *apiRPCClient) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) {
_args := &Z_SearchPostsInTeamArgs{teamID, paramsList}
_returns := &Z_SearchPostsInTeamReturns{}
if err := g.client.Call("Plugin.SearchPostsInTeam", _args, _returns); err != nil {
log.Printf("RPC call to SearchPostsInTeam API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SearchPostsInTeam(args *Z_SearchPostsInTeamArgs, returns *Z_SearchPostsInTeamReturns) error {
if hook, ok := s.impl.(interface {
SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError)
}); ok {
returns.A, returns.B = hook.SearchPostsInTeam(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SearchPostsInTeam called but not implemented."))
}
return nil
}
type Z_SearchPostsInTeamForUserArgs struct {
A string
B string
C model.SearchParameter
}
type Z_SearchPostsInTeamForUserReturns struct {
A *model.PostSearchResults
B *model.AppError
}
func (g *apiRPCClient) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) {
_args := &Z_SearchPostsInTeamForUserArgs{teamID, userID, searchParams}
_returns := &Z_SearchPostsInTeamForUserReturns{}
if err := g.client.Call("Plugin.SearchPostsInTeamForUser", _args, _returns); err != nil {
log.Printf("RPC call to SearchPostsInTeamForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) SearchPostsInTeamForUser(args *Z_SearchPostsInTeamForUserArgs, returns *Z_SearchPostsInTeamForUserReturns) error {
if hook, ok := s.impl.(interface {
SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError)
}); ok {
returns.A, returns.B = hook.SearchPostsInTeamForUser(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API SearchPostsInTeamForUser called but not implemented."))
}
return nil
}
type Z_AddChannelMemberArgs struct {
A string
B string
}
type Z_AddChannelMemberReturns struct {
A *model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) {
_args := &Z_AddChannelMemberArgs{channelId, userID}
_returns := &Z_AddChannelMemberReturns{}
if err := g.client.Call("Plugin.AddChannelMember", _args, _returns); err != nil {
log.Printf("RPC call to AddChannelMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) AddChannelMember(args *Z_AddChannelMemberArgs, returns *Z_AddChannelMemberReturns) error {
if hook, ok := s.impl.(interface {
AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.AddChannelMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API AddChannelMember called but not implemented."))
}
return nil
}
type Z_AddUserToChannelArgs struct {
A string
B string
C string
}
type Z_AddUserToChannelReturns struct {
A *model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError) {
_args := &Z_AddUserToChannelArgs{channelId, userID, asUserId}
_returns := &Z_AddUserToChannelReturns{}
if err := g.client.Call("Plugin.AddUserToChannel", _args, _returns); err != nil {
log.Printf("RPC call to AddUserToChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) AddUserToChannel(args *Z_AddUserToChannelArgs, returns *Z_AddUserToChannelReturns) error {
if hook, ok := s.impl.(interface {
AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.AddUserToChannel(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API AddUserToChannel called but not implemented."))
}
return nil
}
type Z_GetChannelMemberArgs struct {
A string
B string
}
type Z_GetChannelMemberReturns struct {
A *model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) {
_args := &Z_GetChannelMemberArgs{channelId, userID}
_returns := &Z_GetChannelMemberReturns{}
if err := g.client.Call("Plugin.GetChannelMember", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelMember API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelMember(args *Z_GetChannelMemberArgs, returns *Z_GetChannelMemberReturns) error {
if hook, ok := s.impl.(interface {
GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetChannelMember called but not implemented."))
}
return nil
}
type Z_GetChannelMembersArgs struct {
A string
B int
C int
}
type Z_GetChannelMembersReturns struct {
A model.ChannelMembers
B *model.AppError
}
func (g *apiRPCClient) GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError) {
_args := &Z_GetChannelMembersArgs{channelId, page, perPage}
_returns := &Z_GetChannelMembersReturns{}
if err := g.client.Call("Plugin.GetChannelMembers", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelMembers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelMembers(args *Z_GetChannelMembersArgs, returns *Z_GetChannelMembersReturns) error {
if hook, ok := s.impl.(interface {
GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelMembers(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetChannelMembers called but not implemented."))
}
return nil
}
type Z_GetChannelMembersByIdsArgs struct {
A string
B []string
}
type Z_GetChannelMembersByIdsReturns struct {
A model.ChannelMembers
B *model.AppError
}
func (g *apiRPCClient) GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError) {
_args := &Z_GetChannelMembersByIdsArgs{channelId, userIds}
_returns := &Z_GetChannelMembersByIdsReturns{}
if err := g.client.Call("Plugin.GetChannelMembersByIds", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelMembersByIds API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelMembersByIds(args *Z_GetChannelMembersByIdsArgs, returns *Z_GetChannelMembersByIdsReturns) error {
if hook, ok := s.impl.(interface {
GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelMembersByIds(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetChannelMembersByIds called but not implemented."))
}
return nil
}
type Z_GetChannelMembersForUserArgs struct {
A string
B string
C int
D int
}
type Z_GetChannelMembersForUserReturns struct {
A []*model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
_args := &Z_GetChannelMembersForUserArgs{teamID, userID, page, perPage}
_returns := &Z_GetChannelMembersForUserReturns{}
if err := g.client.Call("Plugin.GetChannelMembersForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetChannelMembersForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetChannelMembersForUser(args *Z_GetChannelMembersForUserArgs, returns *Z_GetChannelMembersForUserReturns) error {
if hook, ok := s.impl.(interface {
GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetChannelMembersForUser(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API GetChannelMembersForUser called but not implemented."))
}
return nil
}
type Z_UpdateChannelMemberRolesArgs struct {
A string
B string
C string
}
type Z_UpdateChannelMemberRolesReturns struct {
A *model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
_args := &Z_UpdateChannelMemberRolesArgs{channelId, userID, newRoles}
_returns := &Z_UpdateChannelMemberRolesReturns{}
if err := g.client.Call("Plugin.UpdateChannelMemberRoles", _args, _returns); err != nil {
log.Printf("RPC call to UpdateChannelMemberRoles API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateChannelMemberRoles(args *Z_UpdateChannelMemberRolesArgs, returns *Z_UpdateChannelMemberRolesReturns) error {
if hook, ok := s.impl.(interface {
UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateChannelMemberRoles(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API UpdateChannelMemberRoles called but not implemented."))
}
return nil
}
type Z_UpdateChannelMemberNotificationsArgs struct {
A string
B string
C map[string]string
}
type Z_UpdateChannelMemberNotificationsReturns struct {
A *model.ChannelMember
B *model.AppError
}
func (g *apiRPCClient) UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
_args := &Z_UpdateChannelMemberNotificationsArgs{channelId, userID, notifications}
_returns := &Z_UpdateChannelMemberNotificationsReturns{}
if err := g.client.Call("Plugin.UpdateChannelMemberNotifications", _args, _returns); err != nil {
log.Printf("RPC call to UpdateChannelMemberNotifications API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateChannelMemberNotifications(args *Z_UpdateChannelMemberNotificationsArgs, returns *Z_UpdateChannelMemberNotificationsReturns) error {
if hook, ok := s.impl.(interface {
UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateChannelMemberNotifications(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API UpdateChannelMemberNotifications called but not implemented."))
}
return nil
}
type Z_GetGroupArgs struct {
A string
}
type Z_GetGroupReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroup(groupId string) (*model.Group, *model.AppError) {
_args := &Z_GetGroupArgs{groupId}
_returns := &Z_GetGroupReturns{}
if err := g.client.Call("Plugin.GetGroup", _args, _returns); err != nil {
log.Printf("RPC call to GetGroup API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroup(args *Z_GetGroupArgs, returns *Z_GetGroupReturns) error {
if hook, ok := s.impl.(interface {
GetGroup(groupId string) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroup(args.A)
} else {
return encodableError(fmt.Errorf("API GetGroup called but not implemented."))
}
return nil
}
type Z_GetGroupByNameArgs struct {
A string
}
type Z_GetGroupByNameReturns struct {
A *model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroupByName(name string) (*model.Group, *model.AppError) {
_args := &Z_GetGroupByNameArgs{name}
_returns := &Z_GetGroupByNameReturns{}
if err := g.client.Call("Plugin.GetGroupByName", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupByName API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupByName(args *Z_GetGroupByNameArgs, returns *Z_GetGroupByNameReturns) error {
if hook, ok := s.impl.(interface {
GetGroupByName(name string) (*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupByName(args.A)
} else {
return encodableError(fmt.Errorf("API GetGroupByName called but not implemented."))
}
return nil
}
type Z_GetGroupMemberUsersArgs struct {
A string
B int
C int
}
type Z_GetGroupMemberUsersReturns struct {
A []*model.User
B *model.AppError
}
func (g *apiRPCClient) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
_args := &Z_GetGroupMemberUsersArgs{groupID, page, perPage}
_returns := &Z_GetGroupMemberUsersReturns{}
if err := g.client.Call("Plugin.GetGroupMemberUsers", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupMemberUsers API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupMemberUsers(args *Z_GetGroupMemberUsersArgs, returns *Z_GetGroupMemberUsersReturns) error {
if hook, ok := s.impl.(interface {
GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupMemberUsers(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetGroupMemberUsers called but not implemented."))
}
return nil
}
type Z_GetGroupsBySourceArgs struct {
A model.GroupSource
}
type Z_GetGroupsBySourceReturns struct {
A []*model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
_args := &Z_GetGroupsBySourceArgs{groupSource}
_returns := &Z_GetGroupsBySourceReturns{}
if err := g.client.Call("Plugin.GetGroupsBySource", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupsBySource API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupsBySource(args *Z_GetGroupsBySourceArgs, returns *Z_GetGroupsBySourceReturns) error {
if hook, ok := s.impl.(interface {
GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupsBySource(args.A)
} else {
return encodableError(fmt.Errorf("API GetGroupsBySource called but not implemented."))
}
return nil
}
type Z_GetGroupsForUserArgs struct {
A string
}
type Z_GetGroupsForUserReturns struct {
A []*model.Group
B *model.AppError
}
func (g *apiRPCClient) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) {
_args := &Z_GetGroupsForUserArgs{userID}
_returns := &Z_GetGroupsForUserReturns{}
if err := g.client.Call("Plugin.GetGroupsForUser", _args, _returns); err != nil {
log.Printf("RPC call to GetGroupsForUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetGroupsForUser(args *Z_GetGroupsForUserArgs, returns *Z_GetGroupsForUserReturns) error {
if hook, ok := s.impl.(interface {
GetGroupsForUser(userID string) ([]*model.Group, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetGroupsForUser(args.A)
} else {
return encodableError(fmt.Errorf("API GetGroupsForUser called but not implemented."))
}
return nil
}
type Z_DeleteChannelMemberArgs struct {
A string
B string
}
type Z_DeleteChannelMemberReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteChannelMember(channelId, userID string) *model.AppError {
_args := &Z_DeleteChannelMemberArgs{channelId, userID}
_returns := &Z_DeleteChannelMemberReturns{}
if err := g.client.Call("Plugin.DeleteChannelMember", _args, _returns); err != nil {
log.Printf("RPC call to DeleteChannelMember API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteChannelMember(args *Z_DeleteChannelMemberArgs, returns *Z_DeleteChannelMemberReturns) error {
if hook, ok := s.impl.(interface {
DeleteChannelMember(channelId, userID string) *model.AppError
}); ok {
returns.A = hook.DeleteChannelMember(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API DeleteChannelMember called but not implemented."))
}
return nil
}
type Z_CreatePostArgs struct {
A *model.Post
}
type Z_CreatePostReturns struct {
A *model.Post
B *model.AppError
}
func (g *apiRPCClient) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
_args := &Z_CreatePostArgs{post}
_returns := &Z_CreatePostReturns{}
if err := g.client.Call("Plugin.CreatePost", _args, _returns); err != nil {
log.Printf("RPC call to CreatePost API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreatePost(args *Z_CreatePostArgs, returns *Z_CreatePostReturns) error {
if hook, ok := s.impl.(interface {
CreatePost(post *model.Post) (*model.Post, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreatePost(args.A)
} else {
return encodableError(fmt.Errorf("API CreatePost called but not implemented."))
}
return nil
}
type Z_AddReactionArgs struct {
A *model.Reaction
}
type Z_AddReactionReturns struct {
A *model.Reaction
B *model.AppError
}
func (g *apiRPCClient) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) {
_args := &Z_AddReactionArgs{reaction}
_returns := &Z_AddReactionReturns{}
if err := g.client.Call("Plugin.AddReaction", _args, _returns); err != nil {
log.Printf("RPC call to AddReaction API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) AddReaction(args *Z_AddReactionArgs, returns *Z_AddReactionReturns) error {
if hook, ok := s.impl.(interface {
AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError)
}); ok {
returns.A, returns.B = hook.AddReaction(args.A)
} else {
return encodableError(fmt.Errorf("API AddReaction called but not implemented."))
}
return nil
}
type Z_RemoveReactionArgs struct {
A *model.Reaction
}
type Z_RemoveReactionReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RemoveReaction(reaction *model.Reaction) *model.AppError {
_args := &Z_RemoveReactionArgs{reaction}
_returns := &Z_RemoveReactionReturns{}
if err := g.client.Call("Plugin.RemoveReaction", _args, _returns); err != nil {
log.Printf("RPC call to RemoveReaction API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RemoveReaction(args *Z_RemoveReactionArgs, returns *Z_RemoveReactionReturns) error {
if hook, ok := s.impl.(interface {
RemoveReaction(reaction *model.Reaction) *model.AppError
}); ok {
returns.A = hook.RemoveReaction(args.A)
} else {
return encodableError(fmt.Errorf("API RemoveReaction called but not implemented."))
}
return nil
}
type Z_GetReactionsArgs struct {
A string
}
type Z_GetReactionsReturns struct {
A []*model.Reaction
B *model.AppError
}
func (g *apiRPCClient) GetReactions(postId string) ([]*model.Reaction, *model.AppError) {
_args := &Z_GetReactionsArgs{postId}
_returns := &Z_GetReactionsReturns{}
if err := g.client.Call("Plugin.GetReactions", _args, _returns); err != nil {
log.Printf("RPC call to GetReactions API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetReactions(args *Z_GetReactionsArgs, returns *Z_GetReactionsReturns) error {
if hook, ok := s.impl.(interface {
GetReactions(postId string) ([]*model.Reaction, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetReactions(args.A)
} else {
return encodableError(fmt.Errorf("API GetReactions called but not implemented."))
}
return nil
}
type Z_SendEphemeralPostArgs struct {
A string
B *model.Post
}
type Z_SendEphemeralPostReturns struct {
A *model.Post
}
func (g *apiRPCClient) SendEphemeralPost(userID string, post *model.Post) *model.Post {
_args := &Z_SendEphemeralPostArgs{userID, post}
_returns := &Z_SendEphemeralPostReturns{}
if err := g.client.Call("Plugin.SendEphemeralPost", _args, _returns); err != nil {
log.Printf("RPC call to SendEphemeralPost API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SendEphemeralPost(args *Z_SendEphemeralPostArgs, returns *Z_SendEphemeralPostReturns) error {
if hook, ok := s.impl.(interface {
SendEphemeralPost(userID string, post *model.Post) *model.Post
}); ok {
returns.A = hook.SendEphemeralPost(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SendEphemeralPost called but not implemented."))
}
return nil
}
type Z_UpdateEphemeralPostArgs struct {
A string
B *model.Post
}
type Z_UpdateEphemeralPostReturns struct {
A *model.Post
}
func (g *apiRPCClient) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
_args := &Z_UpdateEphemeralPostArgs{userID, post}
_returns := &Z_UpdateEphemeralPostReturns{}
if err := g.client.Call("Plugin.UpdateEphemeralPost", _args, _returns); err != nil {
log.Printf("RPC call to UpdateEphemeralPost API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) UpdateEphemeralPost(args *Z_UpdateEphemeralPostArgs, returns *Z_UpdateEphemeralPostReturns) error {
if hook, ok := s.impl.(interface {
UpdateEphemeralPost(userID string, post *model.Post) *model.Post
}); ok {
returns.A = hook.UpdateEphemeralPost(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateEphemeralPost called but not implemented."))
}
return nil
}
type Z_DeleteEphemeralPostArgs struct {
A string
B string
}
type Z_DeleteEphemeralPostReturns struct {
}
func (g *apiRPCClient) DeleteEphemeralPost(userID, postId string) {
_args := &Z_DeleteEphemeralPostArgs{userID, postId}
_returns := &Z_DeleteEphemeralPostReturns{}
if err := g.client.Call("Plugin.DeleteEphemeralPost", _args, _returns); err != nil {
log.Printf("RPC call to DeleteEphemeralPost API failed: %s", err.Error())
}
}
func (s *apiRPCServer) DeleteEphemeralPost(args *Z_DeleteEphemeralPostArgs, returns *Z_DeleteEphemeralPostReturns) error {
if hook, ok := s.impl.(interface {
DeleteEphemeralPost(userID, postId string)
}); ok {
hook.DeleteEphemeralPost(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API DeleteEphemeralPost called but not implemented."))
}
return nil
}
type Z_DeletePostArgs struct {
A string
}
type Z_DeletePostReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeletePost(postId string) *model.AppError {
_args := &Z_DeletePostArgs{postId}
_returns := &Z_DeletePostReturns{}
if err := g.client.Call("Plugin.DeletePost", _args, _returns); err != nil {
log.Printf("RPC call to DeletePost API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeletePost(args *Z_DeletePostArgs, returns *Z_DeletePostReturns) error {
if hook, ok := s.impl.(interface {
DeletePost(postId string) *model.AppError
}); ok {
returns.A = hook.DeletePost(args.A)
} else {
return encodableError(fmt.Errorf("API DeletePost called but not implemented."))
}
return nil
}
type Z_GetPostThreadArgs struct {
A string
}
type Z_GetPostThreadReturns struct {
A *model.PostList
B *model.AppError
}
func (g *apiRPCClient) GetPostThread(postId string) (*model.PostList, *model.AppError) {
_args := &Z_GetPostThreadArgs{postId}
_returns := &Z_GetPostThreadReturns{}
if err := g.client.Call("Plugin.GetPostThread", _args, _returns); err != nil {
log.Printf("RPC call to GetPostThread API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPostThread(args *Z_GetPostThreadArgs, returns *Z_GetPostThreadReturns) error {
if hook, ok := s.impl.(interface {
GetPostThread(postId string) (*model.PostList, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPostThread(args.A)
} else {
return encodableError(fmt.Errorf("API GetPostThread called but not implemented."))
}
return nil
}
type Z_GetPostArgs struct {
A string
}
type Z_GetPostReturns struct {
A *model.Post
B *model.AppError
}
func (g *apiRPCClient) GetPost(postId string) (*model.Post, *model.AppError) {
_args := &Z_GetPostArgs{postId}
_returns := &Z_GetPostReturns{}
if err := g.client.Call("Plugin.GetPost", _args, _returns); err != nil {
log.Printf("RPC call to GetPost API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPost(args *Z_GetPostArgs, returns *Z_GetPostReturns) error {
if hook, ok := s.impl.(interface {
GetPost(postId string) (*model.Post, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPost(args.A)
} else {
return encodableError(fmt.Errorf("API GetPost called but not implemented."))
}
return nil
}
type Z_GetPostsSinceArgs struct {
A string
B int64
}
type Z_GetPostsSinceReturns struct {
A *model.PostList
B *model.AppError
}
func (g *apiRPCClient) GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) {
_args := &Z_GetPostsSinceArgs{channelId, time}
_returns := &Z_GetPostsSinceReturns{}
if err := g.client.Call("Plugin.GetPostsSince", _args, _returns); err != nil {
log.Printf("RPC call to GetPostsSince API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPostsSince(args *Z_GetPostsSinceArgs, returns *Z_GetPostsSinceReturns) error {
if hook, ok := s.impl.(interface {
GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPostsSince(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetPostsSince called but not implemented."))
}
return nil
}
type Z_GetPostsAfterArgs struct {
A string
B string
C int
D int
}
type Z_GetPostsAfterReturns struct {
A *model.PostList
B *model.AppError
}
func (g *apiRPCClient) GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) {
_args := &Z_GetPostsAfterArgs{channelId, postId, page, perPage}
_returns := &Z_GetPostsAfterReturns{}
if err := g.client.Call("Plugin.GetPostsAfter", _args, _returns); err != nil {
log.Printf("RPC call to GetPostsAfter API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPostsAfter(args *Z_GetPostsAfterArgs, returns *Z_GetPostsAfterReturns) error {
if hook, ok := s.impl.(interface {
GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPostsAfter(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API GetPostsAfter called but not implemented."))
}
return nil
}
type Z_GetPostsBeforeArgs struct {
A string
B string
C int
D int
}
type Z_GetPostsBeforeReturns struct {
A *model.PostList
B *model.AppError
}
func (g *apiRPCClient) GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) {
_args := &Z_GetPostsBeforeArgs{channelId, postId, page, perPage}
_returns := &Z_GetPostsBeforeReturns{}
if err := g.client.Call("Plugin.GetPostsBefore", _args, _returns); err != nil {
log.Printf("RPC call to GetPostsBefore API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPostsBefore(args *Z_GetPostsBeforeArgs, returns *Z_GetPostsBeforeReturns) error {
if hook, ok := s.impl.(interface {
GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPostsBefore(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API GetPostsBefore called but not implemented."))
}
return nil
}
type Z_GetPostsForChannelArgs struct {
A string
B int
C int
}
type Z_GetPostsForChannelReturns struct {
A *model.PostList
B *model.AppError
}
func (g *apiRPCClient) GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError) {
_args := &Z_GetPostsForChannelArgs{channelId, page, perPage}
_returns := &Z_GetPostsForChannelReturns{}
if err := g.client.Call("Plugin.GetPostsForChannel", _args, _returns); err != nil {
log.Printf("RPC call to GetPostsForChannel API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPostsForChannel(args *Z_GetPostsForChannelArgs, returns *Z_GetPostsForChannelReturns) error {
if hook, ok := s.impl.(interface {
GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPostsForChannel(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetPostsForChannel called but not implemented."))
}
return nil
}
type Z_GetTeamStatsArgs struct {
A string
}
type Z_GetTeamStatsReturns struct {
A *model.TeamStats
B *model.AppError
}
func (g *apiRPCClient) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) {
_args := &Z_GetTeamStatsArgs{teamID}
_returns := &Z_GetTeamStatsReturns{}
if err := g.client.Call("Plugin.GetTeamStats", _args, _returns); err != nil {
log.Printf("RPC call to GetTeamStats API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetTeamStats(args *Z_GetTeamStatsArgs, returns *Z_GetTeamStatsReturns) error {
if hook, ok := s.impl.(interface {
GetTeamStats(teamID string) (*model.TeamStats, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetTeamStats(args.A)
} else {
return encodableError(fmt.Errorf("API GetTeamStats called but not implemented."))
}
return nil
}
type Z_UpdatePostArgs struct {
A *model.Post
}
type Z_UpdatePostReturns struct {
A *model.Post
B *model.AppError
}
func (g *apiRPCClient) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
_args := &Z_UpdatePostArgs{post}
_returns := &Z_UpdatePostReturns{}
if err := g.client.Call("Plugin.UpdatePost", _args, _returns); err != nil {
log.Printf("RPC call to UpdatePost API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdatePost(args *Z_UpdatePostArgs, returns *Z_UpdatePostReturns) error {
if hook, ok := s.impl.(interface {
UpdatePost(post *model.Post) (*model.Post, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdatePost(args.A)
} else {
return encodableError(fmt.Errorf("API UpdatePost called but not implemented."))
}
return nil
}
type Z_GetProfileImageArgs struct {
A string
}
type Z_GetProfileImageReturns struct {
A []byte
B *model.AppError
}
func (g *apiRPCClient) GetProfileImage(userID string) ([]byte, *model.AppError) {
_args := &Z_GetProfileImageArgs{userID}
_returns := &Z_GetProfileImageReturns{}
if err := g.client.Call("Plugin.GetProfileImage", _args, _returns); err != nil {
log.Printf("RPC call to GetProfileImage API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetProfileImage(args *Z_GetProfileImageArgs, returns *Z_GetProfileImageReturns) error {
if hook, ok := s.impl.(interface {
GetProfileImage(userID string) ([]byte, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetProfileImage(args.A)
} else {
return encodableError(fmt.Errorf("API GetProfileImage called but not implemented."))
}
return nil
}
type Z_SetProfileImageArgs struct {
A string
B []byte
}
type Z_SetProfileImageReturns struct {
A *model.AppError
}
func (g *apiRPCClient) SetProfileImage(userID string, data []byte) *model.AppError {
_args := &Z_SetProfileImageArgs{userID, data}
_returns := &Z_SetProfileImageReturns{}
if err := g.client.Call("Plugin.SetProfileImage", _args, _returns); err != nil {
log.Printf("RPC call to SetProfileImage API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SetProfileImage(args *Z_SetProfileImageArgs, returns *Z_SetProfileImageReturns) error {
if hook, ok := s.impl.(interface {
SetProfileImage(userID string, data []byte) *model.AppError
}); ok {
returns.A = hook.SetProfileImage(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API SetProfileImage called but not implemented."))
}
return nil
}
type Z_GetEmojiListArgs struct {
A string
B int
C int
}
type Z_GetEmojiListReturns struct {
A []*model.Emoji
B *model.AppError
}
func (g *apiRPCClient) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) {
_args := &Z_GetEmojiListArgs{sortBy, page, perPage}
_returns := &Z_GetEmojiListReturns{}
if err := g.client.Call("Plugin.GetEmojiList", _args, _returns); err != nil {
log.Printf("RPC call to GetEmojiList API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetEmojiList(args *Z_GetEmojiListArgs, returns *Z_GetEmojiListReturns) error {
if hook, ok := s.impl.(interface {
GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetEmojiList(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetEmojiList called but not implemented."))
}
return nil
}
type Z_GetEmojiByNameArgs struct {
A string
}
type Z_GetEmojiByNameReturns struct {
A *model.Emoji
B *model.AppError
}
func (g *apiRPCClient) GetEmojiByName(name string) (*model.Emoji, *model.AppError) {
_args := &Z_GetEmojiByNameArgs{name}
_returns := &Z_GetEmojiByNameReturns{}
if err := g.client.Call("Plugin.GetEmojiByName", _args, _returns); err != nil {
log.Printf("RPC call to GetEmojiByName API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetEmojiByName(args *Z_GetEmojiByNameArgs, returns *Z_GetEmojiByNameReturns) error {
if hook, ok := s.impl.(interface {
GetEmojiByName(name string) (*model.Emoji, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetEmojiByName(args.A)
} else {
return encodableError(fmt.Errorf("API GetEmojiByName called but not implemented."))
}
return nil
}
type Z_GetEmojiArgs struct {
A string
}
type Z_GetEmojiReturns struct {
A *model.Emoji
B *model.AppError
}
func (g *apiRPCClient) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
_args := &Z_GetEmojiArgs{emojiId}
_returns := &Z_GetEmojiReturns{}
if err := g.client.Call("Plugin.GetEmoji", _args, _returns); err != nil {
log.Printf("RPC call to GetEmoji API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetEmoji(args *Z_GetEmojiArgs, returns *Z_GetEmojiReturns) error {
if hook, ok := s.impl.(interface {
GetEmoji(emojiId string) (*model.Emoji, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetEmoji(args.A)
} else {
return encodableError(fmt.Errorf("API GetEmoji called but not implemented."))
}
return nil
}
type Z_CopyFileInfosArgs struct {
A string
B []string
}
type Z_CopyFileInfosReturns struct {
A []string
B *model.AppError
}
func (g *apiRPCClient) CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError) {
_args := &Z_CopyFileInfosArgs{userID, fileIds}
_returns := &Z_CopyFileInfosReturns{}
if err := g.client.Call("Plugin.CopyFileInfos", _args, _returns); err != nil {
log.Printf("RPC call to CopyFileInfos API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CopyFileInfos(args *Z_CopyFileInfosArgs, returns *Z_CopyFileInfosReturns) error {
if hook, ok := s.impl.(interface {
CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError)
}); ok {
returns.A, returns.B = hook.CopyFileInfos(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API CopyFileInfos called but not implemented."))
}
return nil
}
type Z_GetFileInfoArgs struct {
A string
}
type Z_GetFileInfoReturns struct {
A *model.FileInfo
B *model.AppError
}
func (g *apiRPCClient) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) {
_args := &Z_GetFileInfoArgs{fileId}
_returns := &Z_GetFileInfoReturns{}
if err := g.client.Call("Plugin.GetFileInfo", _args, _returns); err != nil {
log.Printf("RPC call to GetFileInfo API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetFileInfo(args *Z_GetFileInfoArgs, returns *Z_GetFileInfoReturns) error {
if hook, ok := s.impl.(interface {
GetFileInfo(fileId string) (*model.FileInfo, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetFileInfo(args.A)
} else {
return encodableError(fmt.Errorf("API GetFileInfo called but not implemented."))
}
return nil
}
type Z_GetFileInfosArgs struct {
A int
B int
C *model.GetFileInfosOptions
}
type Z_GetFileInfosReturns struct {
A []*model.FileInfo
B *model.AppError
}
func (g *apiRPCClient) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
_args := &Z_GetFileInfosArgs{page, perPage, opt}
_returns := &Z_GetFileInfosReturns{}
if err := g.client.Call("Plugin.GetFileInfos", _args, _returns); err != nil {
log.Printf("RPC call to GetFileInfos API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetFileInfos(args *Z_GetFileInfosArgs, returns *Z_GetFileInfosReturns) error {
if hook, ok := s.impl.(interface {
GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetFileInfos(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API GetFileInfos called but not implemented."))
}
return nil
}
type Z_GetFileArgs struct {
A string
}
type Z_GetFileReturns struct {
A []byte
B *model.AppError
}
func (g *apiRPCClient) GetFile(fileId string) ([]byte, *model.AppError) {
_args := &Z_GetFileArgs{fileId}
_returns := &Z_GetFileReturns{}
if err := g.client.Call("Plugin.GetFile", _args, _returns); err != nil {
log.Printf("RPC call to GetFile API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetFile(args *Z_GetFileArgs, returns *Z_GetFileReturns) error {
if hook, ok := s.impl.(interface {
GetFile(fileId string) ([]byte, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetFile(args.A)
} else {
return encodableError(fmt.Errorf("API GetFile called but not implemented."))
}
return nil
}
type Z_GetFileLinkArgs struct {
A string
}
type Z_GetFileLinkReturns struct {
A string
B *model.AppError
}
func (g *apiRPCClient) GetFileLink(fileId string) (string, *model.AppError) {
_args := &Z_GetFileLinkArgs{fileId}
_returns := &Z_GetFileLinkReturns{}
if err := g.client.Call("Plugin.GetFileLink", _args, _returns); err != nil {
log.Printf("RPC call to GetFileLink API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetFileLink(args *Z_GetFileLinkArgs, returns *Z_GetFileLinkReturns) error {
if hook, ok := s.impl.(interface {
GetFileLink(fileId string) (string, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetFileLink(args.A)
} else {
return encodableError(fmt.Errorf("API GetFileLink called but not implemented."))
}
return nil
}
type Z_ReadFileArgs struct {
A string
}
type Z_ReadFileReturns struct {
A []byte
B *model.AppError
}
func (g *apiRPCClient) ReadFile(path string) ([]byte, *model.AppError) {
_args := &Z_ReadFileArgs{path}
_returns := &Z_ReadFileReturns{}
if err := g.client.Call("Plugin.ReadFile", _args, _returns); err != nil {
log.Printf("RPC call to ReadFile API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ReadFile(args *Z_ReadFileArgs, returns *Z_ReadFileReturns) error {
if hook, ok := s.impl.(interface {
ReadFile(path string) ([]byte, *model.AppError)
}); ok {
returns.A, returns.B = hook.ReadFile(args.A)
} else {
return encodableError(fmt.Errorf("API ReadFile called but not implemented."))
}
return nil
}
type Z_GetEmojiImageArgs struct {
A string
}
type Z_GetEmojiImageReturns struct {
A []byte
B string
C *model.AppError
}
func (g *apiRPCClient) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
_args := &Z_GetEmojiImageArgs{emojiId}
_returns := &Z_GetEmojiImageReturns{}
if err := g.client.Call("Plugin.GetEmojiImage", _args, _returns); err != nil {
log.Printf("RPC call to GetEmojiImage API failed: %s", err.Error())
}
return _returns.A, _returns.B, _returns.C
}
func (s *apiRPCServer) GetEmojiImage(args *Z_GetEmojiImageArgs, returns *Z_GetEmojiImageReturns) error {
if hook, ok := s.impl.(interface {
GetEmojiImage(emojiId string) ([]byte, string, *model.AppError)
}); ok {
returns.A, returns.B, returns.C = hook.GetEmojiImage(args.A)
} else {
return encodableError(fmt.Errorf("API GetEmojiImage called but not implemented."))
}
return nil
}
type Z_UploadFileArgs struct {
A []byte
B string
C string
}
type Z_UploadFileReturns struct {
A *model.FileInfo
B *model.AppError
}
func (g *apiRPCClient) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) {
_args := &Z_UploadFileArgs{data, channelId, filename}
_returns := &Z_UploadFileReturns{}
if err := g.client.Call("Plugin.UploadFile", _args, _returns); err != nil {
log.Printf("RPC call to UploadFile API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UploadFile(args *Z_UploadFileArgs, returns *Z_UploadFileReturns) error {
if hook, ok := s.impl.(interface {
UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError)
}); ok {
returns.A, returns.B = hook.UploadFile(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API UploadFile called but not implemented."))
}
return nil
}
type Z_OpenInteractiveDialogArgs struct {
A model.OpenDialogRequest
}
type Z_OpenInteractiveDialogReturns struct {
A *model.AppError
}
func (g *apiRPCClient) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
_args := &Z_OpenInteractiveDialogArgs{dialog}
_returns := &Z_OpenInteractiveDialogReturns{}
if err := g.client.Call("Plugin.OpenInteractiveDialog", _args, _returns); err != nil {
log.Printf("RPC call to OpenInteractiveDialog API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) OpenInteractiveDialog(args *Z_OpenInteractiveDialogArgs, returns *Z_OpenInteractiveDialogReturns) error {
if hook, ok := s.impl.(interface {
OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError
}); ok {
returns.A = hook.OpenInteractiveDialog(args.A)
} else {
return encodableError(fmt.Errorf("API OpenInteractiveDialog called but not implemented."))
}
return nil
}
type Z_GetPluginsArgs struct {
}
type Z_GetPluginsReturns struct {
A []*model.Manifest
B *model.AppError
}
func (g *apiRPCClient) GetPlugins() ([]*model.Manifest, *model.AppError) {
_args := &Z_GetPluginsArgs{}
_returns := &Z_GetPluginsReturns{}
if err := g.client.Call("Plugin.GetPlugins", _args, _returns); err != nil {
log.Printf("RPC call to GetPlugins API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPlugins(args *Z_GetPluginsArgs, returns *Z_GetPluginsReturns) error {
if hook, ok := s.impl.(interface {
GetPlugins() ([]*model.Manifest, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPlugins()
} else {
return encodableError(fmt.Errorf("API GetPlugins called but not implemented."))
}
return nil
}
type Z_EnablePluginArgs struct {
A string
}
type Z_EnablePluginReturns struct {
A *model.AppError
}
func (g *apiRPCClient) EnablePlugin(id string) *model.AppError {
_args := &Z_EnablePluginArgs{id}
_returns := &Z_EnablePluginReturns{}
if err := g.client.Call("Plugin.EnablePlugin", _args, _returns); err != nil {
log.Printf("RPC call to EnablePlugin API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) EnablePlugin(args *Z_EnablePluginArgs, returns *Z_EnablePluginReturns) error {
if hook, ok := s.impl.(interface {
EnablePlugin(id string) *model.AppError
}); ok {
returns.A = hook.EnablePlugin(args.A)
} else {
return encodableError(fmt.Errorf("API EnablePlugin called but not implemented."))
}
return nil
}
type Z_DisablePluginArgs struct {
A string
}
type Z_DisablePluginReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DisablePlugin(id string) *model.AppError {
_args := &Z_DisablePluginArgs{id}
_returns := &Z_DisablePluginReturns{}
if err := g.client.Call("Plugin.DisablePlugin", _args, _returns); err != nil {
log.Printf("RPC call to DisablePlugin API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DisablePlugin(args *Z_DisablePluginArgs, returns *Z_DisablePluginReturns) error {
if hook, ok := s.impl.(interface {
DisablePlugin(id string) *model.AppError
}); ok {
returns.A = hook.DisablePlugin(args.A)
} else {
return encodableError(fmt.Errorf("API DisablePlugin called but not implemented."))
}
return nil
}
type Z_RemovePluginArgs struct {
A string
}
type Z_RemovePluginReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RemovePlugin(id string) *model.AppError {
_args := &Z_RemovePluginArgs{id}
_returns := &Z_RemovePluginReturns{}
if err := g.client.Call("Plugin.RemovePlugin", _args, _returns); err != nil {
log.Printf("RPC call to RemovePlugin API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RemovePlugin(args *Z_RemovePluginArgs, returns *Z_RemovePluginReturns) error {
if hook, ok := s.impl.(interface {
RemovePlugin(id string) *model.AppError
}); ok {
returns.A = hook.RemovePlugin(args.A)
} else {
return encodableError(fmt.Errorf("API RemovePlugin called but not implemented."))
}
return nil
}
type Z_GetPluginStatusArgs struct {
A string
}
type Z_GetPluginStatusReturns struct {
A *model.PluginStatus
B *model.AppError
}
func (g *apiRPCClient) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
_args := &Z_GetPluginStatusArgs{id}
_returns := &Z_GetPluginStatusReturns{}
if err := g.client.Call("Plugin.GetPluginStatus", _args, _returns); err != nil {
log.Printf("RPC call to GetPluginStatus API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_GetPluginStatusReturns) error {
if hook, ok := s.impl.(interface {
GetPluginStatus(id string) (*model.PluginStatus, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetPluginStatus(args.A)
} else {
return encodableError(fmt.Errorf("API GetPluginStatus called but not implemented."))
}
return nil
}
type Z_KVSetArgs struct {
A string
B []byte
}
type Z_KVSetReturns struct {
A *model.AppError
}
func (g *apiRPCClient) KVSet(key string, value []byte) *model.AppError {
_args := &Z_KVSetArgs{key, value}
_returns := &Z_KVSetReturns{}
if err := g.client.Call("Plugin.KVSet", _args, _returns); err != nil {
log.Printf("RPC call to KVSet API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) KVSet(args *Z_KVSetArgs, returns *Z_KVSetReturns) error {
if hook, ok := s.impl.(interface {
KVSet(key string, value []byte) *model.AppError
}); ok {
returns.A = hook.KVSet(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API KVSet called but not implemented."))
}
return nil
}
type Z_KVCompareAndSetArgs struct {
A string
B []byte
C []byte
}
type Z_KVCompareAndSetReturns struct {
A bool
B *model.AppError
}
func (g *apiRPCClient) KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) {
_args := &Z_KVCompareAndSetArgs{key, oldValue, newValue}
_returns := &Z_KVCompareAndSetReturns{}
if err := g.client.Call("Plugin.KVCompareAndSet", _args, _returns); err != nil {
log.Printf("RPC call to KVCompareAndSet API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) KVCompareAndSet(args *Z_KVCompareAndSetArgs, returns *Z_KVCompareAndSetReturns) error {
if hook, ok := s.impl.(interface {
KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError)
}); ok {
returns.A, returns.B = hook.KVCompareAndSet(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API KVCompareAndSet called but not implemented."))
}
return nil
}
type Z_KVCompareAndDeleteArgs struct {
A string
B []byte
}
type Z_KVCompareAndDeleteReturns struct {
A bool
B *model.AppError
}
func (g *apiRPCClient) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) {
_args := &Z_KVCompareAndDeleteArgs{key, oldValue}
_returns := &Z_KVCompareAndDeleteReturns{}
if err := g.client.Call("Plugin.KVCompareAndDelete", _args, _returns); err != nil {
log.Printf("RPC call to KVCompareAndDelete API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) KVCompareAndDelete(args *Z_KVCompareAndDeleteArgs, returns *Z_KVCompareAndDeleteReturns) error {
if hook, ok := s.impl.(interface {
KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError)
}); ok {
returns.A, returns.B = hook.KVCompareAndDelete(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API KVCompareAndDelete called but not implemented."))
}
return nil
}
type Z_KVSetWithOptionsArgs struct {
A string
B []byte
C model.PluginKVSetOptions
}
type Z_KVSetWithOptionsReturns struct {
A bool
B *model.AppError
}
func (g *apiRPCClient) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
_args := &Z_KVSetWithOptionsArgs{key, value, options}
_returns := &Z_KVSetWithOptionsReturns{}
if err := g.client.Call("Plugin.KVSetWithOptions", _args, _returns); err != nil {
log.Printf("RPC call to KVSetWithOptions API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) KVSetWithOptions(args *Z_KVSetWithOptionsArgs, returns *Z_KVSetWithOptionsReturns) error {
if hook, ok := s.impl.(interface {
KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError)
}); ok {
returns.A, returns.B = hook.KVSetWithOptions(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API KVSetWithOptions called but not implemented."))
}
return nil
}
type Z_KVSetWithExpiryArgs struct {
A string
B []byte
C int64
}
type Z_KVSetWithExpiryReturns struct {
A *model.AppError
}
func (g *apiRPCClient) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError {
_args := &Z_KVSetWithExpiryArgs{key, value, expireInSeconds}
_returns := &Z_KVSetWithExpiryReturns{}
if err := g.client.Call("Plugin.KVSetWithExpiry", _args, _returns); err != nil {
log.Printf("RPC call to KVSetWithExpiry API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) KVSetWithExpiry(args *Z_KVSetWithExpiryArgs, returns *Z_KVSetWithExpiryReturns) error {
if hook, ok := s.impl.(interface {
KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError
}); ok {
returns.A = hook.KVSetWithExpiry(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API KVSetWithExpiry called but not implemented."))
}
return nil
}
type Z_KVGetArgs struct {
A string
}
type Z_KVGetReturns struct {
A []byte
B *model.AppError
}
func (g *apiRPCClient) KVGet(key string) ([]byte, *model.AppError) {
_args := &Z_KVGetArgs{key}
_returns := &Z_KVGetReturns{}
if err := g.client.Call("Plugin.KVGet", _args, _returns); err != nil {
log.Printf("RPC call to KVGet API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) KVGet(args *Z_KVGetArgs, returns *Z_KVGetReturns) error {
if hook, ok := s.impl.(interface {
KVGet(key string) ([]byte, *model.AppError)
}); ok {
returns.A, returns.B = hook.KVGet(args.A)
} else {
return encodableError(fmt.Errorf("API KVGet called but not implemented."))
}
return nil
}
type Z_KVDeleteArgs struct {
A string
}
type Z_KVDeleteReturns struct {
A *model.AppError
}
func (g *apiRPCClient) KVDelete(key string) *model.AppError {
_args := &Z_KVDeleteArgs{key}
_returns := &Z_KVDeleteReturns{}
if err := g.client.Call("Plugin.KVDelete", _args, _returns); err != nil {
log.Printf("RPC call to KVDelete API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) KVDelete(args *Z_KVDeleteArgs, returns *Z_KVDeleteReturns) error {
if hook, ok := s.impl.(interface {
KVDelete(key string) *model.AppError
}); ok {
returns.A = hook.KVDelete(args.A)
} else {
return encodableError(fmt.Errorf("API KVDelete called but not implemented."))
}
return nil
}
type Z_KVDeleteAllArgs struct {
}
type Z_KVDeleteAllReturns struct {
A *model.AppError
}
func (g *apiRPCClient) KVDeleteAll() *model.AppError {
_args := &Z_KVDeleteAllArgs{}
_returns := &Z_KVDeleteAllReturns{}
if err := g.client.Call("Plugin.KVDeleteAll", _args, _returns); err != nil {
log.Printf("RPC call to KVDeleteAll API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) KVDeleteAll(args *Z_KVDeleteAllArgs, returns *Z_KVDeleteAllReturns) error {
if hook, ok := s.impl.(interface {
KVDeleteAll() *model.AppError
}); ok {
returns.A = hook.KVDeleteAll()
} else {
return encodableError(fmt.Errorf("API KVDeleteAll called but not implemented."))
}
return nil
}
type Z_KVListArgs struct {
A int
B int
}
type Z_KVListReturns struct {
A []string
B *model.AppError
}
func (g *apiRPCClient) KVList(page, perPage int) ([]string, *model.AppError) {
_args := &Z_KVListArgs{page, perPage}
_returns := &Z_KVListReturns{}
if err := g.client.Call("Plugin.KVList", _args, _returns); err != nil {
log.Printf("RPC call to KVList API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) KVList(args *Z_KVListArgs, returns *Z_KVListReturns) error {
if hook, ok := s.impl.(interface {
KVList(page, perPage int) ([]string, *model.AppError)
}); ok {
returns.A, returns.B = hook.KVList(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API KVList called but not implemented."))
}
return nil
}
type Z_PublishWebSocketEventArgs struct {
A string
B map[string]any
C *model.WebsocketBroadcast
}
type Z_PublishWebSocketEventReturns struct {
}
func (g *apiRPCClient) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
_args := &Z_PublishWebSocketEventArgs{event, payload, broadcast}
_returns := &Z_PublishWebSocketEventReturns{}
if err := g.client.Call("Plugin.PublishWebSocketEvent", _args, _returns); err != nil {
log.Printf("RPC call to PublishWebSocketEvent API failed: %s", err.Error())
}
}
func (s *apiRPCServer) PublishWebSocketEvent(args *Z_PublishWebSocketEventArgs, returns *Z_PublishWebSocketEventReturns) error {
if hook, ok := s.impl.(interface {
PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast)
}); ok {
hook.PublishWebSocketEvent(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API PublishWebSocketEvent called but not implemented."))
}
return nil
}
type Z_HasPermissionToArgs struct {
A string
B *model.Permission
}
type Z_HasPermissionToReturns struct {
A bool
}
func (g *apiRPCClient) HasPermissionTo(userID string, permission *model.Permission) bool {
_args := &Z_HasPermissionToArgs{userID, permission}
_returns := &Z_HasPermissionToReturns{}
if err := g.client.Call("Plugin.HasPermissionTo", _args, _returns); err != nil {
log.Printf("RPC call to HasPermissionTo API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) HasPermissionTo(args *Z_HasPermissionToArgs, returns *Z_HasPermissionToReturns) error {
if hook, ok := s.impl.(interface {
HasPermissionTo(userID string, permission *model.Permission) bool
}); ok {
returns.A = hook.HasPermissionTo(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API HasPermissionTo called but not implemented."))
}
return nil
}
type Z_HasPermissionToTeamArgs struct {
A string
B string
C *model.Permission
}
type Z_HasPermissionToTeamReturns struct {
A bool
}
func (g *apiRPCClient) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool {
_args := &Z_HasPermissionToTeamArgs{userID, teamID, permission}
_returns := &Z_HasPermissionToTeamReturns{}
if err := g.client.Call("Plugin.HasPermissionToTeam", _args, _returns); err != nil {
log.Printf("RPC call to HasPermissionToTeam API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) HasPermissionToTeam(args *Z_HasPermissionToTeamArgs, returns *Z_HasPermissionToTeamReturns) error {
if hook, ok := s.impl.(interface {
HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool
}); ok {
returns.A = hook.HasPermissionToTeam(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API HasPermissionToTeam called but not implemented."))
}
return nil
}
type Z_HasPermissionToChannelArgs struct {
A string
B string
C *model.Permission
}
type Z_HasPermissionToChannelReturns struct {
A bool
}
func (g *apiRPCClient) HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool {
_args := &Z_HasPermissionToChannelArgs{userID, channelId, permission}
_returns := &Z_HasPermissionToChannelReturns{}
if err := g.client.Call("Plugin.HasPermissionToChannel", _args, _returns); err != nil {
log.Printf("RPC call to HasPermissionToChannel API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) HasPermissionToChannel(args *Z_HasPermissionToChannelArgs, returns *Z_HasPermissionToChannelReturns) error {
if hook, ok := s.impl.(interface {
HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool
}); ok {
returns.A = hook.HasPermissionToChannel(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API HasPermissionToChannel called but not implemented."))
}
return nil
}
type Z_RolesGrantPermissionArgs struct {
A []string
B string
}
type Z_RolesGrantPermissionReturns struct {
A bool
}
func (g *apiRPCClient) RolesGrantPermission(roleNames []string, permissionId string) bool {
_args := &Z_RolesGrantPermissionArgs{roleNames, permissionId}
_returns := &Z_RolesGrantPermissionReturns{}
if err := g.client.Call("Plugin.RolesGrantPermission", _args, _returns); err != nil {
log.Printf("RPC call to RolesGrantPermission API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RolesGrantPermission(args *Z_RolesGrantPermissionArgs, returns *Z_RolesGrantPermissionReturns) error {
if hook, ok := s.impl.(interface {
RolesGrantPermission(roleNames []string, permissionId string) bool
}); ok {
returns.A = hook.RolesGrantPermission(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API RolesGrantPermission called but not implemented."))
}
return nil
}
type Z_SendMailArgs struct {
A string
B string
C string
}
type Z_SendMailReturns struct {
A *model.AppError
}
func (g *apiRPCClient) SendMail(to, subject, htmlBody string) *model.AppError {
_args := &Z_SendMailArgs{to, subject, htmlBody}
_returns := &Z_SendMailReturns{}
if err := g.client.Call("Plugin.SendMail", _args, _returns); err != nil {
log.Printf("RPC call to SendMail API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) SendMail(args *Z_SendMailArgs, returns *Z_SendMailReturns) error {
if hook, ok := s.impl.(interface {
SendMail(to, subject, htmlBody string) *model.AppError
}); ok {
returns.A = hook.SendMail(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API SendMail called but not implemented."))
}
return nil
}
type Z_CreateBotArgs struct {
A *model.Bot
}
type Z_CreateBotReturns struct {
A *model.Bot
B *model.AppError
}
func (g *apiRPCClient) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
_args := &Z_CreateBotArgs{bot}
_returns := &Z_CreateBotReturns{}
if err := g.client.Call("Plugin.CreateBot", _args, _returns); err != nil {
log.Printf("RPC call to CreateBot API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateBot(args *Z_CreateBotArgs, returns *Z_CreateBotReturns) error {
if hook, ok := s.impl.(interface {
CreateBot(bot *model.Bot) (*model.Bot, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateBot(args.A)
} else {
return encodableError(fmt.Errorf("API CreateBot called but not implemented."))
}
return nil
}
type Z_PatchBotArgs struct {
A string
B *model.BotPatch
}
type Z_PatchBotReturns struct {
A *model.Bot
B *model.AppError
}
func (g *apiRPCClient) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
_args := &Z_PatchBotArgs{botUserId, botPatch}
_returns := &Z_PatchBotReturns{}
if err := g.client.Call("Plugin.PatchBot", _args, _returns); err != nil {
log.Printf("RPC call to PatchBot API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) PatchBot(args *Z_PatchBotArgs, returns *Z_PatchBotReturns) error {
if hook, ok := s.impl.(interface {
PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError)
}); ok {
returns.A, returns.B = hook.PatchBot(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API PatchBot called but not implemented."))
}
return nil
}
type Z_GetBotArgs struct {
A string
B bool
}
type Z_GetBotReturns struct {
A *model.Bot
B *model.AppError
}
func (g *apiRPCClient) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
_args := &Z_GetBotArgs{botUserId, includeDeleted}
_returns := &Z_GetBotReturns{}
if err := g.client.Call("Plugin.GetBot", _args, _returns); err != nil {
log.Printf("RPC call to GetBot API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetBot(args *Z_GetBotArgs, returns *Z_GetBotReturns) error {
if hook, ok := s.impl.(interface {
GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetBot(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API GetBot called but not implemented."))
}
return nil
}
type Z_GetBotsArgs struct {
A *model.BotGetOptions
}
type Z_GetBotsReturns struct {
A []*model.Bot
B *model.AppError
}
func (g *apiRPCClient) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) {
_args := &Z_GetBotsArgs{options}
_returns := &Z_GetBotsReturns{}
if err := g.client.Call("Plugin.GetBots", _args, _returns); err != nil {
log.Printf("RPC call to GetBots API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetBots(args *Z_GetBotsArgs, returns *Z_GetBotsReturns) error {
if hook, ok := s.impl.(interface {
GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetBots(args.A)
} else {
return encodableError(fmt.Errorf("API GetBots called but not implemented."))
}
return nil
}
type Z_UpdateBotActiveArgs struct {
A string
B bool
}
type Z_UpdateBotActiveReturns struct {
A *model.Bot
B *model.AppError
}
func (g *apiRPCClient) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) {
_args := &Z_UpdateBotActiveArgs{botUserId, active}
_returns := &Z_UpdateBotActiveReturns{}
if err := g.client.Call("Plugin.UpdateBotActive", _args, _returns); err != nil {
log.Printf("RPC call to UpdateBotActive API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateBotActive(args *Z_UpdateBotActiveArgs, returns *Z_UpdateBotActiveReturns) error {
if hook, ok := s.impl.(interface {
UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateBotActive(args.A, args.B)
} else {
return encodableError(fmt.Errorf("API UpdateBotActive called but not implemented."))
}
return nil
}
type Z_PermanentDeleteBotArgs struct {
A string
}
type Z_PermanentDeleteBotReturns struct {
A *model.AppError
}
func (g *apiRPCClient) PermanentDeleteBot(botUserId string) *model.AppError {
_args := &Z_PermanentDeleteBotArgs{botUserId}
_returns := &Z_PermanentDeleteBotReturns{}
if err := g.client.Call("Plugin.PermanentDeleteBot", _args, _returns); err != nil {
log.Printf("RPC call to PermanentDeleteBot API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) PermanentDeleteBot(args *Z_PermanentDeleteBotArgs, returns *Z_PermanentDeleteBotReturns) error {
if hook, ok := s.impl.(interface {
PermanentDeleteBot(botUserId string) *model.AppError
}); ok {
returns.A = hook.PermanentDeleteBot(args.A)
} else {
return encodableError(fmt.Errorf("API PermanentDeleteBot called but not implemented."))
}
return nil
}
type Z_PublishUserTypingArgs struct {
A string
B string
C string
}
type Z_PublishUserTypingReturns struct {
A *model.AppError
}
func (g *apiRPCClient) PublishUserTyping(userID, channelId, parentId string) *model.AppError {
_args := &Z_PublishUserTypingArgs{userID, channelId, parentId}
_returns := &Z_PublishUserTypingReturns{}
if err := g.client.Call("Plugin.PublishUserTyping", _args, _returns); err != nil {
log.Printf("RPC call to PublishUserTyping API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) PublishUserTyping(args *Z_PublishUserTypingArgs, returns *Z_PublishUserTypingReturns) error {
if hook, ok := s.impl.(interface {
PublishUserTyping(userID, channelId, parentId string) *model.AppError
}); ok {
returns.A = hook.PublishUserTyping(args.A, args.B, args.C)
} else {
return encodableError(fmt.Errorf("API PublishUserTyping called but not implemented."))
}
return nil
}
type Z_CreateCommandArgs struct {
A *model.Command
}
type Z_CreateCommandReturns struct {
A *model.Command
B error
}
func (g *apiRPCClient) CreateCommand(cmd *model.Command) (*model.Command, error) {
_args := &Z_CreateCommandArgs{cmd}
_returns := &Z_CreateCommandReturns{}
if err := g.client.Call("Plugin.CreateCommand", _args, _returns); err != nil {
log.Printf("RPC call to CreateCommand API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateCommand(args *Z_CreateCommandArgs, returns *Z_CreateCommandReturns) error {
if hook, ok := s.impl.(interface {
CreateCommand(cmd *model.Command) (*model.Command, error)
}); ok {
returns.A, returns.B = hook.CreateCommand(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API CreateCommand called but not implemented."))
}
return nil
}
type Z_ListCommandsArgs struct {
A string
}
type Z_ListCommandsReturns struct {
A []*model.Command
B error
}
func (g *apiRPCClient) ListCommands(teamID string) ([]*model.Command, error) {
_args := &Z_ListCommandsArgs{teamID}
_returns := &Z_ListCommandsReturns{}
if err := g.client.Call("Plugin.ListCommands", _args, _returns); err != nil {
log.Printf("RPC call to ListCommands API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ListCommands(args *Z_ListCommandsArgs, returns *Z_ListCommandsReturns) error {
if hook, ok := s.impl.(interface {
ListCommands(teamID string) ([]*model.Command, error)
}); ok {
returns.A, returns.B = hook.ListCommands(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API ListCommands called but not implemented."))
}
return nil
}
type Z_ListCustomCommandsArgs struct {
A string
}
type Z_ListCustomCommandsReturns struct {
A []*model.Command
B error
}
func (g *apiRPCClient) ListCustomCommands(teamID string) ([]*model.Command, error) {
_args := &Z_ListCustomCommandsArgs{teamID}
_returns := &Z_ListCustomCommandsReturns{}
if err := g.client.Call("Plugin.ListCustomCommands", _args, _returns); err != nil {
log.Printf("RPC call to ListCustomCommands API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ListCustomCommands(args *Z_ListCustomCommandsArgs, returns *Z_ListCustomCommandsReturns) error {
if hook, ok := s.impl.(interface {
ListCustomCommands(teamID string) ([]*model.Command, error)
}); ok {
returns.A, returns.B = hook.ListCustomCommands(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API ListCustomCommands called but not implemented."))
}
return nil
}
type Z_ListPluginCommandsArgs struct {
A string
}
type Z_ListPluginCommandsReturns struct {
A []*model.Command
B error
}
func (g *apiRPCClient) ListPluginCommands(teamID string) ([]*model.Command, error) {
_args := &Z_ListPluginCommandsArgs{teamID}
_returns := &Z_ListPluginCommandsReturns{}
if err := g.client.Call("Plugin.ListPluginCommands", _args, _returns); err != nil {
log.Printf("RPC call to ListPluginCommands API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ListPluginCommands(args *Z_ListPluginCommandsArgs, returns *Z_ListPluginCommandsReturns) error {
if hook, ok := s.impl.(interface {
ListPluginCommands(teamID string) ([]*model.Command, error)
}); ok {
returns.A, returns.B = hook.ListPluginCommands(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API ListPluginCommands called but not implemented."))
}
return nil
}
type Z_ListBuiltInCommandsArgs struct {
}
type Z_ListBuiltInCommandsReturns struct {
A []*model.Command
B error
}
func (g *apiRPCClient) ListBuiltInCommands() ([]*model.Command, error) {
_args := &Z_ListBuiltInCommandsArgs{}
_returns := &Z_ListBuiltInCommandsReturns{}
if err := g.client.Call("Plugin.ListBuiltInCommands", _args, _returns); err != nil {
log.Printf("RPC call to ListBuiltInCommands API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) ListBuiltInCommands(args *Z_ListBuiltInCommandsArgs, returns *Z_ListBuiltInCommandsReturns) error {
if hook, ok := s.impl.(interface {
ListBuiltInCommands() ([]*model.Command, error)
}); ok {
returns.A, returns.B = hook.ListBuiltInCommands()
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API ListBuiltInCommands called but not implemented."))
}
return nil
}
type Z_GetCommandArgs struct {
A string
}
type Z_GetCommandReturns struct {
A *model.Command
B error
}
func (g *apiRPCClient) GetCommand(commandID string) (*model.Command, error) {
_args := &Z_GetCommandArgs{commandID}
_returns := &Z_GetCommandReturns{}
if err := g.client.Call("Plugin.GetCommand", _args, _returns); err != nil {
log.Printf("RPC call to GetCommand API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetCommand(args *Z_GetCommandArgs, returns *Z_GetCommandReturns) error {
if hook, ok := s.impl.(interface {
GetCommand(commandID string) (*model.Command, error)
}); ok {
returns.A, returns.B = hook.GetCommand(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API GetCommand called but not implemented."))
}
return nil
}
type Z_UpdateCommandArgs struct {
A string
B *model.Command
}
type Z_UpdateCommandReturns struct {
A *model.Command
B error
}
func (g *apiRPCClient) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) {
_args := &Z_UpdateCommandArgs{commandID, updatedCmd}
_returns := &Z_UpdateCommandReturns{}
if err := g.client.Call("Plugin.UpdateCommand", _args, _returns); err != nil {
log.Printf("RPC call to UpdateCommand API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateCommand(args *Z_UpdateCommandArgs, returns *Z_UpdateCommandReturns) error {
if hook, ok := s.impl.(interface {
UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error)
}); ok {
returns.A, returns.B = hook.UpdateCommand(args.A, args.B)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API UpdateCommand called but not implemented."))
}
return nil
}
type Z_DeleteCommandArgs struct {
A string
}
type Z_DeleteCommandReturns struct {
A error
}
func (g *apiRPCClient) DeleteCommand(commandID string) error {
_args := &Z_DeleteCommandArgs{commandID}
_returns := &Z_DeleteCommandReturns{}
if err := g.client.Call("Plugin.DeleteCommand", _args, _returns); err != nil {
log.Printf("RPC call to DeleteCommand API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteCommand(args *Z_DeleteCommandArgs, returns *Z_DeleteCommandReturns) error {
if hook, ok := s.impl.(interface {
DeleteCommand(commandID string) error
}); ok {
returns.A = hook.DeleteCommand(args.A)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("API DeleteCommand called but not implemented."))
}
return nil
}
type Z_CreateOAuthAppArgs struct {
A *model.OAuthApp
}
type Z_CreateOAuthAppReturns struct {
A *model.OAuthApp
B *model.AppError
}
func (g *apiRPCClient) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
_args := &Z_CreateOAuthAppArgs{app}
_returns := &Z_CreateOAuthAppReturns{}
if err := g.client.Call("Plugin.CreateOAuthApp", _args, _returns); err != nil {
log.Printf("RPC call to CreateOAuthApp API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateOAuthApp(args *Z_CreateOAuthAppArgs, returns *Z_CreateOAuthAppReturns) error {
if hook, ok := s.impl.(interface {
CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError)
}); ok {
returns.A, returns.B = hook.CreateOAuthApp(args.A)
} else {
return encodableError(fmt.Errorf("API CreateOAuthApp called but not implemented."))
}
return nil
}
type Z_GetOAuthAppArgs struct {
A string
}
type Z_GetOAuthAppReturns struct {
A *model.OAuthApp
B *model.AppError
}
func (g *apiRPCClient) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
_args := &Z_GetOAuthAppArgs{appID}
_returns := &Z_GetOAuthAppReturns{}
if err := g.client.Call("Plugin.GetOAuthApp", _args, _returns); err != nil {
log.Printf("RPC call to GetOAuthApp API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetOAuthApp(args *Z_GetOAuthAppArgs, returns *Z_GetOAuthAppReturns) error {
if hook, ok := s.impl.(interface {
GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError)
}); ok {
returns.A, returns.B = hook.GetOAuthApp(args.A)
} else {
return encodableError(fmt.Errorf("API GetOAuthApp called but not implemented."))
}
return nil
}
type Z_UpdateOAuthAppArgs struct {
A *model.OAuthApp
}
type Z_UpdateOAuthAppReturns struct {
A *model.OAuthApp
B *model.AppError
}
func (g *apiRPCClient) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
_args := &Z_UpdateOAuthAppArgs{app}
_returns := &Z_UpdateOAuthAppReturns{}
if err := g.client.Call("Plugin.UpdateOAuthApp", _args, _returns); err != nil {
log.Printf("RPC call to UpdateOAuthApp API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) UpdateOAuthApp(args *Z_UpdateOAuthAppArgs, returns *Z_UpdateOAuthAppReturns) error {
if hook, ok := s.impl.(interface {
UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError)
}); ok {
returns.A, returns.B = hook.UpdateOAuthApp(args.A)
} else {
return encodableError(fmt.Errorf("API UpdateOAuthApp called but not implemented."))
}
return nil
}
type Z_DeleteOAuthAppArgs struct {
A string
}
type Z_DeleteOAuthAppReturns struct {
A *model.AppError
}
func (g *apiRPCClient) DeleteOAuthApp(appID string) *model.AppError {
_args := &Z_DeleteOAuthAppArgs{appID}
_returns := &Z_DeleteOAuthAppReturns{}
if err := g.client.Call("Plugin.DeleteOAuthApp", _args, _returns); err != nil {
log.Printf("RPC call to DeleteOAuthApp API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) DeleteOAuthApp(args *Z_DeleteOAuthAppArgs, returns *Z_DeleteOAuthAppReturns) error {
if hook, ok := s.impl.(interface {
DeleteOAuthApp(appID string) *model.AppError
}); ok {
returns.A = hook.DeleteOAuthApp(args.A)
} else {
return encodableError(fmt.Errorf("API DeleteOAuthApp called but not implemented."))
}
return nil
}
type Z_PublishPluginClusterEventArgs struct {
A model.PluginClusterEvent
B model.PluginClusterEventSendOptions
}
type Z_PublishPluginClusterEventReturns struct {
A error
}
func (g *apiRPCClient) PublishPluginClusterEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error {
_args := &Z_PublishPluginClusterEventArgs{ev, opts}
_returns := &Z_PublishPluginClusterEventReturns{}
if err := g.client.Call("Plugin.PublishPluginClusterEvent", _args, _returns); err != nil {
log.Printf("RPC call to PublishPluginClusterEvent API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) PublishPluginClusterEvent(args *Z_PublishPluginClusterEventArgs, returns *Z_PublishPluginClusterEventReturns) error {
if hook, ok := s.impl.(interface {
PublishPluginClusterEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error
}); ok {
returns.A = hook.PublishPluginClusterEvent(args.A, args.B)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("API PublishPluginClusterEvent called but not implemented."))
}
return nil
}
type Z_RequestTrialLicenseArgs struct {
A string
B int
C bool
D bool
}
type Z_RequestTrialLicenseReturns struct {
A *model.AppError
}
func (g *apiRPCClient) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
_args := &Z_RequestTrialLicenseArgs{requesterID, users, termsAccepted, receiveEmailsAccepted}
_returns := &Z_RequestTrialLicenseReturns{}
if err := g.client.Call("Plugin.RequestTrialLicense", _args, _returns); err != nil {
log.Printf("RPC call to RequestTrialLicense API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RequestTrialLicense(args *Z_RequestTrialLicenseArgs, returns *Z_RequestTrialLicenseReturns) error {
if hook, ok := s.impl.(interface {
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
}); ok {
returns.A = hook.RequestTrialLicense(args.A, args.B, args.C, args.D)
} else {
return encodableError(fmt.Errorf("API RequestTrialLicense called but not implemented."))
}
return nil
}
type Z_GetCloudLimitsArgs struct {
}
type Z_GetCloudLimitsReturns struct {
A *model.ProductLimits
B error
}
func (g *apiRPCClient) GetCloudLimits() (*model.ProductLimits, error) {
_args := &Z_GetCloudLimitsArgs{}
_returns := &Z_GetCloudLimitsReturns{}
if err := g.client.Call("Plugin.GetCloudLimits", _args, _returns); err != nil {
log.Printf("RPC call to GetCloudLimits API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetCloudLimits(args *Z_GetCloudLimitsArgs, returns *Z_GetCloudLimitsReturns) error {
if hook, ok := s.impl.(interface {
GetCloudLimits() (*model.ProductLimits, error)
}); ok {
returns.A, returns.B = hook.GetCloudLimits()
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API GetCloudLimits called but not implemented."))
}
return nil
}
type Z_EnsureBotUserArgs struct {
A *model.Bot
}
type Z_EnsureBotUserReturns struct {
A string
B error
}
func (g *apiRPCClient) EnsureBotUser(bot *model.Bot) (string, error) {
_args := &Z_EnsureBotUserArgs{bot}
_returns := &Z_EnsureBotUserReturns{}
if err := g.client.Call("Plugin.EnsureBotUser", _args, _returns); err != nil {
log.Printf("RPC call to EnsureBotUser API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) EnsureBotUser(args *Z_EnsureBotUserArgs, returns *Z_EnsureBotUserReturns) error {
if hook, ok := s.impl.(interface {
EnsureBotUser(bot *model.Bot) (string, error)
}); ok {
returns.A, returns.B = hook.EnsureBotUser(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API EnsureBotUser called but not implemented."))
}
return nil
}
type Z_RegisterCollectionAndTopicArgs struct {
A string
B string
}
type Z_RegisterCollectionAndTopicReturns struct {
A error
}
func (g *apiRPCClient) RegisterCollectionAndTopic(collectionType, topicType string) error {
_args := &Z_RegisterCollectionAndTopicArgs{collectionType, topicType}
_returns := &Z_RegisterCollectionAndTopicReturns{}
if err := g.client.Call("Plugin.RegisterCollectionAndTopic", _args, _returns); err != nil {
log.Printf("RPC call to RegisterCollectionAndTopic API failed: %s", err.Error())
}
return _returns.A
}
func (s *apiRPCServer) RegisterCollectionAndTopic(args *Z_RegisterCollectionAndTopicArgs, returns *Z_RegisterCollectionAndTopicReturns) error {
if hook, ok := s.impl.(interface {
RegisterCollectionAndTopic(collectionType, topicType string) error
}); ok {
returns.A = hook.RegisterCollectionAndTopic(args.A, args.B)
returns.A = encodableError(returns.A)
} else {
return encodableError(fmt.Errorf("API RegisterCollectionAndTopic called but not implemented."))
}
return nil
}
type Z_CreateUploadSessionArgs struct {
A *model.UploadSession
}
type Z_CreateUploadSessionReturns struct {
A *model.UploadSession
B error
}
func (g *apiRPCClient) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
_args := &Z_CreateUploadSessionArgs{us}
_returns := &Z_CreateUploadSessionReturns{}
if err := g.client.Call("Plugin.CreateUploadSession", _args, _returns); err != nil {
log.Printf("RPC call to CreateUploadSession API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CreateUploadSession(args *Z_CreateUploadSessionArgs, returns *Z_CreateUploadSessionReturns) error {
if hook, ok := s.impl.(interface {
CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error)
}); ok {
returns.A, returns.B = hook.CreateUploadSession(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API CreateUploadSession called but not implemented."))
}
return nil
}
type Z_GetUploadSessionArgs struct {
A string
}
type Z_GetUploadSessionReturns struct {
A *model.UploadSession
B error
}
func (g *apiRPCClient) GetUploadSession(uploadID string) (*model.UploadSession, error) {
_args := &Z_GetUploadSessionArgs{uploadID}
_returns := &Z_GetUploadSessionReturns{}
if err := g.client.Call("Plugin.GetUploadSession", _args, _returns); err != nil {
log.Printf("RPC call to GetUploadSession API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetUploadSession(args *Z_GetUploadSessionArgs, returns *Z_GetUploadSessionReturns) error {
if hook, ok := s.impl.(interface {
GetUploadSession(uploadID string) (*model.UploadSession, error)
}); ok {
returns.A, returns.B = hook.GetUploadSession(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API GetUploadSession called but not implemented."))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"database/sql/driver"
"log"
"net/rpc"
)
// dbRPCClient contains the client-side logic to handle the RPC communication
// with the server. It's API is hand-written because we do not expect
// new methods to be added very frequently.
type dbRPCClient struct {
client *rpc.Client
}
// dbRPCServer is the server-side component which is responsible for calling
// the driver methods and properly encoding the responses back to the RPC client.
type dbRPCServer struct {
dbImpl Driver
}
var _ Driver = &dbRPCClient{}
type Z_DbStrErrReturn struct {
A string
B error
}
type Z_DbErrReturn struct {
A error
}
type Z_DbInt64ErrReturn struct {
A int64
B error
}
type Z_DbBoolReturn struct {
A bool
}
func (db *dbRPCClient) Conn(isMaster bool) (string, error) {
ret := &Z_DbStrErrReturn{}
err := db.client.Call("Plugin.Conn", isMaster, ret)
if err != nil {
log.Printf("error during Plugin.Conn: %v", err)
}
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) Conn(isMaster bool, ret *Z_DbStrErrReturn) error {
ret.A, ret.B = db.dbImpl.Conn(isMaster)
ret.B = encodableError(ret.B)
return nil
}
func (db *dbRPCClient) ConnPing(connID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.ConnPing", connID, ret)
if err != nil {
log.Printf("error during Plugin.ConnPing: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) ConnPing(connID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.ConnPing(connID)
ret.A = encodableError(ret.A)
return nil
}
func (db *dbRPCClient) ConnClose(connID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.ConnClose", connID, ret)
if err != nil {
log.Printf("error during Plugin.ConnClose: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) ConnClose(connID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.ConnClose(connID)
ret.A = encodableError(ret.A)
return nil
}
type Z_DbTxArgs struct {
A string
B driver.TxOptions
}
func (db *dbRPCClient) Tx(connID string, opts driver.TxOptions) (string, error) {
args := &Z_DbTxArgs{
A: connID,
B: opts,
}
ret := &Z_DbStrErrReturn{}
err := db.client.Call("Plugin.Tx", args, ret)
if err != nil {
log.Printf("error during Plugin.Tx: %v", err)
}
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) Tx(args *Z_DbTxArgs, ret *Z_DbStrErrReturn) error {
ret.A, ret.B = db.dbImpl.Tx(args.A, args.B)
ret.B = encodableError(ret.B)
return nil
}
func (db *dbRPCClient) TxCommit(txID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.TxCommit", txID, ret)
if err != nil {
log.Printf("error during Plugin.TxCommit: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) TxCommit(txID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.TxCommit(txID)
ret.A = encodableError(ret.A)
return nil
}
func (db *dbRPCClient) TxRollback(txID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.TxRollback", txID, ret)
if err != nil {
log.Printf("error during Plugin.TxRollback: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) TxRollback(txID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.TxRollback(txID)
ret.A = encodableError(ret.A)
return nil
}
type Z_DbStmtArgs struct {
A string
B string
}
func (db *dbRPCClient) Stmt(connID, q string) (string, error) {
args := &Z_DbStmtArgs{
A: connID,
B: q,
}
ret := &Z_DbStrErrReturn{}
err := db.client.Call("Plugin.Stmt", args, ret)
if err != nil {
log.Printf("error during Plugin.Stmt: %v", err)
}
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) Stmt(args *Z_DbStmtArgs, ret *Z_DbStrErrReturn) error {
ret.A, ret.B = db.dbImpl.Stmt(args.A, args.B)
ret.B = encodableError(ret.B)
return nil
}
func (db *dbRPCClient) StmtClose(stID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.StmtClose", stID, ret)
if err != nil {
log.Printf("error during Plugin.StmtClose: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) StmtClose(stID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.StmtClose(stID)
ret.A = encodableError(ret.A)
return nil
}
type Z_DbIntReturn struct {
A int
}
func (db *dbRPCClient) StmtNumInput(stID string) int {
ret := &Z_DbIntReturn{}
err := db.client.Call("Plugin.StmtNumInput", stID, ret)
if err != nil {
log.Printf("error during Plugin.StmtNumInput: %v", err)
}
return ret.A
}
func (db *dbRPCServer) StmtNumInput(stID string, ret *Z_DbIntReturn) error {
ret.A = db.dbImpl.StmtNumInput(stID)
return nil
}
type Z_DbStmtQueryArgs struct {
A string
B []driver.NamedValue
}
func (db *dbRPCClient) StmtQuery(stID string, argVals []driver.NamedValue) (string, error) {
args := &Z_DbStmtQueryArgs{
A: stID,
B: argVals,
}
ret := &Z_DbStrErrReturn{}
err := db.client.Call("Plugin.StmtQuery", args, ret)
if err != nil {
log.Printf("error during Plugin.StmtQuery: %v", err)
}
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) StmtQuery(args *Z_DbStmtQueryArgs, ret *Z_DbStrErrReturn) error {
ret.A, ret.B = db.dbImpl.StmtQuery(args.A, args.B)
ret.B = encodableError(ret.B)
return nil
}
func (db *dbRPCClient) StmtExec(stID string, argVals []driver.NamedValue) (ResultContainer, error) {
args := &Z_DbStmtQueryArgs{
A: stID,
B: argVals,
}
ret := &Z_DbResultContErrReturn{}
err := db.client.Call("Plugin.StmtExec", args, ret)
if err != nil {
log.Printf("error during Plugin.StmtExec: %v", err)
}
ret.A.LastIDError = decodableError(ret.A.LastIDError)
ret.A.RowsAffectedError = decodableError(ret.A.RowsAffectedError)
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) StmtExec(args *Z_DbStmtQueryArgs, ret *Z_DbResultContErrReturn) error {
ret.A, ret.B = db.dbImpl.StmtExec(args.A, args.B)
ret.A.LastIDError = encodableError(ret.A.LastIDError)
ret.A.RowsAffectedError = encodableError(ret.A.RowsAffectedError)
ret.B = encodableError(ret.B)
return nil
}
type Z_DbConnArgs struct {
A string
B string
C []driver.NamedValue
}
func (db *dbRPCClient) ConnQuery(connID, q string, argVals []driver.NamedValue) (string, error) {
args := &Z_DbConnArgs{
A: connID,
B: q,
C: argVals,
}
ret := &Z_DbStrErrReturn{}
err := db.client.Call("Plugin.ConnQuery", args, ret)
if err != nil {
log.Printf("error during Plugin.ConnQuery: %v", err)
}
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) ConnQuery(args *Z_DbConnArgs, ret *Z_DbStrErrReturn) error {
ret.A, ret.B = db.dbImpl.ConnQuery(args.A, args.B, args.C)
ret.B = encodableError(ret.B)
return nil
}
type Z_DbResultContErrReturn struct {
A ResultContainer
B error
}
func (db *dbRPCClient) ConnExec(connID, q string, argVals []driver.NamedValue) (ResultContainer, error) {
args := &Z_DbConnArgs{
A: connID,
B: q,
C: argVals,
}
ret := &Z_DbResultContErrReturn{}
err := db.client.Call("Plugin.ConnExec", args, ret)
if err != nil {
log.Printf("error during Plugin.ConnExec: %v", err)
}
ret.A.LastIDError = decodableError(ret.A.LastIDError)
ret.A.RowsAffectedError = decodableError(ret.A.RowsAffectedError)
ret.B = decodableError(ret.B)
return ret.A, ret.B
}
func (db *dbRPCServer) ConnExec(args *Z_DbConnArgs, ret *Z_DbResultContErrReturn) error {
ret.A, ret.B = db.dbImpl.ConnExec(args.A, args.B, args.C)
ret.A.LastIDError = encodableError(ret.A.LastIDError)
ret.A.RowsAffectedError = encodableError(ret.A.RowsAffectedError)
ret.B = encodableError(ret.B)
return nil
}
type Z_DbStrSliceReturn struct {
A []string
}
func (db *dbRPCClient) RowsColumns(rowsID string) []string {
ret := &Z_DbStrSliceReturn{}
err := db.client.Call("Plugin.RowsColumns", rowsID, ret)
if err != nil {
log.Printf("error during Plugin.RowsColumns: %v", err)
}
return ret.A
}
func (db *dbRPCServer) RowsColumns(rowsID string, ret *Z_DbStrSliceReturn) error {
ret.A = db.dbImpl.RowsColumns(rowsID)
return nil
}
func (db *dbRPCClient) RowsClose(resID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.RowsClose", resID, ret)
if err != nil {
log.Printf("error during Plugin.RowsClose: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) RowsClose(resID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.RowsClose(resID)
ret.A = encodableError(ret.A)
return nil
}
type Z_DbRowScanReturn struct {
A error
B []driver.Value
}
type Z_DbRowScanArg struct {
A string
B []driver.Value
}
func (db *dbRPCClient) RowsNext(rowsID string, dest []driver.Value) error {
args := &Z_DbRowScanArg{
A: rowsID,
B: dest,
}
ret := &Z_DbRowScanReturn{}
err := db.client.Call("Plugin.RowsNext", args, ret)
if err != nil {
log.Printf("error during Plugin.RowsNext: %v", err)
}
ret.A = decodableError(ret.A)
copy(dest, ret.B)
return ret.A
}
func (db *dbRPCServer) RowsNext(args *Z_DbRowScanArg, ret *Z_DbRowScanReturn) error {
ret.A = db.dbImpl.RowsNext(args.A, args.B)
ret.A = encodableError(ret.A)
// Trick to populate the dest slice. RPC doesn't have a semantic to populate
// pointer type args. So the only way to pass values is via args, and only way
// to return values is via the return struct.
ret.B = args.B
return nil
}
func (db *dbRPCClient) RowsHasNextResultSet(rowsID string) bool {
ret := &Z_DbBoolReturn{}
err := db.client.Call("Plugin.RowsHasNextResultSet", rowsID, ret)
if err != nil {
log.Printf("error during Plugin.RowsHasNextResultSet: %v", err)
}
return ret.A
}
func (db *dbRPCServer) RowsHasNextResultSet(rowsID string, ret *Z_DbBoolReturn) error {
ret.A = db.dbImpl.RowsHasNextResultSet(rowsID)
return nil
}
func (db *dbRPCClient) RowsNextResultSet(rowsID string) error {
ret := &Z_DbErrReturn{}
err := db.client.Call("Plugin.RowsNextResultSet", rowsID, ret)
if err != nil {
log.Printf("error during Plugin.RowsNextResultSet: %v", err)
}
ret.A = decodableError(ret.A)
return ret.A
}
func (db *dbRPCServer) RowsNextResultSet(rowsID string, ret *Z_DbErrReturn) error {
ret.A = db.dbImpl.RowsNextResultSet(rowsID)
ret.A = encodableError(ret.A)
return nil
}
type Z_DbRowsColumnArg struct {
A string
B int
}
func (db *dbRPCClient) RowsColumnTypeDatabaseTypeName(rowsID string, index int) string {
args := &Z_DbRowsColumnArg{
A: rowsID,
B: index,
}
var ret string
err := db.client.Call("Plugin.RowsColumnTypeDatabaseTypeName", args, &ret)
if err != nil {
log.Printf("error during Plugin.RowsColumnTypeDatabaseTypeName: %v", err)
}
return ret
}
func (db *dbRPCServer) RowsColumnTypeDatabaseTypeName(args *Z_DbRowsColumnArg, ret *string) error {
*ret = db.dbImpl.RowsColumnTypeDatabaseTypeName(args.A, args.B)
return nil
}
type Z_DbRowsColumnTypePrecisionScaleReturn struct {
A int64
B int64
C bool
}
func (db *dbRPCClient) RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool) {
args := &Z_DbRowsColumnArg{
A: rowsID,
B: index,
}
ret := &Z_DbRowsColumnTypePrecisionScaleReturn{}
err := db.client.Call("Plugin.RowsColumnTypePrecisionScale", args, ret)
if err != nil {
log.Printf("error during Plugin.RowsColumnTypePrecisionScale: %v", err)
}
return ret.A, ret.B, ret.C
}
func (db *dbRPCServer) RowsColumnTypePrecisionScale(args *Z_DbRowsColumnArg, ret *Z_DbRowsColumnTypePrecisionScaleReturn) error {
ret.A, ret.B, ret.C = db.dbImpl.RowsColumnTypePrecisionScale(args.A, args.B)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"bytes"
"fmt"
"hash/fnv"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrNotFound = errors.New("Item not found")
type apiImplCreatorFunc func(*model.Manifest) API
// registeredPlugin stores the state for a given plugin that has been activated
// or attempted to be activated this server run.
//
// If an installed plugin is missing from the env.registeredPlugins map, then the
// plugin is configured as disabled and has not been activated during this server run.
type registeredPlugin struct {
BundleInfo *model.BundleInfo
State int
Error string
supervisor *supervisor
}
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
type PrepackagedPlugin struct {
Path string
IconData string
Manifest *model.Manifest
Signature []byte
}
// Environment represents the execution environment of active plugins.
//
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
// of active plugins.
type Environment struct {
registeredPlugins sync.Map
pluginHealthCheckJob *PluginHealthCheckJob
logger *mlog.Logger
metrics einterfaces.MetricsInterface
newAPIImpl apiImplCreatorFunc
dbDriver Driver
pluginDir string
webappPluginDir string
patchReactDOM bool
prepackagedPlugins []*PrepackagedPlugin
prepackagedPluginsLock sync.RWMutex
}
func NewEnvironment(
newAPIImpl apiImplCreatorFunc,
dbDriver Driver,
pluginDir string,
webappPluginDir string,
patchReactDOM bool,
logger *mlog.Logger,
metrics einterfaces.MetricsInterface,
) (*Environment, error) {
return &Environment{
logger: logger,
metrics: metrics,
newAPIImpl: newAPIImpl,
dbDriver: dbDriver,
pluginDir: pluginDir,
webappPluginDir: webappPluginDir,
patchReactDOM: patchReactDOM,
}, nil
}
// Performs a full scan of the given path.
//
// This function will return info for all subdirectories that appear to be plugins (i.e. all
// subdirectories containing plugin manifest files, regardless of whether they could actually be
// parsed).
//
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
var ret []*model.BundleInfo
for _, file := range files {
if !file.IsDir() || file.Name()[0] == '.' {
continue
}
info := model.BundleInfoForPath(filepath.Join(path, file.Name()))
if info.Manifest != nil {
ret = append(ret, info)
}
}
return ret, nil
}
var pluginIDBlocklist = map[string]bool{
"playbooks": true,
"com.mattermost.plugin-incident-response": true,
"com.mattermost.plugin-incident-management": true,
"focalboard": true,
}
func PluginIDIsBlocked(id string) bool {
_, ok := pluginIDBlocklist[id]
return ok
}
// Returns a list of all plugins within the environment.
func (env *Environment) Available() ([]*model.BundleInfo, error) {
rawList, err := scanSearchPath(env.pluginDir)
if err != nil {
return nil, err
}
// Filter any plugins that match the blocklist
filteredList := make([]*model.BundleInfo, 0, len(rawList))
for _, bundleInfo := range rawList {
if PluginIDIsBlocked(bundleInfo.Manifest.Id) {
env.logger.Debug("Plugin ignored by blocklist", mlog.String("plugin_id", bundleInfo.Manifest.Id))
} else {
filteredList = append(filteredList, bundleInfo)
}
}
return filteredList, nil
}
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.
// The list content is immutable and should not be modified.
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
env.prepackagedPluginsLock.RLock()
defer env.prepackagedPluginsLock.RUnlock()
return env.prepackagedPlugins
}
// Returns a list of all currently active plugins within the environment.
// The returned list should not be modified.
func (env *Environment) Active() []*model.BundleInfo {
activePlugins := []*model.BundleInfo{}
env.registeredPlugins.Range(func(key, value any) bool {
plugin := value.(registeredPlugin)
if env.IsActive(plugin.BundleInfo.Manifest.Id) {
activePlugins = append(activePlugins, plugin.BundleInfo)
}
return true
})
return activePlugins
}
// IsActive returns true if the plugin with the given id is active.
func (env *Environment) IsActive(id string) bool {
return env.GetPluginState(id) == model.PluginStateRunning
}
func (env *Environment) SetPluginError(id string, err string) {
if rp, ok := env.registeredPlugins.Load(id); ok {
p := rp.(registeredPlugin)
p.Error = err
env.registeredPlugins.Store(id, p)
}
}
func (env *Environment) getPluginError(id string) string {
if rp, ok := env.registeredPlugins.Load(id); ok {
return rp.(registeredPlugin).Error
}
return ""
}
// GetPluginState returns the current state of a plugin (disabled, running, or error)
func (env *Environment) GetPluginState(id string) int {
rp, ok := env.registeredPlugins.Load(id)
if !ok {
return model.PluginStateNotRunning
}
return rp.(registeredPlugin).State
}
// setPluginState sets the current state of a plugin (disabled, running, or error)
func (env *Environment) setPluginState(id string, state int) {
if rp, ok := env.registeredPlugins.Load(id); ok {
p := rp.(registeredPlugin)
p.State = state
env.registeredPlugins.Store(id, p)
}
}
// PublicFilesPath returns a path and true if the plugin with the given id is active.
// It returns an empty string and false if the path is not set or invalid
func (env *Environment) PublicFilesPath(id string) (string, error) {
if !env.IsActive(id) {
return "", fmt.Errorf("plugin not found: %v", id)
}
return filepath.Join(env.pluginDir, id, "public"), nil
}
// Statuses returns a list of plugin statuses representing the state of every plugin
func (env *Environment) Statuses() (model.PluginStatuses, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.Wrap(err, "unable to get plugin statuses")
}
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
for _, plugin := range plugins {
// For now we don't handle bad manifests, we should
if plugin.Manifest == nil {
continue
}
pluginState := env.GetPluginState(plugin.Manifest.Id)
status := &model.PluginStatus{
PluginId: plugin.Manifest.Id,
PluginPath: filepath.Dir(plugin.ManifestPath),
State: pluginState,
Error: env.getPluginError(plugin.Manifest.Id),
Name: plugin.Manifest.Name,
Description: plugin.Manifest.Description,
Version: plugin.Manifest.Version,
}
pluginStatuses = append(pluginStatuses, status)
}
return pluginStatuses, nil
}
// GetManifest returns a manifest for a given pluginId.
// Returns ErrNotFound if plugin is not found.
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.Wrap(err, "unable to get plugin statuses")
}
for _, plugin := range plugins {
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
return plugin.Manifest, nil
}
}
return nil, ErrNotFound
}
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
defer func() {
if reterr != nil {
env.SetPluginError(id, reterr.Error())
} else {
env.SetPluginError(id, "")
}
}()
// Check if we are already active
if env.IsActive(id) {
return nil, false, nil
}
plugins, err := env.Available()
if err != nil {
return nil, false, err
}
var pluginInfo *model.BundleInfo
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
if pluginInfo != nil {
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
}
pluginInfo = p
}
}
if pluginInfo == nil {
return nil, false, fmt.Errorf("plugin not found: %v", id)
}
rp := newRegisteredPlugin(pluginInfo)
env.registeredPlugins.Store(id, rp)
defer func() {
if reterr == nil {
env.setPluginState(id, model.PluginStateRunning)
} else {
env.setPluginState(id, model.PluginStateFailedToStart)
}
}()
if pluginInfo.Manifest.MinServerVersion != "" {
fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion)
if err != nil {
return nil, false, fmt.Errorf("%v: %v", err.Error(), id)
}
if !fulfilled {
return nil, false, fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, id)
}
}
componentActivated := false
if pluginInfo.Manifest.HasWebapp() {
updatedManifest, err := env.UnpackWebappBundle(id)
if err != nil {
return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id)
}
pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash
componentActivated = true
}
if pluginInfo.Manifest.HasServer() {
sup, err := newSupervisor(pluginInfo, env.newAPIImpl(pluginInfo.Manifest), env.dbDriver, env.logger, env.metrics)
if err != nil {
return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id)
}
// We pre-emptively set the state to running to prevent re-entrancy issues.
// The plugin's OnActivate hook can in-turn call UpdateConfiguration
// which again calls this method. This method is guarded against multiple calls,
// but fails if it is called recursively.
//
// Therefore, setting the state to running prevents this from happening,
// and in case there is an error, the defer clause will set the proper state anyways.
env.setPluginState(id, model.PluginStateRunning)
if err := sup.Hooks().OnActivate(); err != nil {
sup.Shutdown()
return nil, false, err
}
rp.supervisor = sup
env.registeredPlugins.Store(id, rp)
componentActivated = true
}
if !componentActivated {
return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component")
}
mlog.Debug("Plugin activated", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version))
return pluginInfo.Manifest, true, nil
}
func (env *Environment) RemovePlugin(id string) {
if _, ok := env.registeredPlugins.Load(id); ok {
env.registeredPlugins.Delete(id)
}
}
// Deactivates the plugin with the given id.
func (env *Environment) Deactivate(id string) bool {
p, ok := env.registeredPlugins.Load(id)
if !ok {
return false
}
isActive := env.IsActive(id)
env.setPluginState(id, model.PluginStateNotRunning)
if !isActive {
return false
}
rp := p.(registeredPlugin)
if rp.supervisor != nil {
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
}
rp.supervisor.Shutdown()
}
return true
}
// RestartPlugin deactivates, then activates the plugin with the given id.
func (env *Environment) RestartPlugin(id string) error {
env.Deactivate(id)
_, _, err := env.Activate(id)
return err
}
// Shutdown deactivates all plugins and gracefully shuts down the environment.
func (env *Environment) Shutdown() {
env.TogglePluginHealthCheckJob(false)
var wg sync.WaitGroup
env.registeredPlugins.Range(func(key, value any) bool {
rp := value.(registeredPlugin)
if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) {
return true
}
wg.Add(1)
done := make(chan bool)
go func() {
defer close(done)
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
}
}()
go func() {
defer wg.Done()
select {
case <-time.After(10 * time.Second):
env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id))
case <-done:
}
rp.supervisor.Shutdown()
}()
return true
})
wg.Wait()
env.registeredPlugins.Range(func(key, value any) bool {
env.registeredPlugins.Delete(key)
return true
})
}
// UnpackWebappBundle unpacks webapp bundle for a given plugin id on disk.
func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.New("Unable to get available plugins")
}
var manifest *model.Manifest
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
if manifest != nil {
return nil, fmt.Errorf("multiple plugins found: %v", id)
}
manifest = p.Manifest
}
}
if manifest == nil {
return nil, fmt.Errorf("plugin not found: %v", id)
}
bundlePath := filepath.Clean(manifest.Webapp.BundlePath)
if bundlePath == "" || bundlePath[0] == '.' {
return nil, fmt.Errorf("invalid webapp bundle path")
}
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
destinationPath := filepath.Join(env.webappPluginDir, id)
if err = os.RemoveAll(destinationPath); err != nil {
return nil, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath)
}
if err = utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil {
return nil, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id)
}
sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath))
sourceBundleFileContents, err := os.ReadFile(sourceBundleFilepath)
if err != nil {
return nil, errors.Wrapf(err, "unable to read webapp bundle: %v", id)
}
if env.patchReactDOM {
newContents, changed := patchReactDOM(sourceBundleFileContents)
if changed {
sourceBundleFileContents = newContents
err = os.WriteFile(sourceBundleFilepath, sourceBundleFileContents, 0644)
if err != nil {
return nil, errors.Wrapf(err, "unable to overwrite webapp bundle: %v", id)
}
}
}
hash := fnv.New64a()
if _, err = hash.Write(sourceBundleFileContents); err != nil {
return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id)
}
manifest.Webapp.BundleHash = hash.Sum([]byte{})
if err = os.Rename(
sourceBundleFilepath,
filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, manifest.Webapp.BundleHash)),
); err != nil {
return nil, errors.Wrapf(err, "unable to rename webapp bundle: %v", id)
}
return manifest, nil
}
func patchReactDOM(initialBytes []byte) ([]byte, bool) {
if !bytes.Contains(initialBytes, []byte("react-dom.production.min.js")) {
return initialBytes, false
}
initial := string(initialBytes)
nameIndex := strings.Index(initial, "react-dom.production.min.js")
beginning := strings.LastIndex(initial[:nameIndex], "{")
var end int
argDefBeginning := strings.LastIndex(initial[:beginning], "function") + 9
argDefEnd := strings.LastIndex(initial[:beginning], ")") - 1
argsNames := strings.Split(initial[argDefBeginning:argDefEnd], ",")
if len(argsNames) != 3 {
return initialBytes, false
}
exportsArgName := strings.TrimSpace(argsNames[1])
numOpenBraces := 0
for i, c := range initial[beginning:] {
if end != 0 {
break
}
switch c {
case '}':
numOpenBraces--
if numOpenBraces == 0 {
end = beginning + i
}
case '{':
numOpenBraces++
}
}
beforePatch := initial[:end]
afterPatch := initial[end:]
patch := fmt.Sprintf("; Object.assign(%s, window.ReactDOM)", exportsArgName)
result := fmt.Sprintf("%s%s%s", beforePatch, patch, afterPatch)
return []byte(result), true
}
// HooksForPlugin returns the hooks API for the plugin with the given id.
//
// Consider using RunMultiPluginHook instead.
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
if p, ok := env.registeredPlugins.Load(id); ok {
rp := p.(registeredPlugin)
if rp.supervisor != nil && env.IsActive(id) {
return rp.supervisor.Hooks(), nil
}
}
return nil, fmt.Errorf("plugin not found: %v", id)
}
// RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId.
//
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
// plugins is not specified.
func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool, hookId int) {
startTime := time.Now()
env.registeredPlugins.Range(func(key, value any) bool {
rp := value.(registeredPlugin)
if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) {
return true
}
hookStartTime := time.Now()
result := hookRunnerFunc(rp.supervisor.Hooks())
if env.metrics != nil {
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime)
}
return result
})
if env.metrics != nil {
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
env.metrics.ObservePluginMultiHookDuration(elapsedTime)
}
}
// PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed.
func (env *Environment) PerformHealthCheck(id string) error {
p, ok := env.registeredPlugins.Load(id)
if !ok {
return nil
}
rp := p.(registeredPlugin)
sup := rp.supervisor
if sup == nil {
return nil
}
return sup.PerformHealthCheck()
}
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) {
env.prepackagedPluginsLock.Lock()
env.prepackagedPlugins = plugins
env.prepackagedPluginsLock.Unlock()
}
func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin {
state := model.PluginStateNotRunning
return registeredPlugin{State: state, BundleInfo: bundle}
}
// TogglePluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled.
func (env *Environment) TogglePluginHealthCheckJob(enable bool) {
// Config is set to enable. No job exists, start a new job.
if enable && env.pluginHealthCheckJob == nil {
mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval))
job := newPluginHealthCheckJob(env)
env.pluginHealthCheckJob = job
go job.run()
}
// Config is set to disable. Job exists, kill existing job.
if !enable && env.pluginHealthCheckJob != nil {
mlog.Debug("Disabling plugin health check job")
env.pluginHealthCheckJob.Cancel()
env.pluginHealthCheckJob = nil
}
}
// GetPluginHealthCheckJob returns the configured PluginHealthCheckJob, if any.
func (env *Environment) GetPluginHealthCheckJob() *PluginHealthCheckJob {
return env.pluginHealthCheckJob
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"fmt"
"io"
"log"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type hclogAdapter struct {
wrappedLogger *mlog.Logger
extrasKey string
}
func (h *hclogAdapter) Log(level hclog.Level, msg string, args ...any) {
switch level {
case hclog.Trace:
h.Trace(msg, args...)
case hclog.Debug:
h.Debug(msg, args...)
case hclog.Info:
h.Info(msg, args...)
case hclog.Warn:
h.Warn(msg, args...)
case hclog.Error:
h.Error(msg, args...)
default:
// For unknown/unexpected log level, treat it as an error so we notice and fix the code.
h.Error(msg, args...)
}
}
func (h *hclogAdapter) Trace(msg string, args ...any) {
extras := strings.TrimSpace(fmt.Sprint(args...))
if extras != "" {
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
} else {
h.wrappedLogger.Debug(msg)
}
}
func (h *hclogAdapter) Debug(msg string, args ...any) {
extras := strings.TrimSpace(fmt.Sprint(args...))
if extras != "" {
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
} else {
h.wrappedLogger.Debug(msg)
}
}
func (h *hclogAdapter) Info(msg string, args ...any) {
extras := strings.TrimSpace(fmt.Sprint(args...))
if extras != "" {
h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, extras))
} else {
h.wrappedLogger.Info(msg)
}
}
func (h *hclogAdapter) Warn(msg string, args ...any) {
extras := strings.TrimSpace(fmt.Sprint(args...))
if extras != "" {
h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, extras))
} else {
h.wrappedLogger.Warn(msg)
}
}
func (h *hclogAdapter) Error(msg string, args ...any) {
extras := strings.TrimSpace(fmt.Sprint(args...))
if extras != "" {
h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, extras))
} else {
h.wrappedLogger.Error(msg)
}
}
func (h *hclogAdapter) IsTrace() bool {
return false
}
func (h *hclogAdapter) IsDebug() bool {
return true
}
func (h *hclogAdapter) IsInfo() bool {
return true
}
func (h *hclogAdapter) IsWarn() bool {
return true
}
func (h *hclogAdapter) IsError() bool {
return true
}
func (h *hclogAdapter) With(args ...any) hclog.Logger {
return h
}
func (h *hclogAdapter) Named(name string) hclog.Logger {
return h
}
func (h *hclogAdapter) ResetNamed(name string) hclog.Logger {
return h
}
func (h *hclogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return h.wrappedLogger.StdLogger(mlog.LvlInfo)
}
func (h *hclogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return h.wrappedLogger.StdLogWriter()
}
func (h *hclogAdapter) SetLevel(hclog.Level) {}
func (h *hclogAdapter) GetLevel() hclog.Level { return hclog.NoLevel }
func (h *hclogAdapter) ImpliedArgs() []any {
return []any{}
}
func (h *hclogAdapter) Name() string {
return "MattermostPluginLogger"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
HealthCheckInterval = 30 * time.Second // How often the health check should run
HealthCheckDeactivationWindow = 60 * time.Minute // How long we wait for num fails to occur before deactivating the plugin
HealthCheckPingFailLimit = 3 // How many times we call RPC ping in a row before it is considered a failure
HealthCheckNumRestartsLimit = 3 // How many times we restart a plugin before we deactivate it
)
type PluginHealthCheckJob struct {
cancel chan struct{}
cancelled chan struct{}
cancelOnce sync.Once
env *Environment
failureTimestamps sync.Map
}
// run continuously performs health checks on all active plugins, on a timer.
func (job *PluginHealthCheckJob) run() {
mlog.Debug("Plugin health check job starting.")
defer close(job.cancelled)
ticker := time.NewTicker(HealthCheckInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
activePlugins := job.env.Active()
for _, plugin := range activePlugins {
job.CheckPlugin(plugin.Manifest.Id)
}
case <-job.cancel:
return
}
}
}
// CheckPlugin determines the plugin's health status, then handles the error or success case.
// If the plugin passes the health check, do nothing.
// If the plugin fails the health check, the function either restarts or deactivates the plugin, based on the quantity and frequency of its failures.
func (job *PluginHealthCheckJob) CheckPlugin(id string) {
err := job.env.PerformHealthCheck(id)
if err == nil {
return
}
mlog.Warn("Health check failed for plugin", mlog.String("id", id), mlog.Err(err))
timestamps := job.getStoredTimestamps(id)
timestamps = append(timestamps, time.Now())
if shouldDeactivatePlugin(timestamps) {
// Order matters here, must deactivate first and then set plugin state
mlog.Debug("Deactivating plugin due to multiple crashes", mlog.String("id", id))
job.env.Deactivate(id)
// Reset timestamp state for this plugin
job.failureTimestamps.Delete(id)
job.env.setPluginState(id, model.PluginStateFailedToStayRunning)
} else {
mlog.Debug("Restarting plugin due to failed health check", mlog.String("id", id))
if err := job.env.RestartPlugin(id); err != nil {
mlog.Error("Failed to restart plugin", mlog.String("id", id), mlog.Err(err))
}
// Store this failure so we can continue to monitor the plugin
job.failureTimestamps.Store(id, removeStaleTimestamps(timestamps))
}
}
// getStoredTimestamps returns the stored failure timestamps for a plugin.
func (job *PluginHealthCheckJob) getStoredTimestamps(id string) []time.Time {
timestamps, ok := job.failureTimestamps.Load(id)
if !ok {
timestamps = []time.Time{}
}
return timestamps.([]time.Time)
}
func newPluginHealthCheckJob(env *Environment) *PluginHealthCheckJob {
return &PluginHealthCheckJob{
cancel: make(chan struct{}),
cancelled: make(chan struct{}),
env: env,
}
}
func (job *PluginHealthCheckJob) Cancel() {
job.cancelOnce.Do(func() {
close(job.cancel)
})
<-job.cancelled
}
// shouldDeactivatePlugin determines if a plugin needs to be deactivated after the plugin has failed (HealthCheckNumRestartsLimit) times,
// within the configured time window (HealthCheckDeactivationWindow).
func shouldDeactivatePlugin(failedTimestamps []time.Time) bool {
if len(failedTimestamps) < HealthCheckNumRestartsLimit {
return false
}
index := len(failedTimestamps) - HealthCheckNumRestartsLimit
return time.Since(failedTimestamps[index]) <= HealthCheckDeactivationWindow
}
// removeStaleTimestamps only keeps the last HealthCheckNumRestartsLimit items in timestamps.
func removeStaleTimestamps(timestamps []time.Time) []time.Time {
if len(timestamps) > HealthCheckNumRestartsLimit {
timestamps = timestamps[len(timestamps)-HealthCheckNumRestartsLimit:]
}
return timestamps
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"bufio"
"errors"
"net"
"net/http"
"net/rpc"
"time"
)
const (
hijackedConnReadBufSize = 4096
)
var (
ErrNotHijacked = errors.New("response is not hijacked")
ErrAlreadyHijacked = errors.New("response was already hijacked")
ErrCannotHijack = errors.New("response cannot be hijacked")
)
func (w *httpResponseWriterRPCServer) HjConnRWRead(b []byte, reply *[]byte) error {
if w.hjr == nil {
return ErrNotHijacked
}
data := make([]byte, len(b))
n, err := w.hjr.bufrw.Read(data)
if err != nil {
return err
}
*reply = data[:n]
return nil
}
func (w *httpResponseWriterRPCServer) HjConnRWWrite(b []byte, reply *int) error {
if w.hjr == nil {
return ErrNotHijacked
}
n, err := w.hjr.bufrw.Write(b)
if err != nil {
return err
}
*reply = n
return nil
}
func (w *httpResponseWriterRPCServer) HjConnRead(size int, reply *[]byte) error {
if w.hjr == nil {
return ErrNotHijacked
}
if len(w.hjr.readBuf) < size {
w.hjr.readBuf = make([]byte, size)
}
n, err := w.hjr.conn.Read(w.hjr.readBuf[:size])
if err != nil {
return err
}
*reply = w.hjr.readBuf[:n]
return nil
}
func (w *httpResponseWriterRPCServer) HjConnWrite(b []byte, reply *int) error {
if w.hjr == nil {
return ErrNotHijacked
}
n, err := w.hjr.conn.Write(b)
if err != nil {
return err
}
*reply = n
return nil
}
func (w *httpResponseWriterRPCServer) HjConnClose(args struct{}, reply *struct{}) error {
if w.hjr == nil {
return ErrNotHijacked
}
return w.hjr.conn.Close()
}
func (w *httpResponseWriterRPCServer) HjConnSetDeadline(t time.Time, reply *struct{}) error {
if w.hjr == nil {
return ErrNotHijacked
}
return w.hjr.conn.SetDeadline(t)
}
func (w *httpResponseWriterRPCServer) HjConnSetReadDeadline(t time.Time, reply *struct{}) error {
if w.hjr == nil {
return ErrNotHijacked
}
return w.hjr.conn.SetReadDeadline(t)
}
func (w *httpResponseWriterRPCServer) HjConnSetWriteDeadline(t time.Time, reply *struct{}) error {
if w.hjr == nil {
return ErrNotHijacked
}
return w.hjr.conn.SetWriteDeadline(t)
}
func (w *httpResponseWriterRPCServer) HijackResponse(args struct{}, reply *struct{}) error {
if w.hjr != nil {
return ErrAlreadyHijacked
}
hj, ok := w.w.(http.Hijacker)
if !ok {
return ErrCannotHijack
}
conn, bufrw, err := hj.Hijack()
if err != nil {
return err
}
w.hjr = &hijackedResponse{
conn: conn,
bufrw: bufrw,
readBuf: make([]byte, hijackedConnReadBufSize),
}
return nil
}
type hijackedConn struct {
client *rpc.Client
}
type hijackedConnRW struct {
client *rpc.Client
}
func (w *hijackedConnRW) Read(b []byte) (int, error) {
var data []byte
if err := w.client.Call("Plugin.HjConnRWRead", b, &data); err != nil {
return 0, err
}
copy(b, data)
return len(data), nil
}
func (w *hijackedConnRW) Write(b []byte) (int, error) {
var n int
if err := w.client.Call("Plugin.HjConnRWWrite", b, &n); err != nil {
return 0, err
}
return n, nil
}
func (w *hijackedConn) Read(b []byte) (int, error) {
var data []byte
if err := w.client.Call("Plugin.HjConnRead", len(b), &data); err != nil {
return 0, err
}
copy(b, data)
return len(data), nil
}
func (w *hijackedConn) Write(b []byte) (int, error) {
var n int
if err := w.client.Call("Plugin.HjConnWrite", b, &n); err != nil {
return 0, err
}
return n, nil
}
func (w *hijackedConn) Close() error {
return w.client.Call("Plugin.HjConnClose", struct{}{}, nil)
}
func (w *hijackedConn) LocalAddr() net.Addr {
return nil
}
func (w *hijackedConn) RemoteAddr() net.Addr {
return nil
}
func (w *hijackedConn) SetDeadline(t time.Time) error {
return w.client.Call("Plugin.HjConnSetDeadline", t, nil)
}
func (w *hijackedConn) SetReadDeadline(t time.Time) error {
return w.client.Call("Plugin.HjConnSetReadDeadline", t, nil)
}
func (w *hijackedConn) SetWriteDeadline(t time.Time) error {
return w.client.Call("Plugin.HjConnSetWriteDeadline", t, nil)
}
func (w *httpResponseWriterRPCClient) Hijack() (net.Conn, *bufio.ReadWriter, error) {
c := &hijackedConn{
client: w.client,
}
rw := &hijackedConnRW{
client: w.client,
}
if err := w.client.Call("Plugin.HijackResponse", struct{}{}, nil); err != nil {
return nil, nil, err
}
return c, bufio.NewReadWriter(bufio.NewReader(rw), bufio.NewWriter(rw)), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
import (
"io"
"net/http"
timePkg "time"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/model"
)
type hooksTimerLayer struct {
pluginID string
hooksImpl Hooks
metrics einterfaces.MetricsInterface
}
func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
if hooks.metrics != nil {
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime)
}
}
func (hooks *hooksTimerLayer) OnActivate() error {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.OnActivate()
hooks.recordTime(startTime, "OnActivate", _returnsA == nil)
return _returnsA
}
func (hooks *hooksTimerLayer) Implemented() ([]string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.Implemented()
hooks.recordTime(startTime, "Implemented", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) OnDeactivate() error {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.OnDeactivate()
hooks.recordTime(startTime, "OnDeactivate", _returnsA == nil)
return _returnsA
}
func (hooks *hooksTimerLayer) OnConfigurationChange() error {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.OnConfigurationChange()
hooks.recordTime(startTime, "OnConfigurationChange", _returnsA == nil)
return _returnsA
}
func (hooks *hooksTimerLayer) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
startTime := timePkg.Now()
hooks.hooksImpl.ServeHTTP(c, w, r)
hooks.recordTime(startTime, "ServeHTTP", true)
}
func (hooks *hooksTimerLayer) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.ExecuteCommand(c, args)
hooks.recordTime(startTime, "ExecuteCommand", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) UserHasBeenCreated(c *Context, user *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasBeenCreated(c, user)
hooks.recordTime(startTime, "UserHasBeenCreated", true)
}
func (hooks *hooksTimerLayer) UserWillLogIn(c *Context, user *model.User) string {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.UserWillLogIn(c, user)
hooks.recordTime(startTime, "UserWillLogIn", true)
return _returnsA
}
func (hooks *hooksTimerLayer) UserHasLoggedIn(c *Context, user *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasLoggedIn(c, user)
hooks.recordTime(startTime, "UserHasLoggedIn", true)
}
func (hooks *hooksTimerLayer) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.MessageWillBePosted(c, post)
hooks.recordTime(startTime, "MessageWillBePosted", true)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.MessageWillBeUpdated(c, newPost, oldPost)
hooks.recordTime(startTime, "MessageWillBeUpdated", true)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) MessageHasBeenPosted(c *Context, post *model.Post) {
startTime := timePkg.Now()
hooks.hooksImpl.MessageHasBeenPosted(c, post)
hooks.recordTime(startTime, "MessageHasBeenPosted", true)
}
func (hooks *hooksTimerLayer) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
startTime := timePkg.Now()
hooks.hooksImpl.MessageHasBeenUpdated(c, newPost, oldPost)
hooks.recordTime(startTime, "MessageHasBeenUpdated", true)
}
func (hooks *hooksTimerLayer) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
startTime := timePkg.Now()
hooks.hooksImpl.ChannelHasBeenCreated(c, channel)
hooks.recordTime(startTime, "ChannelHasBeenCreated", true)
}
func (hooks *hooksTimerLayer) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasJoinedChannel(c, channelMember, actor)
hooks.recordTime(startTime, "UserHasJoinedChannel", true)
}
func (hooks *hooksTimerLayer) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasLeftChannel(c, channelMember, actor)
hooks.recordTime(startTime, "UserHasLeftChannel", true)
}
func (hooks *hooksTimerLayer) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasJoinedTeam(c, teamMember, actor)
hooks.recordTime(startTime, "UserHasJoinedTeam", true)
}
func (hooks *hooksTimerLayer) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
startTime := timePkg.Now()
hooks.hooksImpl.UserHasLeftTeam(c, teamMember, actor)
hooks.recordTime(startTime, "UserHasLeftTeam", true)
}
func (hooks *hooksTimerLayer) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.FileWillBeUploaded(c, info, file, output)
hooks.recordTime(startTime, "FileWillBeUploaded", true)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
startTime := timePkg.Now()
hooks.hooksImpl.ReactionHasBeenAdded(c, reaction)
hooks.recordTime(startTime, "ReactionHasBeenAdded", true)
}
func (hooks *hooksTimerLayer) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
startTime := timePkg.Now()
hooks.hooksImpl.ReactionHasBeenRemoved(c, reaction)
hooks.recordTime(startTime, "ReactionHasBeenRemoved", true)
}
func (hooks *hooksTimerLayer) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
startTime := timePkg.Now()
hooks.hooksImpl.OnPluginClusterEvent(c, ev)
hooks.recordTime(startTime, "OnPluginClusterEvent", true)
}
func (hooks *hooksTimerLayer) OnWebSocketConnect(webConnID, userID string) {
startTime := timePkg.Now()
hooks.hooksImpl.OnWebSocketConnect(webConnID, userID)
hooks.recordTime(startTime, "OnWebSocketConnect", true)
}
func (hooks *hooksTimerLayer) OnWebSocketDisconnect(webConnID, userID string) {
startTime := timePkg.Now()
hooks.hooksImpl.OnWebSocketDisconnect(webConnID, userID)
hooks.recordTime(startTime, "OnWebSocketDisconnect", true)
}
func (hooks *hooksTimerLayer) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
startTime := timePkg.Now()
hooks.hooksImpl.WebSocketMessageHasBeenPosted(webConnID, userID, req)
hooks.recordTime(startTime, "WebSocketMessageHasBeenPosted", true)
}
func (hooks *hooksTimerLayer) RunDataRetention(nowTime, batchSize int64) (int64, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.RunDataRetention(nowTime, batchSize)
hooks.recordTime(startTime, "RunDataRetention", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) OnInstall(c *Context, event model.OnInstallEvent) error {
startTime := timePkg.Now()
_returnsA := hooks.hooksImpl.OnInstall(c, event)
hooks.recordTime(startTime, "OnInstall", _returnsA == nil)
return _returnsA
}
func (hooks *hooksTimerLayer) OnSendDailyTelemetry() {
startTime := timePkg.Now()
hooks.hooksImpl.OnSendDailyTelemetry()
hooks.recordTime(startTime, "OnSendDailyTelemetry", true)
}
func (hooks *hooksTimerLayer) OnCloudLimitsUpdated(limits *model.ProductLimits) {
startTime := timePkg.Now()
hooks.hooksImpl.OnCloudLimitsUpdated(limits)
hooks.recordTime(startTime, "OnCloudLimitsUpdated", true)
}
func (hooks *hooksTimerLayer) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission)
hooks.recordTime(startTime, "UserHasPermissionToCollection", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GetAllCollectionIDsForUser(c, userID, collectionType)
hooks.recordTime(startTime, "GetAllCollectionIDsForUser", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GetAllUserIdsForCollection(c, collectionType, collectionID)
hooks.recordTime(startTime, "GetAllUserIdsForCollection", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GetTopicRedirect(c, topicType, topicID)
hooks.recordTime(startTime, "GetTopicRedirect", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GetCollectionMetadataByIds(c, collectionType, collectionIds)
hooks.recordTime(startTime, "GetCollectionMetadataByIds", _returnsB == nil)
return _returnsA, _returnsB
}
func (hooks *hooksTimerLayer) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GetTopicMetadataByIds(c, topicType, topicIds)
hooks.recordTime(startTime, "GetTopicMetadataByIds", _returnsB == nil)
return _returnsA, _returnsB
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/rpc"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type hijackedResponse struct {
conn net.Conn
bufrw *bufio.ReadWriter
readBuf []byte
}
type httpResponseWriterRPCServer struct {
w http.ResponseWriter
log *mlog.Logger
hjr *hijackedResponse
}
func (w *httpResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error {
*reply = w.w.Header()
return nil
}
func (w *httpResponseWriterRPCServer) Write(args []byte, reply *struct{}) error {
_, err := w.w.Write(args)
return err
}
func (w *httpResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error {
// Check if args is a valid http status code. This prevents plugins from crashing the server with a panic.
// This is a copy of the checkWriteHeaderCode function in net/http/server.go in the go source.
if args < 100 || args > 999 {
w.log.Error(fmt.Sprintf("Plugin tried to write an invalid http status code: %v. Did not write the invalid header.", args))
return errors.New("invalid http status code")
}
w.w.WriteHeader(args)
return nil
}
func (w *httpResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error {
dest := w.w.Header()
for k := range dest {
if _, ok := args[k]; !ok {
delete(dest, k)
}
}
for k, v := range args {
dest[k] = v
}
return nil
}
type httpResponseWriterRPCClient struct {
client *rpc.Client
header http.Header
}
var _ http.ResponseWriter = (*httpResponseWriterRPCClient)(nil)
func (w *httpResponseWriterRPCClient) Header() http.Header {
if w.header == nil {
w.client.Call("Plugin.Header", struct{}{}, &w.header)
}
return w.header
}
func (w *httpResponseWriterRPCClient) Write(b []byte) (int, error) {
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
return 0, err
}
if err := w.client.Call("Plugin.Write", b, nil); err != nil {
return 0, err
}
return len(b), nil
}
func (w *httpResponseWriterRPCClient) WriteHeader(statusCode int) {
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
return
}
w.client.Call("Plugin.WriteHeader", statusCode, nil)
}
func (w *httpResponseWriterRPCClient) Close() error {
return w.client.Close()
}
func connectHTTPResponseWriter(conn io.ReadWriteCloser) *httpResponseWriterRPCClient {
return &httpResponseWriterRPCClient{
client: rpc.NewClient(conn),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"bufio"
"encoding/binary"
"io"
)
type remoteIOReader struct {
conn io.ReadWriteCloser
}
func (r *remoteIOReader) Read(b []byte) (int, error) {
var buf [10]byte
n := binary.PutVarint(buf[:], int64(len(b)))
if _, err := r.conn.Write(buf[:n]); err != nil {
return 0, err
}
return r.conn.Read(b)
}
func (r *remoteIOReader) Close() error {
return r.conn.Close()
}
func connectIOReader(conn io.ReadWriteCloser) io.ReadCloser {
return &remoteIOReader{conn}
}
func serveIOReader(r io.Reader, conn io.ReadWriteCloser) {
cr := bufio.NewReader(conn)
defer conn.Close()
buf := make([]byte, 32*1024)
for {
n, err := binary.ReadVarint(cr)
if err != nil {
break
}
if written, err := io.CopyBuffer(conn, io.LimitReader(r, n), buf); err != nil || written < n {
break
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This package provides aliases for the contents of "github.com/stretchr/testify/mock". Because
// external packages can't import our vendored dependencies, this is necessary for them to be able
// to fully utilize the plugintest package.
package mock
import (
"github.com/stretchr/testify/mock"
)
const (
Anything = mock.Anything
)
type Arguments = mock.Arguments
type AnythingOfTypeArgument = mock.AnythingOfTypeArgument
type Call = mock.Call
type Mock = mock.Mock
type TestingT = mock.TestingT
func AnythingOfType(t string) AnythingOfTypeArgument {
return mock.AnythingOfType(t)
}
func AssertExpectationsForObjects(t TestingT, testObjects ...any) bool {
return mock.AssertExpectationsForObjects(t, testObjects...)
}
func MatchedBy(fn any) any {
return mock.MatchedBy(fn)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"net/http"
)
type RegisteredProduct struct {
ProductID string
Adapter Hooks
}
func (rp *RegisteredProduct) Implements(hookId int) bool {
adapter, ok := rp.Adapter.(*HooksAdapter)
if !ok {
return false
}
_, ok = adapter.implemented[hookId]
return ok
}
// Implemented method is overridden intentionally to prevent calling it from outside.
func (a *HooksAdapter) Implemented() ([]string, error) {
return nil, nil
}
// OnActivate is overridden intentionally as product should not call it.
func (a *HooksAdapter) OnActivate() error {
return nil
}
// OnDeactivate is overridden intentionally as product should not call it.
func (a *HooksAdapter) OnDeactivate() error {
return nil
}
// ServeHTTP is overridden intentionally as product should not call it.
func (a *HooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
import (
"errors"
"io"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
)
type OnConfigurationChangeIFace interface {
OnConfigurationChange() error
}
type ExecuteCommandIFace interface {
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
}
type UserHasBeenCreatedIFace interface {
UserHasBeenCreated(c *Context, user *model.User)
}
type UserWillLogInIFace interface {
UserWillLogIn(c *Context, user *model.User) string
}
type UserHasLoggedInIFace interface {
UserHasLoggedIn(c *Context, user *model.User)
}
type MessageWillBePostedIFace interface {
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
}
type MessageWillBeUpdatedIFace interface {
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
}
type MessageHasBeenPostedIFace interface {
MessageHasBeenPosted(c *Context, post *model.Post)
}
type MessageHasBeenUpdatedIFace interface {
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
}
type ChannelHasBeenCreatedIFace interface {
ChannelHasBeenCreated(c *Context, channel *model.Channel)
}
type UserHasJoinedChannelIFace interface {
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}
type UserHasLeftChannelIFace interface {
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}
type UserHasJoinedTeamIFace interface {
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}
type UserHasLeftTeamIFace interface {
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}
type FileWillBeUploadedIFace interface {
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
}
type ReactionHasBeenAddedIFace interface {
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
}
type ReactionHasBeenRemovedIFace interface {
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
}
type OnPluginClusterEventIFace interface {
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
}
type OnWebSocketConnectIFace interface {
OnWebSocketConnect(webConnID, userID string)
}
type OnWebSocketDisconnectIFace interface {
OnWebSocketDisconnect(webConnID, userID string)
}
type WebSocketMessageHasBeenPostedIFace interface {
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
}
type RunDataRetentionIFace interface {
RunDataRetention(nowTime, batchSize int64) (int64, error)
}
type OnInstallIFace interface {
OnInstall(c *Context, event model.OnInstallEvent) error
}
type OnSendDailyTelemetryIFace interface {
OnSendDailyTelemetry()
}
type OnCloudLimitsUpdatedIFace interface {
OnCloudLimitsUpdated(limits *model.ProductLimits)
}
type UserHasPermissionToCollectionIFace interface {
UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error)
}
type GetAllCollectionIDsForUserIFace interface {
GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error)
}
type GetAllUserIdsForCollectionIFace interface {
GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error)
}
type GetTopicRedirectIFace interface {
GetTopicRedirect(c *Context, topicType, topicID string) (string, error)
}
type GetCollectionMetadataByIdsIFace interface {
GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error)
}
type GetTopicMetadataByIdsIFace interface {
GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error)
}
type HooksAdapter struct {
implemented map[int]struct{}
productHooks any
}
func NewAdapter(productHooks any) (*HooksAdapter, error) {
a := &HooksAdapter{
implemented: make(map[int]struct{}),
productHooks: productHooks,
}
var tt reflect.Type
ft := reflect.TypeOf(productHooks)
// Assessing the type of the productHooks if it individually implements OnConfigurationChange interface.
tt = reflect.TypeOf((*OnConfigurationChangeIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnConfigurationChangeID] = struct{}{}
} else if _, ok := ft.MethodByName("OnConfigurationChange"); ok {
return nil, errors.New("hook has OnConfigurationChange method but does not implement plugin.OnConfigurationChange interface")
}
// Assessing the type of the productHooks if it individually implements ExecuteCommand interface.
tt = reflect.TypeOf((*ExecuteCommandIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ExecuteCommandID] = struct{}{}
} else if _, ok := ft.MethodByName("ExecuteCommand"); ok {
return nil, errors.New("hook has ExecuteCommand method but does not implement plugin.ExecuteCommand interface")
}
// Assessing the type of the productHooks if it individually implements UserHasBeenCreated interface.
tt = reflect.TypeOf((*UserHasBeenCreatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasBeenCreatedID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasBeenCreated"); ok {
return nil, errors.New("hook has UserHasBeenCreated method but does not implement plugin.UserHasBeenCreated interface")
}
// Assessing the type of the productHooks if it individually implements UserWillLogIn interface.
tt = reflect.TypeOf((*UserWillLogInIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserWillLogInID] = struct{}{}
} else if _, ok := ft.MethodByName("UserWillLogIn"); ok {
return nil, errors.New("hook has UserWillLogIn method but does not implement plugin.UserWillLogIn interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLoggedIn interface.
tt = reflect.TypeOf((*UserHasLoggedInIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLoggedInID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLoggedIn"); ok {
return nil, errors.New("hook has UserHasLoggedIn method but does not implement plugin.UserHasLoggedIn interface")
}
// Assessing the type of the productHooks if it individually implements MessageWillBePosted interface.
tt = reflect.TypeOf((*MessageWillBePostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageWillBePostedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageWillBePosted"); ok {
return nil, errors.New("hook has MessageWillBePosted method but does not implement plugin.MessageWillBePosted interface")
}
// Assessing the type of the productHooks if it individually implements MessageWillBeUpdated interface.
tt = reflect.TypeOf((*MessageWillBeUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageWillBeUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageWillBeUpdated"); ok {
return nil, errors.New("hook has MessageWillBeUpdated method but does not implement plugin.MessageWillBeUpdated interface")
}
// Assessing the type of the productHooks if it individually implements MessageHasBeenPosted interface.
tt = reflect.TypeOf((*MessageHasBeenPostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageHasBeenPostedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageHasBeenPosted"); ok {
return nil, errors.New("hook has MessageHasBeenPosted method but does not implement plugin.MessageHasBeenPosted interface")
}
// Assessing the type of the productHooks if it individually implements MessageHasBeenUpdated interface.
tt = reflect.TypeOf((*MessageHasBeenUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageHasBeenUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageHasBeenUpdated"); ok {
return nil, errors.New("hook has MessageHasBeenUpdated method but does not implement plugin.MessageHasBeenUpdated interface")
}
// Assessing the type of the productHooks if it individually implements ChannelHasBeenCreated interface.
tt = reflect.TypeOf((*ChannelHasBeenCreatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ChannelHasBeenCreatedID] = struct{}{}
} else if _, ok := ft.MethodByName("ChannelHasBeenCreated"); ok {
return nil, errors.New("hook has ChannelHasBeenCreated method but does not implement plugin.ChannelHasBeenCreated interface")
}
// Assessing the type of the productHooks if it individually implements UserHasJoinedChannel interface.
tt = reflect.TypeOf((*UserHasJoinedChannelIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasJoinedChannelID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasJoinedChannel"); ok {
return nil, errors.New("hook has UserHasJoinedChannel method but does not implement plugin.UserHasJoinedChannel interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLeftChannel interface.
tt = reflect.TypeOf((*UserHasLeftChannelIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLeftChannelID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLeftChannel"); ok {
return nil, errors.New("hook has UserHasLeftChannel method but does not implement plugin.UserHasLeftChannel interface")
}
// Assessing the type of the productHooks if it individually implements UserHasJoinedTeam interface.
tt = reflect.TypeOf((*UserHasJoinedTeamIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasJoinedTeamID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasJoinedTeam"); ok {
return nil, errors.New("hook has UserHasJoinedTeam method but does not implement plugin.UserHasJoinedTeam interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLeftTeam interface.
tt = reflect.TypeOf((*UserHasLeftTeamIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLeftTeamID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLeftTeam"); ok {
return nil, errors.New("hook has UserHasLeftTeam method but does not implement plugin.UserHasLeftTeam interface")
}
// Assessing the type of the productHooks if it individually implements FileWillBeUploaded interface.
tt = reflect.TypeOf((*FileWillBeUploadedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[FileWillBeUploadedID] = struct{}{}
} else if _, ok := ft.MethodByName("FileWillBeUploaded"); ok {
return nil, errors.New("hook has FileWillBeUploaded method but does not implement plugin.FileWillBeUploaded interface")
}
// Assessing the type of the productHooks if it individually implements ReactionHasBeenAdded interface.
tt = reflect.TypeOf((*ReactionHasBeenAddedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ReactionHasBeenAddedID] = struct{}{}
} else if _, ok := ft.MethodByName("ReactionHasBeenAdded"); ok {
return nil, errors.New("hook has ReactionHasBeenAdded method but does not implement plugin.ReactionHasBeenAdded interface")
}
// Assessing the type of the productHooks if it individually implements ReactionHasBeenRemoved interface.
tt = reflect.TypeOf((*ReactionHasBeenRemovedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ReactionHasBeenRemovedID] = struct{}{}
} else if _, ok := ft.MethodByName("ReactionHasBeenRemoved"); ok {
return nil, errors.New("hook has ReactionHasBeenRemoved method but does not implement plugin.ReactionHasBeenRemoved interface")
}
// Assessing the type of the productHooks if it individually implements OnPluginClusterEvent interface.
tt = reflect.TypeOf((*OnPluginClusterEventIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnPluginClusterEventID] = struct{}{}
} else if _, ok := ft.MethodByName("OnPluginClusterEvent"); ok {
return nil, errors.New("hook has OnPluginClusterEvent method but does not implement plugin.OnPluginClusterEvent interface")
}
// Assessing the type of the productHooks if it individually implements OnWebSocketConnect interface.
tt = reflect.TypeOf((*OnWebSocketConnectIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnWebSocketConnectID] = struct{}{}
} else if _, ok := ft.MethodByName("OnWebSocketConnect"); ok {
return nil, errors.New("hook has OnWebSocketConnect method but does not implement plugin.OnWebSocketConnect interface")
}
// Assessing the type of the productHooks if it individually implements OnWebSocketDisconnect interface.
tt = reflect.TypeOf((*OnWebSocketDisconnectIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnWebSocketDisconnectID] = struct{}{}
} else if _, ok := ft.MethodByName("OnWebSocketDisconnect"); ok {
return nil, errors.New("hook has OnWebSocketDisconnect method but does not implement plugin.OnWebSocketDisconnect interface")
}
// Assessing the type of the productHooks if it individually implements WebSocketMessageHasBeenPosted interface.
tt = reflect.TypeOf((*WebSocketMessageHasBeenPostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[WebSocketMessageHasBeenPostedID] = struct{}{}
} else if _, ok := ft.MethodByName("WebSocketMessageHasBeenPosted"); ok {
return nil, errors.New("hook has WebSocketMessageHasBeenPosted method but does not implement plugin.WebSocketMessageHasBeenPosted interface")
}
// Assessing the type of the productHooks if it individually implements RunDataRetention interface.
tt = reflect.TypeOf((*RunDataRetentionIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[RunDataRetentionID] = struct{}{}
} else if _, ok := ft.MethodByName("RunDataRetention"); ok {
return nil, errors.New("hook has RunDataRetention method but does not implement plugin.RunDataRetention interface")
}
// Assessing the type of the productHooks if it individually implements OnInstall interface.
tt = reflect.TypeOf((*OnInstallIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnInstallID] = struct{}{}
} else if _, ok := ft.MethodByName("OnInstall"); ok {
return nil, errors.New("hook has OnInstall method but does not implement plugin.OnInstall interface")
}
// Assessing the type of the productHooks if it individually implements OnSendDailyTelemetry interface.
tt = reflect.TypeOf((*OnSendDailyTelemetryIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSendDailyTelemetryID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSendDailyTelemetry"); ok {
return nil, errors.New("hook has OnSendDailyTelemetry method but does not implement plugin.OnSendDailyTelemetry interface")
}
// Assessing the type of the productHooks if it individually implements OnCloudLimitsUpdated interface.
tt = reflect.TypeOf((*OnCloudLimitsUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnCloudLimitsUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("OnCloudLimitsUpdated"); ok {
return nil, errors.New("hook has OnCloudLimitsUpdated method but does not implement plugin.OnCloudLimitsUpdated interface")
}
// Assessing the type of the productHooks if it individually implements UserHasPermissionToCollection interface.
tt = reflect.TypeOf((*UserHasPermissionToCollectionIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasPermissionToCollectionID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasPermissionToCollection"); ok {
return nil, errors.New("hook has UserHasPermissionToCollection method but does not implement plugin.UserHasPermissionToCollection interface")
}
// Assessing the type of the productHooks if it individually implements GetAllCollectionIDsForUser interface.
tt = reflect.TypeOf((*GetAllCollectionIDsForUserIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[GetAllCollectionIDsForUserID] = struct{}{}
} else if _, ok := ft.MethodByName("GetAllCollectionIDsForUser"); ok {
return nil, errors.New("hook has GetAllCollectionIDsForUser method but does not implement plugin.GetAllCollectionIDsForUser interface")
}
// Assessing the type of the productHooks if it individually implements GetAllUserIdsForCollection interface.
tt = reflect.TypeOf((*GetAllUserIdsForCollectionIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[GetAllUserIdsForCollectionID] = struct{}{}
} else if _, ok := ft.MethodByName("GetAllUserIdsForCollection"); ok {
return nil, errors.New("hook has GetAllUserIdsForCollection method but does not implement plugin.GetAllUserIdsForCollection interface")
}
// Assessing the type of the productHooks if it individually implements GetTopicRedirect interface.
tt = reflect.TypeOf((*GetTopicRedirectIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[GetTopicRedirectID] = struct{}{}
} else if _, ok := ft.MethodByName("GetTopicRedirect"); ok {
return nil, errors.New("hook has GetTopicRedirect method but does not implement plugin.GetTopicRedirect interface")
}
// Assessing the type of the productHooks if it individually implements GetCollectionMetadataByIds interface.
tt = reflect.TypeOf((*GetCollectionMetadataByIdsIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[GetCollectionMetadataByIdsID] = struct{}{}
} else if _, ok := ft.MethodByName("GetCollectionMetadataByIds"); ok {
return nil, errors.New("hook has GetCollectionMetadataByIds method but does not implement plugin.GetCollectionMetadataByIds interface")
}
// Assessing the type of the productHooks if it individually implements GetTopicMetadataByIds interface.
tt = reflect.TypeOf((*GetTopicMetadataByIdsIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[GetTopicMetadataByIdsID] = struct{}{}
} else if _, ok := ft.MethodByName("GetTopicMetadataByIds"); ok {
return nil, errors.New("hook has GetTopicMetadataByIds method but does not implement plugin.GetTopicMetadataByIds interface")
}
return a, nil
}
func (a *HooksAdapter) OnConfigurationChange() error {
if _, ok := a.implemented[OnConfigurationChangeID]; !ok {
panic("product hooks must implement OnConfigurationChange")
}
return a.productHooks.(OnConfigurationChangeIFace).OnConfigurationChange()
}
func (a *HooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
if _, ok := a.implemented[ExecuteCommandID]; !ok {
panic("product hooks must implement ExecuteCommand")
}
return a.productHooks.(ExecuteCommandIFace).ExecuteCommand(c, args)
}
func (a *HooksAdapter) UserHasBeenCreated(c *Context, user *model.User) {
if _, ok := a.implemented[UserHasBeenCreatedID]; !ok {
panic("product hooks must implement UserHasBeenCreated")
}
a.productHooks.(UserHasBeenCreatedIFace).UserHasBeenCreated(c, user)
}
func (a *HooksAdapter) UserWillLogIn(c *Context, user *model.User) string {
if _, ok := a.implemented[UserWillLogInID]; !ok {
panic("product hooks must implement UserWillLogIn")
}
return a.productHooks.(UserWillLogInIFace).UserWillLogIn(c, user)
}
func (a *HooksAdapter) UserHasLoggedIn(c *Context, user *model.User) {
if _, ok := a.implemented[UserHasLoggedInID]; !ok {
panic("product hooks must implement UserHasLoggedIn")
}
a.productHooks.(UserHasLoggedInIFace).UserHasLoggedIn(c, user)
}
func (a *HooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
if _, ok := a.implemented[MessageWillBePostedID]; !ok {
panic("product hooks must implement MessageWillBePosted")
}
return a.productHooks.(MessageWillBePostedIFace).MessageWillBePosted(c, post)
}
func (a *HooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
if _, ok := a.implemented[MessageWillBeUpdatedID]; !ok {
panic("product hooks must implement MessageWillBeUpdated")
}
return a.productHooks.(MessageWillBeUpdatedIFace).MessageWillBeUpdated(c, newPost, oldPost)
}
func (a *HooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) {
if _, ok := a.implemented[MessageHasBeenPostedID]; !ok {
panic("product hooks must implement MessageHasBeenPosted")
}
a.productHooks.(MessageHasBeenPostedIFace).MessageHasBeenPosted(c, post)
}
func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
if _, ok := a.implemented[MessageHasBeenUpdatedID]; !ok {
panic("product hooks must implement MessageHasBeenUpdated")
}
a.productHooks.(MessageHasBeenUpdatedIFace).MessageHasBeenUpdated(c, newPost, oldPost)
}
func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok {
panic("product hooks must implement ChannelHasBeenCreated")
}
a.productHooks.(ChannelHasBeenCreatedIFace).ChannelHasBeenCreated(c, channel)
}
func (a *HooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
if _, ok := a.implemented[UserHasJoinedChannelID]; !ok {
panic("product hooks must implement UserHasJoinedChannel")
}
a.productHooks.(UserHasJoinedChannelIFace).UserHasJoinedChannel(c, channelMember, actor)
}
func (a *HooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
if _, ok := a.implemented[UserHasLeftChannelID]; !ok {
panic("product hooks must implement UserHasLeftChannel")
}
a.productHooks.(UserHasLeftChannelIFace).UserHasLeftChannel(c, channelMember, actor)
}
func (a *HooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
if _, ok := a.implemented[UserHasJoinedTeamID]; !ok {
panic("product hooks must implement UserHasJoinedTeam")
}
a.productHooks.(UserHasJoinedTeamIFace).UserHasJoinedTeam(c, teamMember, actor)
}
func (a *HooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
if _, ok := a.implemented[UserHasLeftTeamID]; !ok {
panic("product hooks must implement UserHasLeftTeam")
}
a.productHooks.(UserHasLeftTeamIFace).UserHasLeftTeam(c, teamMember, actor)
}
func (a *HooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
if _, ok := a.implemented[FileWillBeUploadedID]; !ok {
panic("product hooks must implement FileWillBeUploaded")
}
return a.productHooks.(FileWillBeUploadedIFace).FileWillBeUploaded(c, info, file, output)
}
func (a *HooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
if _, ok := a.implemented[ReactionHasBeenAddedID]; !ok {
panic("product hooks must implement ReactionHasBeenAdded")
}
a.productHooks.(ReactionHasBeenAddedIFace).ReactionHasBeenAdded(c, reaction)
}
func (a *HooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
if _, ok := a.implemented[ReactionHasBeenRemovedID]; !ok {
panic("product hooks must implement ReactionHasBeenRemoved")
}
a.productHooks.(ReactionHasBeenRemovedIFace).ReactionHasBeenRemoved(c, reaction)
}
func (a *HooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
if _, ok := a.implemented[OnPluginClusterEventID]; !ok {
panic("product hooks must implement OnPluginClusterEvent")
}
a.productHooks.(OnPluginClusterEventIFace).OnPluginClusterEvent(c, ev)
}
func (a *HooksAdapter) OnWebSocketConnect(webConnID, userID string) {
if _, ok := a.implemented[OnWebSocketConnectID]; !ok {
panic("product hooks must implement OnWebSocketConnect")
}
a.productHooks.(OnWebSocketConnectIFace).OnWebSocketConnect(webConnID, userID)
}
func (a *HooksAdapter) OnWebSocketDisconnect(webConnID, userID string) {
if _, ok := a.implemented[OnWebSocketDisconnectID]; !ok {
panic("product hooks must implement OnWebSocketDisconnect")
}
a.productHooks.(OnWebSocketDisconnectIFace).OnWebSocketDisconnect(webConnID, userID)
}
func (a *HooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
if _, ok := a.implemented[WebSocketMessageHasBeenPostedID]; !ok {
panic("product hooks must implement WebSocketMessageHasBeenPosted")
}
a.productHooks.(WebSocketMessageHasBeenPostedIFace).WebSocketMessageHasBeenPosted(webConnID, userID, req)
}
func (a *HooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) {
if _, ok := a.implemented[RunDataRetentionID]; !ok {
panic("product hooks must implement RunDataRetention")
}
return a.productHooks.(RunDataRetentionIFace).RunDataRetention(nowTime, batchSize)
}
func (a *HooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error {
if _, ok := a.implemented[OnInstallID]; !ok {
panic("product hooks must implement OnInstall")
}
return a.productHooks.(OnInstallIFace).OnInstall(c, event)
}
func (a *HooksAdapter) OnSendDailyTelemetry() {
if _, ok := a.implemented[OnSendDailyTelemetryID]; !ok {
panic("product hooks must implement OnSendDailyTelemetry")
}
a.productHooks.(OnSendDailyTelemetryIFace).OnSendDailyTelemetry()
}
func (a *HooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) {
if _, ok := a.implemented[OnCloudLimitsUpdatedID]; !ok {
panic("product hooks must implement OnCloudLimitsUpdated")
}
a.productHooks.(OnCloudLimitsUpdatedIFace).OnCloudLimitsUpdated(limits)
}
func (a *HooksAdapter) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) {
if _, ok := a.implemented[UserHasPermissionToCollectionID]; !ok {
panic("product hooks must implement UserHasPermissionToCollection")
}
return a.productHooks.(UserHasPermissionToCollectionIFace).UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission)
}
func (a *HooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) {
if _, ok := a.implemented[GetAllCollectionIDsForUserID]; !ok {
panic("product hooks must implement GetAllCollectionIDsForUser")
}
return a.productHooks.(GetAllCollectionIDsForUserIFace).GetAllCollectionIDsForUser(c, userID, collectionType)
}
func (a *HooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) {
if _, ok := a.implemented[GetAllUserIdsForCollectionID]; !ok {
panic("product hooks must implement GetAllUserIdsForCollection")
}
return a.productHooks.(GetAllUserIdsForCollectionIFace).GetAllUserIdsForCollection(c, collectionType, collectionID)
}
func (a *HooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) {
if _, ok := a.implemented[GetTopicRedirectID]; !ok {
panic("product hooks must implement GetTopicRedirect")
}
return a.productHooks.(GetTopicRedirectIFace).GetTopicRedirect(c, topicType, topicID)
}
func (a *HooksAdapter) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
if _, ok := a.implemented[GetCollectionMetadataByIdsID]; !ok {
panic("product hooks must implement GetCollectionMetadataByIds")
}
return a.productHooks.(GetCollectionMetadataByIdsIFace).GetCollectionMetadataByIds(c, collectionType, collectionIds)
}
func (a *HooksAdapter) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
if _, ok := a.implemented[GetTopicMetadataByIdsID]; !ok {
panic("product hooks must implement GetTopicMetadataByIds")
}
return a.productHooks.(GetTopicMetadataByIdsIFace).GetTopicMetadataByIds(c, topicType, topicIds)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package scheduler
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return true
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypePlugins, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package scheduler
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AppIface interface {
DeleteAllExpiredPluginKeys() *model.AppError
}
type Worker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
app AppIface
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
worker := Worker{
name: "Plugins",
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
app: app,
}
return &worker
}
func (worker *Worker) Run() {
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *Worker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
worker.stop <- true
<-worker.stopped
}
func (worker *Worker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *Worker) IsEnabled(cfg *model.Config) bool {
return true
}
func (worker *Worker) DoJob(job *model.Job) {
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Info("Worker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.String("error", err.Error()))
return
} else if !claimed {
return
}
if err := worker.app.DeleteAllExpiredPluginKeys(); err != nil {
mlog.Error("Worker: Failed to delete expired keys", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
}
mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
}
func (worker *Worker) setJobSuccess(job *model.Job) {
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
}
}
func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"fmt"
)
func stringify(objects []any) []string {
stringified := make([]string, len(objects))
for i, object := range objects {
stringified[i] = fmt.Sprintf("%+v", object)
}
return stringified
}
func toObjects(strings []string) []any {
if strings == nil {
return nil
}
objects := make([]any, len(strings))
for i, string := range strings {
objects[i] = string
}
return objects
}
func stringifyToObjects(objects []any) []any {
return toObjects(stringify(objects))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"crypto/sha256"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type supervisor struct {
lock sync.RWMutex
client *plugin.Client
hooks Hooks
implemented [TotalHooksID]bool
pid int
hooksClient *hooksRPCClient
}
func newSupervisor(pluginInfo *model.BundleInfo, apiImpl API, driver Driver, parentLogger *mlog.Logger, metrics einterfaces.MetricsInterface) (retSupervisor *supervisor, retErr error) {
sup := supervisor{}
defer func() {
if retErr != nil {
sup.Shutdown()
}
}()
wrappedLogger := pluginInfo.WrapLogger(parentLogger)
hclogAdaptedLogger := &hclogAdapter{
wrappedLogger: wrappedLogger,
extrasKey: "wrapped_extras",
}
pluginMap := map[string]plugin.Plugin{
"hooks": &hooksPlugin{
log: wrappedLogger,
driverImpl: driver,
apiImpl: &apiTimerLayer{pluginInfo.Manifest.Id, apiImpl, metrics},
},
}
executable := filepath.Clean(filepath.Join(
".",
pluginInfo.Manifest.GetExecutableForRuntime(runtime.GOOS, runtime.GOARCH),
))
if executable == "" {
return nil, fmt.Errorf("backend executable not found for environment %s/%s", runtime.GOOS, runtime.GOARCH)
}
if strings.HasPrefix(executable, "..") {
return nil, fmt.Errorf("invalid backend executable")
}
executable = filepath.Join(pluginInfo.Path, executable)
cmd := exec.Command(executable)
// This doesn't add more security than before
// but removes the SecureConfig is nil warning.
// https://mattermost.atlassian.net/browse/MM-49167
pluginChecksum, err := getPluginExecutableChecksum(executable)
if err != nil {
return nil, errors.Wrapf(err, "unable to generate plugin checksum")
}
sup.client = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshake,
Plugins: pluginMap,
Cmd: cmd,
SyncStdout: wrappedLogger.With(mlog.String("source", "plugin_stdout")).StdLogWriter(),
SyncStderr: wrappedLogger.With(mlog.String("source", "plugin_stderr")).StdLogWriter(),
Logger: hclogAdaptedLogger,
StartTimeout: time.Second * 3,
SecureConfig: &plugin.SecureConfig{
Checksum: pluginChecksum,
Hash: sha256.New(),
},
})
rpcClient, err := sup.client.Client()
if err != nil {
return nil, err
}
sup.pid = cmd.Process.Pid
raw, err := rpcClient.Dispense("hooks")
if err != nil {
return nil, err
}
c, ok := raw.(*hooksRPCClient)
if ok {
sup.hooksClient = c
}
sup.hooks = &hooksTimerLayer{pluginInfo.Manifest.Id, raw.(Hooks), metrics}
impl, err := sup.hooks.Implemented()
if err != nil {
return nil, err
}
for _, hookName := range impl {
if hookId, ok := hookNameToId[hookName]; ok {
sup.implemented[hookId] = true
}
}
return &sup, nil
}
func (sup *supervisor) Shutdown() {
sup.lock.RLock()
defer sup.lock.RUnlock()
if sup.client != nil {
sup.client.Kill()
}
// Wait for API RPC server and DB RPC server to exit.
if sup.hooksClient != nil {
sup.hooksClient.doneWg.Wait()
}
}
func (sup *supervisor) Hooks() Hooks {
sup.lock.RLock()
defer sup.lock.RUnlock()
return sup.hooks
}
// PerformHealthCheck checks the plugin through an an RPC ping.
func (sup *supervisor) PerformHealthCheck() error {
// No need for a lock here because Ping is read-locked.
if pingErr := sup.Ping(); pingErr != nil {
for pingFails := 1; pingFails < HealthCheckPingFailLimit; pingFails++ {
pingErr = sup.Ping()
if pingErr == nil {
break
}
}
if pingErr != nil {
return fmt.Errorf("plugin RPC connection is not responding")
}
}
return nil
}
// Ping checks that the RPC connection with the plugin is alive and healthy.
func (sup *supervisor) Ping() error {
sup.lock.RLock()
defer sup.lock.RUnlock()
client, err := sup.client.Client()
if err != nil {
return err
}
return client.Ping()
}
func (sup *supervisor) Implements(hookId int) bool {
sup.lock.RLock()
defer sup.lock.RUnlock()
return sup.implemented[hookId]
}
func getPluginExecutableChecksum(executablePath string) ([]byte, error) {
pathHash := sha256.New()
file, err := os.Open(executablePath)
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(pathHash, file)
if err != nil {
return nil, err
}
return pathHash.Sum(nil), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AdminSetPasswordData struct {
Password string `json:"password"`
}
func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var requestData AdminSetPasswordData
err = json.Unmarshal(requestBody, &requestData)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "adminSetPassword", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", username)
if !strings.Contains(requestData.Password, "") {
a.errorResponse(w, r, model.NewErrBadRequest("password is required"))
return
}
err = a.app.UpdateUserPassword(username, requestData.Password)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("AdminSetPassword, username: %s", mlog.String("username", username))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime/debug"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
HeaderRequestedWith = "X-Requested-With"
HeaderRequestedWithXML = "XMLHttpRequest"
UploadFormFileKey = "file"
True = "true"
ErrorNoTeamCode = 1000
ErrorNoTeamMessage = "No team"
)
var (
ErrHandlerPanic = errors.New("http handler panic")
)
// ----------------------------------------------------------------------------------------------------
// REST APIs
type API struct {
app *app.App
authService string
permissions permissions.PermissionsService
singleUserToken string
MattermostAuth bool
logger mlog.LoggerIFace
audit *audit.Audit
isPlugin bool
}
func NewAPI(
app *app.App,
singleUserToken string,
authService string,
permissions permissions.PermissionsService,
logger mlog.LoggerIFace,
audit *audit.Audit,
isPlugin bool,
) *API {
return &API{
app: app,
singleUserToken: singleUserToken,
authService: authService,
permissions: permissions,
logger: logger,
audit: audit,
isPlugin: isPlugin,
}
}
func (a *API) RegisterRoutes(r *mux.Router) {
apiv2 := r.PathPrefix("/api/v2").Subrouter()
apiv2.Use(a.panicHandler)
apiv2.Use(a.requireCSRFToken)
/* ToDo:
apiv3 := r.PathPrefix("/api/v3").Subrouter()
apiv3.Use(a.panicHandler)
apiv3.Use(a.requireCSRFToken)
*/
// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
a.registerUsersRoutes(apiv2)
a.registerAuthRoutes(apiv2)
a.registerMembersRoutes(apiv2)
a.registerCategoriesRoutes(apiv2)
a.registerSharingRoutes(apiv2)
a.registerTeamsRoutes(apiv2)
a.registerAchivesRoutes(apiv2)
a.registerSubscriptionsRoutes(apiv2)
a.registerFilesRoutes(apiv2)
a.registerLimitsRoutes(apiv2)
a.registerInsightsRoutes(apiv2)
a.registerOnboardingRoutes(apiv2)
a.registerSearchRoutes(apiv2)
a.registerConfigRoutes(apiv2)
a.registerBoardsAndBlocksRoutes(apiv2)
a.registerChannelsRoutes(apiv2)
a.registerTemplatesRoutes(apiv2)
a.registerBoardsRoutes(apiv2)
a.registerBlocksRoutes(apiv2)
a.registerContentBlocksRoutes(apiv2)
a.registerStatisticsRoutes(apiv2)
a.registerComplianceRoutes(apiv2)
// V3 routes
a.registerCardsRoutes(apiv2)
// System routes are outside the /api/v2 path
a.registerSystemRoutes(r)
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {
r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
}
func getUserID(r *http.Request) string {
ctx := r.Context()
session, ok := ctx.Value(sessionContextKey).(*model.Session)
if !ok {
return ""
}
return session.UserID
}
func (a *API) panicHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if p := recover(); p != nil {
a.logger.Error("Http handler panic",
mlog.Any("panic", p),
mlog.String("stack", string(debug.Stack())),
mlog.String("uri", r.URL.Path),
)
a.errorResponse(w, r, ErrHandlerPanic)
}
}()
next.ServeHTTP(w, r)
})
}
func (a *API) requireCSRFToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !a.checkCSRFToken(r) {
a.logger.Error("checkCSRFToken FAILED")
a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED"))
return
}
next.ServeHTTP(w, r)
})
}
func (a *API) checkCSRFToken(r *http.Request) bool {
token := r.Header.Get(HeaderRequestedWith)
return token == HeaderRequestedWithXML
}
func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
query := r.URL.Query()
readToken := query.Get("read_token")
if len(readToken) < 1 {
return false
}
isValid, err := a.app.IsValidReadToken(boardID, readToken)
if err != nil {
a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
return false
}
return isValid
}
func (a *API) userIsGuest(userID string) (bool, error) {
if a.singleUserToken != "" {
return false, nil
}
return a.app.UserIsGuest(userID)
}
// Response helpers
func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
a.logger.Error(err.Error())
errorResponse := model.ErrorResponse{Error: err.Error()}
switch {
case model.IsErrBadRequest(err):
errorResponse.ErrorCode = http.StatusBadRequest
case model.IsErrUnauthorized(err):
errorResponse.ErrorCode = http.StatusUnauthorized
case model.IsErrForbidden(err):
errorResponse.ErrorCode = http.StatusForbidden
case model.IsErrNotFound(err):
errorResponse.ErrorCode = http.StatusNotFound
case model.IsErrRequestEntityTooLarge(err):
errorResponse.ErrorCode = http.StatusRequestEntityTooLarge
case model.IsErrNotImplemented(err):
errorResponse.ErrorCode = http.StatusNotImplemented
default:
a.logger.Error("API ERROR",
mlog.Int("code", http.StatusInternalServerError),
mlog.Err(err),
mlog.String("api", r.URL.Path),
)
errorResponse.Error = "internal server error"
errorResponse.ErrorCode = http.StatusInternalServerError
}
setResponseHeader(w, "Content-Type", "application/json")
data, err := json.Marshal(errorResponse)
if err != nil {
data = []byte("{}")
}
w.WriteHeader(errorResponse.ErrorCode)
_, _ = w.Write(data)
}
func stringResponse(w http.ResponseWriter, message string) {
setResponseHeader(w, "Content-Type", "text/plain")
_, _ = fmt.Fprint(w, message)
}
func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write(json)
}
func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam
header := w.Header()
if header == nil {
return
}
header.Set(key, value)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
archiveExtension = ".boardarchive"
)
func (a *API) registerAchivesRoutes(r *mux.Router) {
// Archive APIs
r.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
r.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
}
func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/archive/export archiveExportBoard
//
// Exports an archive of all blocks for one boards.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Id of board to export
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// content:
// application-octet-stream:
// type: string
// format: binary
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
// check user has permission to board
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
// if this user has `manage_system` permission and there is a license with the compliance
// feature enabled, then we will allow the export.
license := a.app.GetLicense()
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) || license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("BoardID", boardID)
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
opts := model.ExportArchiveOptions{
TeamID: board.TeamID,
BoardIDs: []string{board.ID},
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r, err)
}
auditRec.Success()
}
func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/archive/import archiveImport
//
// Import an archive of boards.
//
// ---
// produces:
// - application/json
// consumes:
// - multipart/form-data
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: file
// in: formData
// description: archive file to import
// required: true
// type: file
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
fmt.Fprintf(w, "%v", err)
return
}
defer file.Close()
auditRec := a.makeAuditRecord(r, "import", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("filename", handle.Filename)
auditRec.AddMeta("size", handle.Size)
opt := model.ImportArchiveOptions{
TeamID: teamID,
ModifiedBy: userID,
}
if err := a.app.ImportArchive(file, opt); err != nil {
a.logger.Debug("Error importing archive",
mlog.String("team_id", teamID),
mlog.Err(err),
)
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/archive/export archiveExportTeam
//
// Exports an archive of all blocks for all the boards in a team.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Id of team
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// content:
// application-octet-stream:
// type: string
// format: binary
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("TeamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
ids := []string{}
for _, board := range boards {
ids = append(ids, board.ID)
}
opts := model.ExportArchiveOptions{
TeamID: teamID,
BoardIDs: ids,
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r, err)
}
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
// makeAuditRecord creates an audit record pre-populated with data from the request.
func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus string) *audit.Record { //nolint:unparam
ctx := r.Context()
var sessionID string
var userID string
if session, ok := ctx.Value(sessionContextKey).(*model.Session); ok {
sessionID = session.ID
userID = session.UserID
}
teamID := "unknown"
rec := &audit.Record{
APIPath: r.URL.Path,
Event: event,
Status: initialStatus,
UserID: userID,
SessionID: sessionID,
Client: r.UserAgent(),
IPAddress: r.RemoteAddr,
Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}},
}
return rec
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/services/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerAuthRoutes(r *mux.Router) {
// personal-server specific routes. These are not needed in plugin mode.
if !a.isPlugin {
r.HandleFunc("/login", a.handleLogin).Methods("POST")
r.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
r.HandleFunc("/register", a.handleRegister).Methods("POST")
r.HandleFunc("/teams/{teamID}/regenerate_signup_token", a.sessionRequired(a.handlePostTeamRegenerateSignupToken)).Methods("POST")
r.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST")
}
}
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /login login
//
// Login user
//
// ---
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// description: Login request
// required: true
// schema:
// "$ref": "#/definitions/LoginRequest"
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/LoginResponse"
// '401':
// description: invalid login
// schema:
// "$ref": "#/definitions/ErrorResponse"
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var loginData model.LoginRequest
err = json.Unmarshal(requestBody, &loginData)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "login", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", loginData.Username)
auditRec.AddMeta("type", loginData.Type)
if loginData.Type == "normal" {
token, err := a.app.Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken)
if err != nil {
a.errorResponse(w, r, model.NewErrUnauthorized("incorrect login"))
return
}
json, err := json.Marshal(model.LoginResponse{Token: token})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.Success()
return
}
a.errorResponse(w, r, model.NewErrBadRequest("invalid login type"))
}
func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /logout logout
//
// Logout user
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "logout", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("userID", session.UserID)
if err := a.app.Logout(session.ID); err != nil {
a.errorResponse(w, r, model.NewErrUnauthorized("incorrect logout"))
return
}
auditRec.AddMeta("sessionID", session.ID)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /register register
//
// Register new user
//
// ---
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// description: Register request
// required: true
// schema:
// "$ref": "#/definitions/RegisterRequest"
// responses:
// '200':
// description: success
// '401':
// description: invalid registration token
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var registerData model.RegisterRequest
err = json.Unmarshal(requestBody, ®isterData)
if err != nil {
a.errorResponse(w, r, err)
return
}
registerData.Email = strings.TrimSpace(registerData.Email)
registerData.Username = strings.TrimSpace(registerData.Username)
// Validate token
if registerData.Token != "" {
team, err2 := a.app.GetRootTeam()
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if registerData.Token != team.SignupToken {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid token"))
return
}
} else {
// No signup token, check if no active users
userCount, err2 := a.app.GetRegisteredUserCount()
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if userCount > 0 {
a.errorResponse(w, r, model.NewErrUnauthorized("no sign-up token and user(s) already exist"))
return
}
}
if err = registerData.IsValid(); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "register", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", registerData.Username)
err = a.app.RegisterUser(registerData.Username, registerData.Email, registerData.Password)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /users/{userID}/changepassword changePassword
//
// Change a user's password
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: body
// in: body
// description: Change password request
// required: true
// schema:
// "$ref": "#/definitions/ChangePasswordRequest"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '400':
// description: invalid request
// schema:
// "$ref": "#/definitions/ErrorResponse"
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
vars := mux.Vars(r)
userID := vars["userID"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var requestData model.ChangePasswordRequest
if err = json.Unmarshal(requestBody, &requestData); err != nil {
a.errorResponse(w, r, err)
return
}
if err = requestData.IsValid(); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "changePassword", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
if err = a.app.ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return a.attachSession(handler, true)
}
func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request), required bool) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
token, _ := auth.ParseAuthTokenFromRequest(r)
a.logger.Debug(`attachSession`, mlog.Bool("single_user", a.singleUserToken != ""))
if a.singleUserToken != "" {
if required && (token != a.singleUserToken) {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid single user token"))
return
}
now := utils.GetMillis()
session := &model.Session{
ID: model.SingleUser,
Token: token,
UserID: model.SingleUser,
AuthService: a.authService,
Props: map[string]interface{}{},
CreateAt: now,
UpdateAt: now,
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
return
}
if a.MattermostAuth && r.Header.Get("Mattermost-User-Id") != "" {
userID := r.Header.Get("Mattermost-User-Id")
now := utils.GetMillis()
session := &model.Session{
ID: userID,
Token: userID,
UserID: userID,
AuthService: a.authService,
Props: map[string]interface{}{},
CreateAt: now,
UpdateAt: now,
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
return
}
session, err := a.app.GetSession(token)
if err != nil {
if required {
a.errorResponse(w, r, model.NewErrUnauthorized(err.Error()))
return
}
handler(w, r)
return
}
authService := session.AuthService
if authService != a.authService {
msg := `Session authService mismatch`
a.logger.Error(msg,
mlog.String("sessionID", session.ID),
mlog.String("want", a.authService),
mlog.String("got", authService),
)
a.errorResponse(w, r, model.NewErrUnauthorized(msg))
return
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
}
}
func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Currently, admin APIs require local unix connections
conn := GetContextConn(r)
if _, isUnix := conn.(*net.UnixConn); !isUnix {
a.errorResponse(w, r, model.NewErrUnauthorized("not a local unix connection"))
return
}
handler(w, r)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBlocksRoutes(r *mux.Router) {
// Blocks APIs
r.HandleFunc("/boards/{boardID}/blocks", a.attachSession(a.handleGetBlocks, false)).Methods("GET")
r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST")
r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST")
}
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/blocks getBlocks
//
// Returns blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: parent_id
// in: query
// description: ID of parent block, omit to specify all blocks
// required: false
// type: string
// - name: type
// in: query
// description: Type of blocks to return, omit to specify all types
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
parentID := query.Get("parent_id")
blockType := query.Get("type")
all := query.Get("all")
blockID := query.Get("block_id")
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !hasValidReadToken {
if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
if board.IsTemplate {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("guest are not allowed to get board templates"))
return
}
}
}
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("parentID", parentID)
auditRec.AddMeta("blockType", blockType)
auditRec.AddMeta("all", all)
auditRec.AddMeta("blockID", blockID)
var blocks []*model.Block
var block *model.Block
switch {
case all != "":
blocks, err = a.app.GetBlocksForBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
case blockID != "":
block, err = a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
blocks = append(blocks, block)
default:
blocks, err = a.app.GetBlocks(boardID, parentID, blockType)
if err != nil {
a.errorResponse(w, r, err)
return
}
}
a.logger.Debug("GetBlocks",
mlog.String("boardID", boardID),
mlog.String("parentID", parentID),
mlog.String("blockType", blockType),
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
var bErr error
blocks, bErr = a.app.ApplyCloudLimits(blocks)
if bErr != nil {
a.errorResponse(w, r, err)
return
}
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks updateBlocks
//
// Insert blocks. The specified IDs will only be used to link
// blocks with existing ones, the rest will be replaced by server
// generated IDs
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk inserting)
// required: false
// type: bool
// - name: Body
// in: body
// description: array of blocks to insert or update
// required: true
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// items:
// $ref: '#/definitions/Block'
// type: array
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var blocks []*model.Block
err = json.Unmarshal(requestBody, &blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
hasComments := false
hasContents := false
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.Type == model.TypeComment {
hasComments = true
} else {
hasContents = true
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("invalid BoardID for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
}
if hasContents {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
}
if hasComments {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to post card comments"))
return
}
}
blocks = model.GenerateBlockIDs(blocks, a.logger)
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("disable_notify", disableNotify)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
model.StampModificationMetadata(userID, blocks, auditRec)
// this query param exists when creating template from board, or board from template
sourceBoardID := r.URL.Query().Get("sourceBoardID")
if sourceBoardID != "" {
if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil {
a.errorResponse(w, r, updateFileIDsErr)
return
}
}
newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("POST Blocks",
mlog.Int("block_count", len(blocks)),
mlog.Bool("disable_notify", disableNotify),
)
json, err := json.Marshal(newBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID}/blocks/{blockID} deleteBlock
//
// Deletes a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to delete
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk deletion)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
boardID := vars["boardID"]
blockID := vars["blockID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks/{blockID}/undelete undeleteBlock
//
// Undeletes a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to undelete
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BlockPatch"
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
blockID := vars["blockID"]
boardID := vars["boardID"]
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
block, err := a.app.GetLastBlockHistoryEntry(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board.ID != block.BoardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
undeletedBlock, err := a.app.UndeleteBlock(blockID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
undeletedBlockData, err := json.Marshal(undeletedBlock)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
jsonBytesResponse(w, http.StatusOK, undeletedBlockData)
auditRec.Success()
}
func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID}/blocks/{blockID} patchBlock
//
// Partially updates a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to patch
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk patching)
// required: false
// type: bool
// - name: Body
// in: body
// description: block patch to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
boardID := vars["boardID"]
blockID := vars["blockID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.BlockPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
if _, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID}/blocks/ patchBlocks
//
// Partially updates batch of blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk patching)
// required: false
// type: bool
// - name: Body
// in: body
// description: block Ids and block patches to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatchBatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patches *model.BlockPatchBatch
err = json.Unmarshal(requestBody, &patches)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
for i := range patches.BlockIDs {
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i])
}
for _, blockID := range patches.BlockIDs {
var block *model.Block
block, err = a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, model.NewErrForbidden("access denied to make board changes"))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changesa"))
return
}
}
err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH Blocks", mlog.String("patches", strconv.Itoa(len(patches.BlockIDs))))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks/{blockID}/duplicate duplicateBlock
//
// Returns the new created blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board or block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
blockID := mux.Vars(r)["blockID"]
userID := getUserID(r)
query := r.URL.Query()
asTemplate := query.Get("asTemplate")
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board.ID != block.BoardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
if block.Type == model.TypeComment {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to comment on board cards"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards"))
return
}
}
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
a.logger.Debug("DuplicateBlock",
mlog.String("boardID", boardID),
mlog.String("blockID", blockID),
)
blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBoardsRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET")
r.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET")
r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/undelete", a.sessionRequired(a.handleUndeleteBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
}
func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards getBoards
//
// Returns team boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards createBoard
//
// Creates a new board
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the board to create
// required: true
// schema:
// "$ref": "#/definitions/Board"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Board'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBoard *model.Board
if err = json.Unmarshal(requestBody, &newBoard); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if newBoard.Type == model.BoardTypeOpen {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create public boards"))
return
}
} else {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create private boards"))
return
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
if err = newBoard.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("teamID", newBoard.TeamID)
auditRec.AddMeta("boardType", newBoard.Type)
// create board
board, err := a.app.CreateBoard(newBoard, userID, true)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateBoard",
mlog.String("teamID", board.TeamID),
mlog.String("boardID", board.ID),
mlog.String("boardType", string(board.Type)),
mlog.String("userID", userID),
)
data, err := json.Marshal(board)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID} getBoard
//
// Returns a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Board"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !hasValidReadToken {
if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
}
auditRec := a.makeAuditRecord(r, "getBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
a.logger.Debug("GetBoard",
mlog.String("boardID", boardID),
)
data, err := json.Marshal(board)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID} patchBoard
//
// Partially updates a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: board patch to apply
// required: true
// schema:
// "$ref": "#/definitions/BoardPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Board'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.BoardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if err = patch.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return
}
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return
}
}
if patch.ChannelID != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board access"))
return
}
}
auditRec := a.makeAuditRecord(r, "patchBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("userID", userID)
// patch board
updatedBoard, err := a.app.PatchBoard(patch, boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchBoard",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
)
data, err := json.Marshal(updatedBoard)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID} deleteBoard
//
// Removes a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
// Check if board exists
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return
}
auditRec := a.makeAuditRecord(r, "deleteBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if err := a.app.DeleteBoard(boardID, userID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE Board", mlog.String("boardID", boardID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/duplicate duplicateBoard
//
// Returns the new created board and all the blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
query := r.URL.Query()
asTemplate := query.Get("asTemplate")
toTeam := query.Get("toTeam")
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if toTeam == "" {
toTeam = board.TeamID
}
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
a.logger.Debug("DuplicateBoard",
mlog.String("boardID", boardID),
)
boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsAndBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleUndeleteBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/undelete undeleteBoard
//
// Undeletes a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: ID of board to undelete
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
boardID := vars["boardID"]
auditRec := a.makeAuditRecord(r, "undeleteBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to undelete board"))
return
}
err := a.app.UndeleteBoard(boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("UNDELETE Board", mlog.String("boardID", boardID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/metadata getBoardMetadata
//
// Returns a board's metadata
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardMetadata"
// '404':
// description: board not found
// '501':
// description: required license not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
board, boardMetadata, err := a.app.GetBoardMetadata(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board == nil || boardMetadata == nil {
a.errorResponse(w, r, model.NewErrNotFound("board metadata BoardID="+boardID))
return
}
if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
auditRec := a.makeAuditRecord(r, "getBoardMetadata", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
data, err := json.Marshal(boardMetadata)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBoardsAndBlocksRoutes(r *mux.Router) {
// BoardsAndBlocks APIs
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleCreateBoardsAndBlocks)).Methods("POST")
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handlePatchBoardsAndBlocks)).Methods("PATCH")
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleDeleteBoardsAndBlocks)).Methods("DELETE")
}
func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards-and-blocks insertBoardsAndBlocks
//
// Creates new boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the boards and blocks to create
// required: true
// schema:
// "$ref": "#/definitions/BoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBab *model.BoardsAndBlocks
if err = json.Unmarshal(requestBody, &newBab); err != nil {
a.errorResponse(w, r, err)
return
}
if len(newBab.Boards) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("at least one board is required"))
return
}
teamID := ""
boardIDs := map[string]bool{}
for _, board := range newBab.Boards {
boardIDs[board.ID] = true
if teamID == "" {
teamID = board.TeamID
continue
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("cannot create boards for multiple teams"))
return
}
if board.ID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("boards need an ID to be referenced from the blocks"))
return
}
}
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
for _, block := range newBab.Blocks {
// Error checking
if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !boardIDs[block.BoardID] {
message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
}
// IDs of boards and blocks are used to confirm that they're
// linked and then regenerated by the server
newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("userID", userID)
auditRec.AddMeta("boardsCount", len(newBab.Boards))
auditRec.AddMeta("blocksCount", len(newBab.Blocks))
// create boards and blocks
bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateBoardsAndBlocks",
mlog.String("teamID", teamID),
mlog.String("userID", userID),
mlog.Int("boardCount", len(bab.Boards)),
mlog.Int("blockCount", len(bab.Blocks)),
)
data, err := json.Marshal(bab)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards-and-blocks patchBoardsAndBlocks
//
// Patches a set of related boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the patches for the boards and blocks
// required: true
// schema:
// "$ref": "#/definitions/PatchBoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var pbab *model.PatchBoardsAndBlocks
if err = json.Unmarshal(requestBody, &pbab); err != nil {
a.errorResponse(w, r, err)
return
}
if err = pbab.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
teamID := ""
boardIDMap := map[string]bool{}
for i, boardID := range pbab.BoardIDs {
boardIDMap[boardID] = true
patch := pbab.BoardPatches[i]
if err = patch.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return
}
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return
}
}
board, err2 := a.app.GetBoard(boardID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("mismatched team ID"))
return
}
}
for _, blockID := range pbab.BlockIDs {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return
}
}
auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardsCount", len(pbab.BoardIDs))
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH BoardsAndBlocks",
mlog.Int("boardsCount", len(pbab.BoardIDs)),
mlog.Int("blocksCount", len(pbab.BlockIDs)),
)
data, err := json.Marshal(bab)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards-and-blocks deleteBoardsAndBlocks
//
// Deletes boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the boards and blocks to delete
// required: true
// schema:
// "$ref": "#/definitions/DeleteBoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var dbab *model.DeleteBoardsAndBlocks
if err = json.Unmarshal(requestBody, &dbab); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
// user must have permission to delete all the boards, and that
// would include the permission to manage their blocks
teamID := ""
boardIDMap := map[string]bool{}
for _, boardID := range dbab.Boards {
boardIDMap[boardID] = true
// all boards in the request should belong to the same team
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("all boards should belong to the same team"))
return
}
// permission check
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return
}
}
for _, blockID := range dbab.Blocks {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return
}
}
if err := dbab.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "deleteBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardsCount", len(dbab.Boards))
auditRec.AddMeta("blocksCount", len(dbab.Blocks))
if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE BoardsAndBlocks",
mlog.Int("boardsCount", len(dbab.Boards)),
mlog.Int("blocksCount", len(dbab.Blocks)),
)
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defaultPage = "0"
defaultPerPage = "100"
)
func (a *API) registerCardsRoutes(r *mux.Router) {
// Cards APIs
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleCreateCard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleGetCards)).Methods("GET")
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handlePatchCard)).Methods("PATCH")
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handleGetCard)).Methods("GET")
}
func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/cards createCard
//
// Creates a new card for the specified board.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: the card to create
// required: true
// schema:
// "$ref": "#/definitions/Card"
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk data inserting)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
boardID := mux.Vars(r)["boardID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newCard *model.Card
if err = json.Unmarshal(requestBody, &newCard); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create card"))
return
}
if newCard.BoardID != "" && newCard.BoardID != boardID {
a.errorResponse(w, r, model.ErrBoardIDMismatch)
return
}
newCard.PopulateWithBoardID(boardID)
if err = newCard.CheckValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
// create card
card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateCard",
mlog.String("boardID", boardID),
mlog.String("cardID", card.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(card)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/cards getCards
//
// Fetches cards for the specified board.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of cards to return per page(default=100)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Card"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
boardID := mux.Vars(r)["boardID"]
query := r.URL.Query()
strPage := query.Get("page")
strPerPage := query.Get("per_page")
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to fetch cards"))
return
}
if strPage == "" {
strPage = defaultPage
}
if strPerPage == "" {
strPerPage = defaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
}
auditRec := a.makeAuditRecord(r, "getCards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("page", page)
auditRec.AddMeta("per_page", perPage)
cards, err := a.app.GetCardsForBoard(boardID, page, perPage)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetCards",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
mlog.Int("page", page),
mlog.Int("per_page", perPage),
mlog.Int("count", len(cards)),
)
data, err := json.Marshal(cards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /cards/{cardID}/cards patchCard
//
// Patches the specified card.
//
// ---
// produces:
// - application/json
// parameters:
// - name: cardID
// in: path
// description: Card ID
// required: true
// type: string
// - name: Body
// in: body
// description: the card patch
// required: true
// schema:
// "$ref": "#/definitions/CardPatch"
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk data patching)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
cardID := mux.Vars(r)["cardID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
card, err := a.app.GetCardByID(cardID)
if err != nil {
message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to patch card"))
return
}
var patch *model.CardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "patchCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", card.BoardID)
auditRec.AddMeta("cardID", card.ID)
// patch card
cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchCard",
mlog.String("boardID", cardPatched.BoardID),
mlog.String("cardID", cardPatched.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(cardPatched)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /cards/{cardID} getCard
//
// Fetches the specified card.
//
// ---
// produces:
// - application/json
// parameters:
// - name: cardID
// in: path
// description: Card ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
cardID := mux.Vars(r)["cardID"]
card, err := a.app.GetCardByID(cardID)
if err != nil {
message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to fetch card"))
return
}
auditRec := a.makeAuditRecord(r, "getCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", card.BoardID)
auditRec.AddMeta("cardID", card.ID)
a.logger.Debug("GetCard",
mlog.String("boardID", card.BoardID),
mlog.String("cardID", card.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(card)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
func (a *API) registerCategoriesRoutes(r *mux.Router) {
// Category APIs
r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleCreateCategory)).Methods(http.MethodPost)
r.HandleFunc("/teams/{teamID}/categories/reorder", a.sessionRequired(a.handleReorderCategories)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleUpdateCategory)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleDeleteCategory)).Methods(http.MethodDelete)
r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBoards)).Methods(http.MethodGet)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/reorder", a.sessionRequired(a.handleReorderCategoryBoards)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}", a.sessionRequired(a.handleUpdateCategoryBoard)).Methods(http.MethodPost)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide", a.sessionRequired(a.handleHideBoard)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/unhide", a.sessionRequired(a.handleUnhideBoard)).Methods(http.MethodPut)
}
func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories createCategory
//
// Create a category for boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: Body
// in: body
// description: category to create
// required: true
// schema:
// "$ref": "#/definitions/Category"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var category model.Category
err = json.Unmarshal(requestBody, &category)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "createCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
// user can only create category for themselves
if category.UserID != session.UserID {
message := fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
if category.TeamID != teamID {
a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
return
}
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
createdCategory, err := a.app.CreateCategory(&category)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(createdCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("categoryID", createdCategory.ID)
auditRec.Success()
}
func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/{categoryID} updateCategory
//
// Create a category for boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// - name: Body
// in: body
// description: category to update
// required: true
// schema:
// "$ref": "#/definitions/Category"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
categoryID := vars["categoryID"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var category model.Category
err = json.Unmarshal(requestBody, &category)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "updateCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if categoryID != category.ID {
a.errorResponse(w, r, model.NewErrBadRequest("categoryID mismatch in patch and body"))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
// user can only update category for themselves
if category.UserID != session.UserID {
a.errorResponse(w, r, model.NewErrBadRequest("user ID mismatch in session and category"))
return
}
teamID := vars["teamID"]
if category.TeamID != teamID {
a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
return
}
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
updatedCategory, err := a.app.UpdateCategory(&category)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /teams/{teamID}/categories/{categoryID} deleteCategory
//
// Delete a category
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
userID := session.UserID
teamID := vars["teamID"]
categoryID := vars["categoryID"]
auditRec := a.makeAuditRecord(r, "deleteCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(deletedCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetUserCategoryBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/categories getUserCategoryBoards
//
// Gets the user's board categories
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// items:
// "$ref": "#/definitions/CategoryBoards"
// type: array
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
auditRec := a.makeAuditRecord(r, "getUserCategoryBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(categoryBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID} updateCategoryBoard
//
// Set the category of a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
auditRec := a.makeAuditRecord(r, "updateCategoryBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
vars := mux.Vars(r)
categoryID := vars["categoryID"]
boardID := vars["boardID"]
teamID := vars["teamID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
// TODO: Check the category and the team matches
err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, []string{boardID})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, []byte("ok"))
auditRec.Success()
}
func (a *API) handleReorderCategories(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/reorder handleReorderCategories
//
// Updated sidebar category order
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newCategoryOrder []string
err = json.Unmarshal(requestBody, &newCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "reorderCategories", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("TeamID", teamID)
auditRec.AddMeta("CategoryCount", len(newCategoryOrder))
updatedCategoryOrder, err := a.app.ReorderCategories(userID, teamID, newCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleReorderCategoryBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/{categoryID}/boards/reorder handleReorderCategoryBoards
//
// Updates order of boards inside a sidebar category
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
categoryID := vars["categoryID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
category, err := a.app.GetCategory(categoryID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if category.UserID != userID {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBoardsOrder []string
err = json.Unmarshal(requestBody, &newBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "reorderCategoryBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
updatedBoardsOrder, err := a.app.ReorderCategoryBoards(userID, teamID, categoryID, newBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleHideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide hideBoard
//
// Hide the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be hidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be hidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "hideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("board_id", boardID)
auditRec.AddMeta("team_id", teamID)
auditRec.AddMeta("category_id", categoryID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, false); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUnhideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide unhideBoard
//
// Unhides the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be unhidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be unhidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "unhideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, true); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerChannelsRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/channels/{channelID}", a.sessionRequired(a.handleGetChannel)).Methods("GET")
}
func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/channels/{channelID} getChannel
//
// Returns the requested channel
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: channelID
// in: path
// description: Channel ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Channel"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
teamID := mux.Vars(r)["teamID"]
channelID := mux.Vars(r)["channelID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to channel"))
return
}
auditRec := a.makeAuditRecord(r, "getChannel", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("channelID", teamID)
channel, err := a.app.GetChannel(teamID, channelID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetChannel",
mlog.String("teamID", teamID),
mlog.String("channelID", channelID),
)
if channel.TeamId != teamID {
if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup {
message := fmt.Sprintf("channel ID=%s on TeamID=%s", channel.Id, teamID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
}
data, err := json.Marshal(channel)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
complianceDefaultPage = "0"
complianceDefaultPerPage = "60"
)
func (a *API) registerComplianceRoutes(r *mux.Router) {
// Compliance APIs
r.HandleFunc("/admin/boards", a.sessionRequired(a.handleGetBoardsForCompliance)).Methods("GET")
r.HandleFunc("/admin/boards_history", a.sessionRequired(a.handleGetBoardsComplianceHistory)).Methods("GET")
r.HandleFunc("/admin/blocks_history", a.sessionRequired(a.handleGetBlocksComplianceHistory)).Methods("GET")
}
func (a *API) handleGetBoardsForCompliance(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards getBoardsForCompliance
//
// Returns boards for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: team_id
// in: query
// description: Team ID. If empty then boards across all teams are included.
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of boards to return per page(default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
teamID := query.Get("team_id")
strPage := query.Get("page")
strPerPage := query.Get("per_page")
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getAllBoards"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getAllBoards"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsForComplianceOptions{
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsForCompliance(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsForCompliance",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBoardsComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards_history getBoardsComplianceHistory
//
// Returns boards histories for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then board histories across all teams are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of board histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBoardsHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBoardsHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsComplianceHistory",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceHistoryResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBlocksComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/blocks_history getBlocksComplianceHistory
//
// Returns block histories for a specific team, specific board, or all teams and boards.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then block histories across all teams are included
// required: false
// type: string
// - name: board_id
// in: query
// description: Board ID. If empty then block histories for all boards are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of block histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BlocksComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
boardID := query.Get("board_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBlocksHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBlocksHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
// check for valid board if specified
if boardID != "" {
_, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid board id: "+boardID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBlocksComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
BoardID: boardID,
Page: page,
PerPage: perPage,
}
blocks, more, err := a.app.GetBlocksComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBlocksComplianceHistory",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.Int("blocksCount", len(blocks)),
mlog.Bool("hasNext", more),
)
response := model.BlocksComplianceHistoryResponse{
HasNext: more,
Results: blocks,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func (a *API) registerConfigRoutes(r *mux.Router) {
// Config APIs
r.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET")
}
func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /clientConfig getClientConfig
//
// Returns the client configuration
//
// ---
// produces:
// - application/json
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/ClientConfig"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
clientConfig := a.app.GetClientConfig()
configData, err := json.Marshal(clientConfig)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, configData)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
func (a *API) registerContentBlocksRoutes(r *mux.Router) {
// Blocks APIs
r.HandleFunc("/content-blocks/{blockID}/moveto/{where}/{dstBlockID}", a.sessionRequired(a.handleMoveBlockTo)).Methods("POST")
}
func (a *API) handleMoveBlockTo(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /content-blocks/{blockID}/move/{where}/{dstBlockID} moveBlockTo
//
// Move a block after another block in the parent card
//
// ---
// produces:
// - application/json
// parameters:
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// - name: where
// in: path
// description: Relative location respect destination block (after or before)
// required: true
// type: string
// - name: dstBlockID
// in: path
// description: Destination Block ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board or block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
blockID := mux.Vars(r)["blockID"]
dstBlockID := mux.Vars(r)["dstBlockID"]
where := mux.Vars(r)["where"]
userID := getUserID(r)
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
dstBlock, err := a.app.GetBlockByID(dstBlockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if where != "after" && where != "before" {
a.errorResponse(w, r, model.NewErrBadRequest("invalid where parameter, use before or after"))
return
}
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards"))
return
}
auditRec := a.makeAuditRecord(r, "moveBlockTo", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
auditRec.AddMeta("dstBlockID", dstBlockID)
err = a.app.MoveContentBlock(block, dstBlock, where, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"net"
"net/http"
)
type contextKey int
const (
httpConnContextKey contextKey = iota
sessionContextKey
)
// SetContextConn stores the connection in the request context.
func SetContextConn(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, httpConnContextKey, c)
}
// GetContextConn gets the stored connection from the request context.
func GetContextConn(r *http.Request) net.Conn {
value := r.Context().Value(httpConnContextKey)
if value == nil {
return nil
}
return value.(net.Conn)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
// The FileID to retrieve the uploaded file
// required: true
FileID string `json:"fileId"`
}
func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) {
var fileUploadResponse FileUploadResponse
if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil {
return nil, err
}
return &fileUploadResponse, nil
}
func FileInfoResponseFromJSON(data io.Reader) (*mm_model.FileInfo, error) {
var fileInfo mm_model.FileInfo
if err := json.NewDecoder(data).Decode(&fileInfo); err != nil {
return nil, err
}
return &fileInfo, nil
}
func (a *API) registerFilesRoutes(r *mux.Router) {
// Files API
r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}/info", a.attachSession(a.getFileInfo, false)).Methods("GET")
r.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
}
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile
//
// Returns the contents of an uploaded file
//
// ---
// produces:
// - application/json
// - image/jpg
// - image/png
// - image/gif
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: filename
// in: path
// description: name of the file
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: file not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
filename := vars["filename"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", filename)
fileInfo, fileReader, err := a.app.GetFile(board.TeamID, boardID, filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
return
}
if errors.Is(err, app.ErrFileNotFound) && board.ChannelID != "" {
// prior to moving from workspaces to teams, the filepath was constructed from
// workspaceID, which is the channel ID in plugin mode.
// If a file is not found from team ID as we tried above, try looking for it via
// channel ID.
fileReader, err = a.app.GetFileReader(board.ChannelID, boardID, filename)
if err != nil {
a.errorResponse(w, r, err)
return
}
// move file to team location
// nothing to do if there is an error
_ = a.app.MoveFile(board.ChannelID, board.TeamID, boardID, filename)
}
defer fileReader.Close()
mimeType := ""
var fileSize int64
if fileInfo != nil {
mimeType = fileInfo.MimeType
fileSize = fileInfo.Size
}
web.WriteFileResponse(filename, mimeType, fileSize, time.Now(), "", fileReader, false, w, r)
auditRec.Success()
}
func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile
//
// Returns the metadata of an uploaded file
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: filename
// in: path
// description: name of the file
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: file not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
teamID := vars["teamID"]
filename := vars["filename"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("filename", filename)
fileInfo, err := a.app.GetFileInfo(filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(fileInfo)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/boards/{boardID}/files uploadFile
//
// Upload a binary file, attached to a root block
//
// ---
// consumes:
// - multipart/form-data
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: ID of the team
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: uploaded file
// in: formData
// type: file
// description: The file to upload
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/FileUploadResponse"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if a.app.GetConfig().MaxFileSize > 0 {
r.Body = http.MaxBytesReader(w, r.Body, a.app.GetConfig().MaxFileSize)
}
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
if strings.HasSuffix(err.Error(), "http: request body too large") {
a.errorResponse(w, r, model.ErrRequestEntityTooLarge)
return
}
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
defer file.Close()
auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", handle.Filename)
fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("uploadFile",
mlog.String("filename", handle.Filename),
mlog.String("fileID", fileID),
)
data, err := json.Marshal(FileUploadResponse{FileID: fileID})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("fileID", fileID)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
func (a *API) registerInsightsRoutes(r *mux.Router) {
// Insights APIs
r.HandleFunc("/teams/{teamID}/boards/insights", a.sessionRequired(a.handleTeamBoardsInsights)).Methods("GET")
r.HandleFunc("/users/me/boards/insights", a.sessionRequired(a.handleUserBoardsInsights)).Methods("GET")
}
func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/insights handleTeamBoardsInsights
//
// Returns team boards insights
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: time_range
// in: query
// description: duration of data to calculate insights for
// required: true
// type: string
// - name: page
// in: query
// description: page offset for top boards
// required: true
// type: string
// - name: per_page
// in: query
// description: limit for boards in a page.
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardInsight"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
query := r.URL.Query()
timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getTeamBoardsInsights", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
page, err := strconv.Atoi(query.Get("page"))
if err != nil {
message := fmt.Sprintf("error converting page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if page < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if perPage < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil {
message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
userLocation, _ := time.LoadLocation(userTimezone)
if userLocation == nil {
userLocation = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation)
if appErr != nil {
a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message))
return
}
boardsInsights, err := a.app.GetTeamBoardsInsights(userID, teamID, &mm_model.InsightsOpts{
StartUnixMilli: mm_model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsInsights)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("teamBoardsInsightCount", len(boardsInsights.Items))
auditRec.Success()
}
func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/boards/insights getUserBoardsInsights
//
// Returns user boards insights
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: time_range
// in: query
// description: duration of data to calculate insights for
// required: true
// type: string
// - name: page
// in: query
// description: page offset for top boards
// required: true
// type: string
// - name: per_page
// in: query
// description: limit for boards in a page.
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardInsight"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
userID := getUserID(r)
query := r.URL.Query()
teamID := query.Get("team_id")
timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getUserBoardsInsights", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
page, err := strconv.Atoi(query.Get("page"))
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("error converting page parameter to integer"))
return
}
if page < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if perPage < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil {
message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
userLocation, _ := time.LoadLocation(userTimezone)
if userLocation == nil {
userLocation = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation)
if appErr != nil {
a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message))
return
}
boardsInsights, err := a.app.GetUserBoardsInsights(userID, teamID, &mm_model.InsightsOpts{
StartUnixMilli: mm_model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsInsights)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("userBoardInsightCount", len(boardsInsights.Items))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerLimitsRoutes(r *mux.Router) {
// limits
r.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET")
r.HandleFunc("/teams/{teamID}/notifyadminupgrade", a.sessionRequired(a.handleNotifyAdminUpgrade)).Methods(http.MethodPost)
}
func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /limits cloudLimits
//
// Fetches the cloud limits of the server.
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardsCloudLimits"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardsCloudLimits, err := a.app.GetBoardsCloudLimits()
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsCloudLimits)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleNotifyAdminUpgrade(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v2/teams/{teamID}/notifyadminupgrade handleNotifyAdminUpgrade
//
// Notifies admins for upgrade request.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
if err := a.app.NotifyPortalAdminsUpgradeRequest(teamID); err != nil {
jsonStringResponse(w, http.StatusOK, "{}")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerMembersRoutes(r *mux.Router) {
// Member APIs
r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET")
r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST")
}
func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/members getMembersForBoard
//
// Returns the members of the board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardMember"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board members"))
return
}
auditRec := a.makeAuditRecord(r, "getMembersForBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
members, err := a.app.GetMembersForBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetMembersForBoard",
mlog.String("boardID", boardID),
mlog.Int("membersCount", len(members)),
)
data, err := json.Marshal(members)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/members addMember
//
// Adds a new member to a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: membership to replace the current one with
// required: true
// schema:
// "$ref": "#/definitions/BoardMember"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) &&
!(board.Type == model.BoardTypeOpen && a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties)) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if reqBoardMember.UserID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("empty userID"))
return
}
if !a.permissions.HasPermissionToTeam(reqBoardMember.UserID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
newBoardMember := &model.BoardMember{
UserID: reqBoardMember.UserID,
BoardID: boardID,
SchemeEditor: reqBoardMember.SchemeEditor,
SchemeAdmin: reqBoardMember.SchemeAdmin,
SchemeViewer: reqBoardMember.SchemeViewer,
SchemeCommenter: reqBoardMember.SchemeCommenter,
}
auditRec := a.makeAuditRecord(r, "addMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", reqBoardMember.UserID)
member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("AddMember",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", reqBoardMember.UserID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/join joinBoard
//
// Become a member of a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: allow_admin
// in: path
// description: allows admin users to join private boards
// required: false
// type: boolean
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// '404':
// description: board not found
// '403':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
allowAdmin := query.Has("allow_admin")
userID := getUserID(r)
if userID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("missing user ID"))
return
}
boardID := mux.Vars(r)["boardID"]
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
isAdmin := false
if board.Type != model.BoardTypeOpen {
if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
return
}
isAdmin = true
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("guests not allowed to join boards"))
return
}
newBoardMember := &model.BoardMember{
UserID: userID,
BoardID: boardID,
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin,
SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor,
SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter,
SchemeViewer: board.MinimumRole == model.BoardRoleViewer,
}
auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", userID)
member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("JoinBoard",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", userID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/leave leaveBoard
//
// Remove your own membership from a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// '403':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
if userID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("invalid session"))
return
}
boardID := mux.Vars(r)["boardID"]
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "leaveBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", userID)
err = a.app.DeleteBoardMember(boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("LeaveBoard",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", userID),
)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
//
// Updates a board member
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: Body
// in: body
// description: membership to replace the current one with
// required: true
// schema:
// "$ref": "#/definitions/BoardMember"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
newBoardMember := &model.BoardMember{
UserID: paramsUserID,
BoardID: boardID,
SchemeAdmin: reqBoardMember.SchemeAdmin,
SchemeEditor: reqBoardMember.SchemeEditor,
SchemeCommenter: reqBoardMember.SchemeCommenter,
SchemeViewer: reqBoardMember.SchemeViewer,
}
isGuest, err := a.userIsGuest(paramsUserID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
newBoardMember.SchemeAdmin = false
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "patchMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("patchedUserID", paramsUserID)
member, err := a.app.UpdateBoardMember(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchMember",
mlog.String("boardID", boardID),
mlog.String("patchedUserID", paramsUserID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID}/members/{userID} deleteMember
//
// Deletes a member from a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r)
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", paramsUserID)
deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID)
if deleteErr != nil {
a.errorResponse(w, r, deleteErr)
return
}
a.logger.Debug("DeleteMember",
mlog.String("boardID", boardID),
mlog.String("addedUserID", paramsUserID),
)
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerOnboardingRoutes(r *mux.Router) {
// Onboarding tour endpoints APIs
r.HandleFunc("/teams/{teamID}/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost)
}
func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /team/{teamID}/onboard onboard
//
// Onboards a user on Boards.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// properties:
// teamID:
// type: string
// description: Team ID
// boardID:
// type: string
// description: Board ID
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
response := map[string]string{
"teamID": teamID,
"boardID": boardID,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerSearchRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/channels", a.sessionRequired(a.handleSearchMyChannels)).Methods("GET")
r.HandleFunc("/teams/{teamID}/boards/search", a.sessionRequired(a.handleSearchBoards)).Methods("GET")
r.HandleFunc("/teams/{teamID}/boards/search/linkable", a.sessionRequired(a.handleSearchLinkableBoards)).Methods("GET")
r.HandleFunc("/boards/search", a.sessionRequired(a.handleSearchAllBoards)).Methods("GET")
}
func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/channels searchMyChannels
//
// Returns the user available channels
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: search
// in: query
// description: string to filter channels list
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Channel"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
query := r.URL.Query()
searchQuery := query.Get("search")
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "searchMyChannels", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetUserChannels",
mlog.String("teamID", teamID),
mlog.Int("channelsCount", len(channels)),
)
data, err := json.Marshal(channels)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("channelsCount", len(channels))
auditRec.Success()
}
func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/search searchBoards
//
// Returns the boards that match with a search term in the team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// - name: field
// in: query
// description: The field to search on for search term. Can be `title`, `property_name`. Defaults to `title`
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
var err error
teamID := mux.Vars(r)["teamID"]
term := r.URL.Query().Get("q")
searchFieldText := r.URL.Query().Get("field")
searchField := model.BoardSearchFieldTitle
if searchFieldText != "" {
searchField, err = model.BoardSearchFieldFromString(searchFieldText)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
}
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, searchField, userID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("SearchBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/search/linkable searchLinkableBoards
//
// Returns the boards that match with a search term in the team and the
// user has permission to manage members
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
teamID := mux.Vars(r)["teamID"]
term := r.URL.Query().Get("q")
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchLinkableBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
// retrieve boards list
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
linkableBoards := []*model.Board{}
for _, board := range boards {
if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionManageBoardRoles) {
linkableBoards = append(linkableBoards, board)
}
}
a.logger.Debug("SearchLinkableBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(linkableBoards)),
)
data, err := json.Marshal(linkableBoards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(linkableBoards))
auditRec.Success()
}
func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/search searchAllBoards
//
// Returns the boards that match with a search term
//
// ---
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
term := r.URL.Query().Get("q")
userID := getUserID(r)
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, model.BoardSearchFieldTitle, userID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("SearchAllBoards",
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrTurningOnSharing = errors.New("turning on sharing for board failed, see log for details")
func (a *API) registerSharingRoutes(r *mux.Router) {
// Sharing APIs
r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handleGetSharing)).Methods("GET")
}
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/sharing getSharing
//
// Returns sharing information for a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Sharing"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return
}
auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
sharing, err := a.app.GetSharing(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
sharingData, err := json.Marshal(sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, sharingData)
a.logger.Debug("GET sharing",
mlog.String("boardID", boardID),
mlog.String("shareID", sharing.ID),
mlog.Bool("enabled", sharing.Enabled),
)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
auditRec.Success()
}
func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/sharing postSharing
//
// Sets sharing information for a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: sharing information for a root block
// required: true
// schema:
// "$ref": "#/definitions/Sharing"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var sharing model.Sharing
err = json.Unmarshal(requestBody, &sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
// Stamp boardID from the URL
sharing.ID = boardID
auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
// Stamp ModifiedBy
modifiedBy := userID
if userID == model.SingleUser {
modifiedBy = ""
}
sharing.ModifiedBy = modifiedBy
if userID == model.SingleUser {
userID = ""
}
if !a.app.GetClientConfig().EnablePublicSharedBoards {
a.logger.Warn(
"Attempt to turn on sharing for board via API failed, sharing off in configuration.",
mlog.String("boardID", sharing.ID),
mlog.String("userID", userID))
a.errorResponse(w, r, ErrTurningOnSharing)
return
}
sharing.ModifiedBy = userID
err = a.app.UpsertSharing(sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerStatisticsRoutes(r *mux.Router) {
// statistics
r.HandleFunc("/statistics", a.sessionRequired(a.handleStatistics)).Methods("GET")
}
func (a *API) handleStatistics(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /statistics handleStatistics
//
// Fetches the statistic of the server.
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardStatistics"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
// user must have right to access analytics
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionGetAnalytics) {
a.errorResponse(w, r, model.NewErrPermission("access denied System Statistics"))
return
}
boardCount, err := a.app.GetBoardCount()
if err != nil {
a.errorResponse(w, r, err)
return
}
cardCount, err := a.app.GetUsedCardsCount()
if err != nil {
a.errorResponse(w, r, err)
return
}
stats := model.BoardsStatistics{
Boards: int(boardCount),
Cards: cardCount,
}
data, err := json.Marshal(stats)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerSubscriptionsRoutes(r *mux.Router) {
// Subscription APIs
r.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
r.HandleFunc("/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE")
r.HandleFunc("/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET")
}
// subscriptions
func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /subscriptions createSubscription
//
// Creates a subscription to a block for a user. The user will receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: subscription definition
// required: true
// schema:
// "$ref": "#/definitions/Subscription"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var sub model.Subscription
if err = json.Unmarshal(requestBody, &sub); err != nil {
a.errorResponse(w, r, err)
return
}
if err = sub.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "createSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("subscriber_id", sub.SubscriberID)
auditRec.AddMeta("block_id", sub.BlockID)
// User can only create subscriptions for themselves (for now)
if session.UserID != sub.SubscriberID {
a.errorResponse(w, r, model.NewErrBadRequest("userID and subscriberID mismatch"))
return
}
// check for valid block
_, bErr := a.app.GetBlockByID(sub.BlockID)
if bErr != nil {
message := fmt.Sprintf("invalid blockID: %s", bErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
subNew, err := a.app.CreateSubscription(&sub)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CREATE subscription",
mlog.String("subscriber_id", subNew.SubscriberID),
mlog.String("block_id", subNew.BlockID),
)
json, err := json.Marshal(subNew)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.Success()
}
func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /subscriptions/{blockID}/{subscriberID} deleteSubscription
//
// Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
blockID := vars["blockID"]
subscriberID := vars["subscriberID"]
auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("block_id", blockID)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only delete subscriptions for themselves
if session.UserID != subscriberID {
a.errorResponse(w, r, model.NewErrPermission("access denied"))
return
}
if _, err := a.app.DeleteSubscription(blockID, subscriberID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE subscription",
mlog.String("blockID", blockID),
mlog.String("subscriberID", subscriberID),
)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /subscriptions/{subscriberID} getSubscriptions
//
// Gets subscriptions for a user.
//
// ---
// produces:
// - application/json
// parameters:
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
subscriberID := vars["subscriberID"]
auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only get subscriptions for themselves (for now)
if session.UserID != subscriberID {
a.errorResponse(w, r, model.NewErrPermission("access denied"))
return
}
subs, err := a.app.GetSubscriptions(subscriberID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GET subscriptions",
mlog.String("subscriberID", subscriberID),
mlog.Int("count", len(subs)),
)
json, err := json.Marshal(subs)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("subscription_count", len(subs))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func (a *API) registerSystemRoutes(r *mux.Router) {
// System APIs
r.HandleFunc("/hello", a.handleHello).Methods("GET")
r.HandleFunc("/ping", a.handlePing).Methods("GET")
}
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /hello hello
//
// Responds with `Hello` if the web service is running.
//
// ---
// produces:
// - text/plain
// responses:
// '200':
// description: success
stringResponse(w, "Hello")
}
func (a *API) handlePing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /ping ping
//
// Responds with server metadata if the web service is running.
//
// ---
// produces:
// - application/json
// responses:
// '200':
// description: success
serverMetadata := a.app.GetServerMetadata()
if a.singleUserToken != "" {
serverMetadata.SKU = "personal_desktop"
}
if serverMetadata.Edition == "plugin" {
serverMetadata.SKU = "suite"
}
bytes, err := json.Marshal(serverMetadata)
if err != nil {
a.errorResponse(w, r, err)
}
jsonStringResponse(w, 200, string(bytes))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func (a *API) registerTeamsRoutes(r *mux.Router) {
// Team APIs
r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET")
r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET")
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET")
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST")
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
}
func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams getTeams
//
// Returns information of all the teams
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Team"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
teams, err := a.app.GetTeamsForUser(userID)
if err != nil {
a.errorResponse(w, r, err)
}
auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamCount", len(teams))
data, err := json.Marshal(teams)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID} getTeam
//
// Returns information of the root team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Team"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
var team *model.Team
var err error
if a.MattermostAuth {
team, err = a.app.GetTeam(teamID)
if model.IsErrNotFound(err) {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid team"))
}
if err != nil {
a.errorResponse(w, r, err)
}
} else {
team, err = a.app.GetRootTeam()
if err != nil {
a.errorResponse(w, r, err)
return
}
}
auditRec := a.makeAuditRecord(r, "getTeam", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("resultTeamID", team.ID)
data, err := json.Marshal(team)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/regenerate_signup_token regenerateSignupToken
//
// Regenerates the signup token for the root team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
team, err := a.app.GetRootTeam()
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
team.SignupToken = utils.NewID(utils.IDTypeToken)
if err = a.app.UpsertTeamSignupToken(*team); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/users getTeamUsers
//
// Returns team users
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: search
// in: query
// description: string to filter users list
// required: false
// type: string
// - name: exclude_bots
// in: query
// description: exclude bot users
// required: false
// type: boolean
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
query := r.URL.Query()
searchQuery := query.Get("search")
excludeBots := r.URL.Query().Get("exclude_bots") == True
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
asGuestUser := ""
if isGuest {
asGuestUser = userID
}
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser, excludeBots)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("userCount", len(users))
auditRec.Success()
}
func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/users getTeamUsersByID
//
// Returns a user[]
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: Body
// in: body
// description: []UserIDs to return
// required: true
// type: []string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var userIDs []string
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
var users []*model.User
var error error
if len(userIDs) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
return
}
if userIDs[0] == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user := &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
users = append(users, user)
} else {
users, error = a.app.GetUsersList(userIDs)
if error != nil {
a.errorResponse(w, r, error)
return
}
for i, u := range users {
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
}
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
}
}
}
usersList, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, string(usersList))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerTemplatesRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/templates", a.sessionRequired(a.handleGetTemplates)).Methods("GET")
}
func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/templates getTemplates
//
// Returns team templates
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to templates"))
return
}
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
// retrieve boards list
boards, err := a.app.GetTemplateBoards(teamID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
results := []*model.Board{}
for _, board := range boards {
if board.Type == model.BoardTypeOpen {
results = append(results, board)
} else if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionViewBoard) {
results = append(results, board)
}
}
a.logger.Debug("GetTemplates",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(results)),
)
data, err := json.Marshal(results)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("templatesCount", len(results))
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func (a *API) registerUsersRoutes(r *mux.Router) {
// Users APIs
r.HandleFunc("/users", a.sessionRequired(a.handleGetUsersList)).Methods("POST")
r.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
r.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET")
r.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
r.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
r.HandleFunc("/users/me/config", a.sessionRequired(a.handleGetUserPreferences)).Methods(http.MethodGet)
}
func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /users getUsersList
//
// Returns a user[]
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var userIDs []string
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getUsersList", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
var users []*model.User
var error error
if len(userIDs) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
return
}
if userIDs[0] == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user := &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
users = append(users, user)
} else {
users, error = a.app.GetUsersList(userIDs)
if error != nil {
a.errorResponse(w, r, error)
return
}
}
usersList, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, string(usersList))
auditRec.Success()
}
func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me getMe
//
// Returns the currently logged-in user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: false
// type: string
// - name: channelID
// in: path
// description: Channel ID
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
teamID := query.Get("teamID")
channelID := query.Get("channelID")
userID := getUserID(r)
var user *model.User
var err error
auditRec := a.makeAuditRecord(r, "getMe", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
if userID == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user = &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
} else {
user, err = a.app.GetUser(userID)
if err != nil {
// ToDo: wrap with an invalid token error
a.errorResponse(w, r, err)
return
}
}
if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) {
user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id)
}
if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) {
user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id)
}
if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
user.Permissions = append(user.Permissions, model.PermissionCreatePost.Id)
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID)
auditRec.Success()
}
func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/memberships getMyMemberships
//
// Returns the currently users board memberships
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardMember"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
auditRec := a.makeAuditRecord(r, "getMyBoardMemberships", audit.Fail)
auditRec.AddMeta("userID", userID)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
members, err := a.app.GetMembersForUser(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
membersData, err := json.Marshal(members)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, membersData)
auditRec.Success()
}
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/{userID} getUser
//
// Returns a user
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
userID := vars["userID"]
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("userID", userID)
user, err := a.app.GetUser(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !canSeeUser {
a.errorResponse(w, r, model.NewErrNotFound("user ID="+userID))
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.Success()
}
func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /users/{userID}/config updateUserConfig
//
// Updates user config
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: Body
// in: body
// description: User config patch to apply
// required: true
// schema:
// "$ref": "#/definitions/UserPreferencesPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.UserPreferencesPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
vars := mux.Vars(r)
userID := vars["userID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
// a user can update only own config
if userID != session.UserID {
a.errorResponse(w, r, model.NewErrForbidden(""))
return
}
updatedConfig, err := a.app.UpdateUserConfig(userID, *patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedConfig)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetUserPreferences(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/config getUserConfig
//
// Returns an array of user preferences
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Preferences"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
auditRec := a.makeAuditRecord(r, "getUserConfig", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
preferences, err := a.app.GetUserPreferences(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(preferences)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"io"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/metrics"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/services/webhook"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/boards/ws"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
blockChangeNotifierQueueSize = 1000
blockChangeNotifierPoolSize = 10
blockChangeNotifierShutdownTimeout = time.Second * 10
)
type servicesAPI interface {
GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error)
}
type ReadCloseSeeker = filestore.ReadCloseSeeker
type fileBackend interface {
Reader(path string) (ReadCloseSeeker, error)
FileExists(path string) (bool, error)
CopyFile(oldPath, newPath string) error
MoveFile(oldPath, newPath string) error
WriteFile(fr io.Reader, path string) (int64, error)
RemoveFile(path string) error
}
type Services struct {
Auth *auth.Auth
Store store.Store
FilesBackend fileBackend
Webhook *webhook.Client
Metrics *metrics.Metrics
Notifications *notify.Service
Logger mlog.LoggerIFace
Permissions permissions.PermissionsService
SkipTemplateInit bool
ServicesAPI servicesAPI
}
type App struct {
config *config.Configuration
store store.Store
auth *auth.Auth
wsAdapter ws.Adapter
filesBackend fileBackend
webhook *webhook.Client
metrics *metrics.Metrics
notifications *notify.Service
logger mlog.LoggerIFace
permissions permissions.PermissionsService
blockChangeNotifier *utils.CallbackQueue
servicesAPI servicesAPI
cardLimitMux sync.RWMutex
cardLimit int
}
func (a *App) SetConfig(config *config.Configuration) {
a.config = config
}
func (a *App) GetConfig() *config.Configuration {
return a.config
}
func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App {
app := &App{
config: config,
store: services.Store,
auth: services.Auth,
wsAdapter: wsAdapter,
filesBackend: services.FilesBackend,
webhook: services.Webhook,
metrics: services.Metrics,
notifications: services.Notifications,
logger: services.Logger,
permissions: services.Permissions,
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
servicesAPI: services.ServicesAPI,
}
app.initialize(services.SkipTemplateInit)
return app
}
func (a *App) CardLimit() int {
a.cardLimitMux.RLock()
defer a.cardLimitMux.RUnlock()
return a.cardLimit
}
func (a *App) SetCardLimit(cardLimit int) {
a.cardLimitMux.Lock()
defer a.cardLimitMux.Unlock()
a.cardLimit = cardLimit
}
func (a *App) GetLicense() *mm_model.License {
return a.store.GetLicense()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
const (
DaysPerMonth = 30
DaysPerWeek = 7
HoursPerDay = 24
MinutesPerHour = 60
SecondsPerMinute = 60
)
// GetSession Get a user active session and refresh the session if is needed.
func (a *App) GetSession(token string) (*model.Session, error) {
return a.auth.GetSession(token)
}
// IsValidReadToken validates the read token for a block.
func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) {
return a.auth.IsValidReadToken(boardID, readToken)
}
// GetRegisteredUserCount returns the number of registered users.
func (a *App) GetRegisteredUserCount() (int, error) {
return a.store.GetRegisteredUserCount()
}
// GetDailyActiveUsers returns the number of daily active users.
func (a *App) GetDailyActiveUsers() (int, error) {
secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay)
return a.store.GetActiveUserCount(secondsAgo)
}
// GetWeeklyActiveUsers returns the number of weekly active users.
func (a *App) GetWeeklyActiveUsers() (int, error) {
secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay * DaysPerWeek)
return a.store.GetActiveUserCount(secondsAgo)
}
// GetMonthlyActiveUsers returns the number of monthly active users.
func (a *App) GetMonthlyActiveUsers() (int, error) {
secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay * DaysPerMonth)
return a.store.GetActiveUserCount(secondsAgo)
}
// GetUser gets an existing active user by id.
func (a *App) GetUser(id string) (*model.User, error) {
if len(id) < 1 {
return nil, errors.New("no user ID")
}
user, err := a.store.GetUserByID(id)
if err != nil {
return nil, errors.Wrap(err, "unable to find user")
}
return user, nil
}
func (a *App) GetUsersList(userIDs []string) ([]*model.User, error) {
if len(userIDs) == 0 {
return nil, errors.New("No User IDs")
}
users, err := a.store.GetUsersList(userIDs, a.config.ShowEmailAddress, a.config.ShowFullName)
if err != nil {
return nil, errors.Wrap(err, "unable to find users")
}
return users, nil
}
// Login create a new user session if the authentication data is valid.
func (a *App) Login(username, email, password, mfaToken string) (string, error) {
var user *model.User
if username != "" {
var err error
user, err = a.store.GetUserByUsername(username)
if err != nil && !model.IsErrNotFound(err) {
a.metrics.IncrementLoginFailCount(1)
return "", errors.Wrap(err, "invalid username or password")
}
}
if user == nil && email != "" {
var err error
user, err = a.store.GetUserByEmail(email)
if err != nil && model.IsErrNotFound(err) {
a.metrics.IncrementLoginFailCount(1)
return "", errors.Wrap(err, "invalid username or password")
}
}
if user == nil {
a.metrics.IncrementLoginFailCount(1)
return "", errors.New("invalid username or password")
}
if !auth.ComparePassword(user.Password, password) {
a.metrics.IncrementLoginFailCount(1)
a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID))
return "", errors.New("invalid username or password")
}
authService := user.AuthService
if authService == "" {
authService = "native"
}
session := model.Session{
ID: utils.NewID(utils.IDTypeSession),
Token: utils.NewID(utils.IDTypeToken),
UserID: user.ID,
AuthService: authService,
Props: map[string]interface{}{},
}
err := a.store.CreateSession(&session)
if err != nil {
return "", errors.Wrap(err, "unable to create session")
}
a.metrics.IncrementLoginCount(1)
// TODO: MFA verification
return session.Token, nil
}
// Logout invalidates the user session.
func (a *App) Logout(sessionID string) error {
err := a.store.DeleteSession(sessionID)
if err != nil {
return errors.Wrap(err, "unable to delete the session")
}
a.metrics.IncrementLogoutCount(1)
return nil
}
// RegisterUser creates a new user if the provided data is valid.
func (a *App) RegisterUser(username, email, password string) error {
var user *model.User
if username != "" {
var err error
user, err = a.store.GetUserByUsername(username)
if err != nil && !model.IsErrNotFound(err) {
return err
}
if user != nil {
return errors.New("The username already exists")
}
}
if user == nil && email != "" {
var err error
user, err = a.store.GetUserByEmail(email)
if err != nil && !model.IsErrNotFound(err) {
return err
}
if user != nil {
return errors.New("The email already exists")
}
}
// TODO: Move this into the config
passwordSettings := auth.PasswordSettings{
MinimumLength: 6,
}
err := auth.IsPasswordValid(password, passwordSettings)
if err != nil {
return errors.Wrap(err, "Invalid password")
}
_, err = a.store.CreateUser(&model.User{
ID: utils.NewID(utils.IDTypeUser),
Username: username,
Email: email,
Password: auth.HashPassword(password),
MfaSecret: "",
AuthService: a.config.AuthMode,
AuthData: "",
})
if err != nil {
return errors.Wrap(err, "Unable to create the new user")
}
return nil
}
func (a *App) UpdateUserPassword(username, password string) error {
err := a.store.UpdateUserPassword(username, auth.HashPassword(password))
if err != nil {
return err
}
return nil
}
func (a *App) ChangePassword(userID, oldPassword, newPassword string) error {
var user *model.User
if userID != "" {
var err error
user, err = a.store.GetUserByID(userID)
if err != nil {
return errors.Wrap(err, "invalid username or password")
}
}
if user == nil {
return errors.New("invalid username or password")
}
if !auth.ComparePassword(user.Password, oldPassword) {
a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID))
return errors.New("invalid username or password")
}
err := a.store.UpdateUserPasswordByID(userID, auth.HashPassword(newPassword))
if err != nil {
return errors.Wrap(err, "unable to update password")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]*model.Block, error) {
if boardID == "" {
return []*model.Block{}, nil
}
if blockType != "" && parentID != "" {
return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType)
}
if blockType != "" {
return a.store.GetBlocksWithType(boardID, blockType)
}
return a.store.GetBlocksWithParent(boardID, parentID)
}
func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) {
board, err := a.GetBoard(boardID)
if err != nil {
return nil, err
}
if board == nil {
return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err)
}
blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
for _, block := range blocks {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
}
return nil
})
go func() {
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed duplicating a block",
mlog.Err(uErr),
)
}
}()
return blocks, err
}
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) (*model.Block, error) {
return a.PatchBlockAndNotify(blockID, blockPatch, modifiedByID, false)
}
func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch, modifiedByID string, disableNotify bool) (*model.Block, error) {
oldBlock, err := a.store.GetBlock(blockID)
if err != nil {
return nil, err
}
if a.IsCloudLimited() {
containsLimitedBlocks, lErr := a.ContainsLimitedBlocks([]*model.Block{oldBlock})
if lErr != nil {
return nil, lErr
}
if containsLimitedBlocks {
return nil, model.ErrPatchUpdatesLimitedCards
}
}
board, err := a.store.GetBoard(oldBlock.BoardID)
if err != nil {
return nil, err
}
err = a.store.PatchBlock(blockID, blockPatch, modifiedByID)
if err != nil {
return nil, err
}
a.metrics.IncrementBlocksPatched(1)
block, err := a.store.GetBlock(blockID)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
// broadcast on websocket
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
// broadcast on webhooks
a.webhook.NotifyUpdate(block)
// send notifications
if !disableNotify {
a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID)
}
return nil
})
return block, nil
}
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
return a.PatchBlocksAndNotify(teamID, blockPatches, modifiedByID, false)
}
func (a *App) PatchBlocksAndNotify(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string, disableNotify bool) error {
oldBlocks, err := a.store.GetBlocksByIDs(blockPatches.BlockIDs)
if err != nil {
return err
}
if a.IsCloudLimited() {
containsLimitedBlocks, err := a.ContainsLimitedBlocks(oldBlocks)
if err != nil {
return err
}
if containsLimitedBlocks {
return model.ErrPatchUpdatesLimitedCards
}
}
if err := a.store.PatchBlocks(blockPatches, modifiedByID); err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
a.metrics.IncrementBlocksPatched(len(oldBlocks))
for i, blockID := range blockPatches.BlockIDs {
newBlock, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
a.wsAdapter.BroadcastBlockChange(teamID, newBlock)
a.webhook.NotifyUpdate(newBlock)
if !disableNotify {
a.notifyBlockChanged(notify.Update, newBlock, oldBlocks[i], modifiedByID)
}
}
return nil
})
return nil
}
func (a *App) InsertBlock(block *model.Block, modifiedByID string) error {
return a.InsertBlockAndNotify(block, modifiedByID, false)
}
func (a *App) InsertBlockAndNotify(block *model.Block, modifiedByID string, disableNotify bool) error {
board, bErr := a.store.GetBoard(block.BoardID)
if bErr != nil {
return bErr
}
err := a.store.InsertBlock(block, modifiedByID)
if err == nil {
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(block)
if !disableNotify {
a.notifyBlockChanged(notify.Add, block, nil, modifiedByID)
}
return nil
})
}
go func() {
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after inserting a block",
mlog.Err(uErr),
)
}
}()
return err
}
func (a *App) isWithinViewsLimit(boardID string, block *model.Block) (bool, error) {
// ToDo: Cloud Limits have been disabled by design. We should
// revisit the decision and update the related code accordingly
/*
limits, err := a.GetBoardsCloudLimits()
if err != nil {
return false, err
}
if limits.Views == model.LimitUnlimited {
return true, nil
}
views, err := a.store.GetBlocksWithParentAndType(boardID, block.ParentID, model.TypeView)
if err != nil {
return false, err
}
// < rather than <= because we'll be creating new view if this
// check passes. When that view is created, the limit will be reached.
// That's why we need to check for if existing + the being-created
// view doesn't exceed the limit.
return len(views) < limits.Views, nil
*/
return true, nil
}
func (a *App) InsertBlocks(blocks []*model.Block, modifiedByID string) ([]*model.Block, error) {
return a.InsertBlocksAndNotify(blocks, modifiedByID, false)
}
func (a *App) InsertBlocksAndNotify(blocks []*model.Block, modifiedByID string, disableNotify bool) ([]*model.Block, error) {
if len(blocks) == 0 {
return []*model.Block{}, nil
}
// all blocks must belong to the same board
boardID := blocks[0].BoardID
for _, block := range blocks {
if block.BoardID != boardID {
return nil, ErrBlocksFromMultipleBoards
}
}
board, err := a.store.GetBoard(boardID)
if err != nil {
return nil, err
}
needsNotify := make([]*model.Block, 0, len(blocks))
for i := range blocks {
// this check is needed to whitelist inbuilt template
// initialization. They do contain more than 5 views per board.
if boardID != "0" && blocks[i].Type == model.TypeView {
withinLimit, err := a.isWithinViewsLimit(board.ID, blocks[i])
if err != nil {
return nil, err
}
if !withinLimit {
a.logger.Info("views limit reached on board", mlog.String("board_id", blocks[i].ParentID), mlog.String("team_id", board.TeamID))
return nil, model.ErrViewsLimitReached
}
}
err := a.store.InsertBlock(blocks[i], modifiedByID)
if err != nil {
return nil, err
}
needsNotify = append(needsNotify, blocks[i])
a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i])
a.metrics.IncrementBlocksInserted(1)
}
a.blockChangeNotifier.Enqueue(func() error {
for _, b := range needsNotify {
block := b
a.webhook.NotifyUpdate(block)
if !disableNotify {
a.notifyBlockChanged(notify.Add, block, nil, modifiedByID)
}
}
return nil
})
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after inserting blocks",
mlog.Err(err),
)
}
}()
return blocks, nil
}
func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) error {
// Images attached in cards have a path comprising the card's board ID.
// When we create a template from this board, we need to copy the files
// with the new board ID in path.
// Not doing so causing images in templates (and boards created from this
// template) to fail to load.
// look up ID of source sourceBoard, which may be different than the blocks.
sourceBoard, err := a.GetBoard(sourceBoardID)
if err != nil || sourceBoard == nil {
return fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err)
}
var destTeamID string
var destBoardID string
for i := range copiedBlocks {
block := copiedBlocks[i]
fileName := ""
isOk := false
switch block.Type {
case model.TypeImage:
fileName, isOk = block.Fields["fileId"].(string)
if !isOk || fileName == "" {
continue
}
case model.TypeAttachment:
fileName, isOk = block.Fields["attachmentId"].(string)
if !isOk || fileName == "" {
continue
}
default:
continue
}
// create unique filename in case we are copying cards within the same board.
ext := filepath.Ext(fileName)
destFilename := utils.NewID(utils.IDTypeNone) + ext
if destBoardID == "" || block.BoardID != destBoardID {
destBoardID = block.BoardID
destBoard, err := a.GetBoard(destBoardID)
if err != nil {
return fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err)
}
destTeamID = destBoard.TeamID
}
sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName)
destinationFilePath := filepath.Join(destTeamID, block.BoardID, destFilename)
a.logger.Debug(
"Copying card file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
)
if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil {
a.logger.Error(
"CopyCardFiles failed to copy file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
mlog.Err(err),
)
}
if block.Type == model.TypeAttachment {
block.Fields["attachmentId"] = destFilename
parts := strings.Split(fileName, ".")
fileInfoID := parts[0][1:]
fileInfo, err := a.store.GetFileInfo(fileInfoID)
if err != nil {
return fmt.Errorf("CopyCardFiles: cannot retrieve original fileinfo: %w", err)
}
newParts := strings.Split(destFilename, ".")
newFileID := newParts[0][1:]
fileInfo.Id = newFileID
err = a.store.SaveFileInfo(fileInfo)
if err != nil {
return fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err)
}
} else {
block.Fields["fileId"] = destFilename
}
}
return nil
}
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
return a.store.GetBlock(blockID)
}
func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
return a.DeleteBlockAndNotify(blockID, modifiedBy, false)
}
func (a *App) DeleteBlockAndNotify(blockID string, modifiedBy string, disableNotify bool) error {
block, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return err
}
if block == nil {
// deleting non-existing block not considered an error
return nil
}
err = a.store.DeleteBlock(blockID, modifiedBy)
if err != nil {
return err
}
if block.Type == model.TypeImage {
fileName, fileIDExists := block.Fields["fileId"]
if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString {
filePath := filepath.Join(block.BoardID, fileName)
err = a.filesBackend.RemoveFile(filePath)
if err != nil {
a.logger.Error("Error deleting image file",
mlog.String("FilePath", filePath),
mlog.Err(err))
}
}
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
a.metrics.IncrementBlocksDeleted(1)
if !disableNotify {
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
}
return nil
})
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after deleting a block",
mlog.Err(err),
)
}
}()
return nil
}
func (a *App) GetLastBlockHistoryEntry(blockID string) (*model.Block, error) {
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return nil, err
}
if len(blocks) == 0 {
return nil, nil
}
return blocks[0], nil
}
func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, error) {
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return nil, err
}
if len(blocks) == 0 {
// undeleting non-existing block not considered an error
return nil, nil
}
err = a.store.UndeleteBlock(blockID, modifiedBy)
if err != nil {
return nil, err
}
block, err := a.store.GetBlock(blockID)
if model.IsErrNotFound(err) {
a.logger.Error("Error loading the block after a successful undelete, not propagating through websockets or notifications", mlog.String("blockID", blockID))
return nil, err
}
if err != nil {
return nil, err
}
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(block)
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
return nil
})
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after undeleting a block",
mlog.Err(err),
)
}
}()
return block, nil
}
func (a *App) GetBlockCountsByType() (map[string]int64, error) {
return a.store.GetBlockCountsByType()
}
func (a *App) GetBlocksForBoard(boardID string) ([]*model.Block, error) {
return a.store.GetBlocksForBoard(boardID)
}
func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) {
// don't notify if notifications service disabled, or block change is generated via system user.
if a.notifications == nil || modifiedByID == model.SystemUserID {
return
}
// find card and board for the changed block.
board, card, err := a.getBoardAndCard(block)
if err != nil {
a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err))
return
}
boardMember, _ := a.GetMemberForBoard(board.ID, modifiedByID)
if boardMember == nil {
// create temporary guest board member
boardMember = &model.BoardMember{
BoardID: board.ID,
UserID: modifiedByID,
}
}
evt := notify.BlockChangeEvent{
Action: action,
TeamID: board.TeamID,
Board: board,
Card: card,
BlockChanged: block,
BlockOld: oldBlock,
ModifiedBy: boardMember,
}
a.notifications.BlockChanged(evt)
}
const (
maxSearchDepth = 50
)
// getBoardAndCard returns the first parent of type `card` its board for the specified block.
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) {
board, err = a.store.GetBoard(block.BoardID)
if err != nil {
return board, card, err
}
var count int // don't let invalid blocks hierarchy cause infinite loop.
iter := block
for {
count++
if card == nil && iter.Type == model.TypeCard {
card = iter
}
if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth {
break
}
iter, err = a.store.GetBlock(iter.ParentID)
if model.IsErrNotFound(err) {
return board, card, nil
}
if err != nil {
return board, card, err
}
}
return board, card, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID")
)
const linkBoardMessage = "@%s linked the board [%s](%s) with this channel"
const unlinkBoardMessage = "@%s unlinked the board [%s](%s) with this channel"
var errNoDefaultCategoryFound = errors.New("no default category found for user")
func (a *App) GetBoard(boardID string) (*model.Board, error) {
board, err := a.store.GetBoard(boardID)
if err != nil {
return nil, err
}
return board, nil
}
func (a *App) GetBoardCount() (int64, error) {
return a.store.GetBoardCount()
}
func (a *App) GetBoardMetadata(boardID string) (*model.Board, *model.BoardMetadata, error) {
license := a.store.GetLicense()
if license == nil || !(*license.Features.Compliance) {
return nil, nil, model.ErrInsufficientLicense
}
board, err := a.GetBoard(boardID)
if model.IsErrNotFound(err) {
// Board may have been deleted, retrieve most recent history instead
board, err = a.getBoardHistory(boardID, true)
if err != nil {
return nil, nil, err
}
}
if err != nil {
return nil, nil, err
}
earliestTime, _, err := a.getBoardDescendantModifiedInfo(boardID, false)
if err != nil {
return nil, nil, err
}
latestTime, lastModifiedBy, err := a.getBoardDescendantModifiedInfo(boardID, true)
if err != nil {
return nil, nil, err
}
boardMetadata := model.BoardMetadata{
BoardID: boardID,
DescendantFirstUpdateAt: earliestTime,
DescendantLastUpdateAt: latestTime,
CreatedBy: board.CreatedBy,
LastModifiedBy: lastModifiedBy,
}
return board, &boardMetadata, nil
}
// getBoardForBlock returns the board that owns the specified block.
func (a *App) getBoardForBlock(blockID string) (*model.Board, error) {
block, err := a.GetBlockByID(blockID)
if err != nil {
return nil, fmt.Errorf("cannot get block %s: %w", blockID, err)
}
board, err := a.GetBoard(block.BoardID)
if err != nil {
return nil, fmt.Errorf("cannot get board %s: %w", block.BoardID, err)
}
return board, nil
}
func (a *App) getBoardHistory(boardID string, latest bool) (*model.Board, error) {
opts := model.QueryBoardHistoryOptions{
Limit: 1,
Descending: latest,
}
boards, err := a.store.GetBoardHistory(boardID, opts)
if err != nil {
return nil, fmt.Errorf("could not get history for board: %w", err)
}
if len(boards) == 0 {
return nil, nil
}
return boards[0], nil
}
func (a *App) getBoardDescendantModifiedInfo(boardID string, latest bool) (int64, string, error) {
board, err := a.getBoardHistory(boardID, latest)
if err != nil {
return 0, "", err
}
if board == nil {
return 0, "", fmt.Errorf("history not found for board: %w", err)
}
var timestamp int64
modifiedBy := board.ModifiedBy
if latest {
timestamp = board.UpdateAt
} else {
timestamp = board.CreateAt
}
// use block_history to fetch blocks in case they were deleted and no longer exist in blocks table.
opts := model.QueryBlockHistoryOptions{
Limit: 1,
Descending: latest,
}
blocks, err := a.store.GetBlockHistoryDescendants(boardID, opts)
if err != nil {
return 0, "", fmt.Errorf("could not get blocks history descendants for board: %w", err)
}
if len(blocks) > 0 {
// Compare the board history info with the descendant block info, if it exists
block := blocks[0]
if latest && block.UpdateAt > timestamp {
timestamp = block.UpdateAt
modifiedBy = block.ModifiedBy
} else if !latest && block.CreateAt < timestamp {
timestamp = block.CreateAt
modifiedBy = block.ModifiedBy
}
}
return timestamp, modifiedBy, nil
}
func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, userID, teamID string, asTemplate bool) error {
// find source board's category ID for the user
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var destinationCategoryID string
for _, categoryBoard := range userCategoryBoards {
for _, metadata := range categoryBoard.BoardMetadata {
if metadata.BoardID == sourceBoardID {
// category found!
destinationCategoryID = categoryBoard.ID
break
}
}
}
if destinationCategoryID == "" {
// if source board is not mapped to a category for this user,
// then move new board to default category
if !asTemplate {
return a.addBoardsToDefaultCategory(userID, teamID, []*model.Board{{ID: destinationBoardID}})
}
return nil
}
// now that we have source board's category,
// we send destination board to the same category
return a.AddUpdateUserCategoryBoard(teamID, userID, destinationCategoryID, []string{destinationBoardID})
}
func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate)
if err != nil {
return nil, nil, err
}
// copy any file attachments from the duplicated blocks.
if err = a.CopyCardFiles(boardID, bab.Blocks); err != nil {
a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err))
}
if !asTemplate {
for _, board := range bab.Boards {
if categoryErr := a.setBoardCategoryFromSource(boardID, board.ID, userID, toTeam, asTemplate); categoryErr != nil {
return nil, nil, categoryErr
}
}
}
// bab.Blocks now has updated file ids for any blocks containing files. We need to store them.
blockIDs := make([]string, 0)
blockPatches := make([]model.BlockPatch, 0)
for _, block := range bab.Blocks {
fieldName := ""
if block.Type == model.TypeImage {
fieldName = "fileId"
} else if block.Type == model.TypeAttachment {
fieldName = "attachmentId"
}
if fieldName != "" {
if fieldID, ok := block.Fields[fieldName]; ok {
blockIDs = append(blockIDs, block.ID)
blockPatches = append(blockPatches, model.BlockPatch{
UpdatedFields: map[string]interface{}{
fieldName: fieldID,
},
})
}
}
}
a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs)))
if len(blockIDs) != 0 {
patches := &model.BlockPatchBatch{
BlockIDs: blockIDs,
BlockPatches: blockPatches,
}
if err = a.store.PatchBlocks(patches, userID); err != nil {
dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab)
if err = a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil {
a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(err))
}
return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err)
}
}
a.blockChangeNotifier.Enqueue(func() error {
teamID := ""
for _, board := range bab.Boards {
teamID = board.TeamID
a.wsAdapter.BroadcastBoardChange(teamID, board)
}
for _, block := range bab.Blocks {
blk := block
a.wsAdapter.BroadcastBlockChange(teamID, blk)
a.notifyBlockChanged(notify.Add, blk, nil, userID)
}
for _, member := range members {
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
}
return nil
})
if len(bab.Blocks) != 0 {
go func() {
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after duplicating a board",
mlog.Err(uErr),
)
}
}()
}
return bab, members, err
}
func (a *App) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
return a.store.GetBoardsForUserAndTeam(userID, teamID, includePublicBoards)
}
func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
return a.store.GetTemplateBoards(teamID, userID)
}
func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) {
if board.ID != "" {
return nil, ErrNewBoardCannotHaveID
}
board.ID = utils.NewID(utils.IDTypeBoard)
var newBoard *model.Board
var member *model.BoardMember
var err error
if addMember {
newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID)
} else {
newBoard, err = a.store.InsertBoard(board, userID)
}
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard)
if newBoard.ChannelID != "" {
members, err := a.GetMembersForBoard(board.ID)
if err != nil {
a.logger.Error("Unable to get the board members", mlog.Err(err))
}
for _, member := range members {
a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, member.BoardID, member)
}
} else if addMember {
a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member)
}
return nil
})
if !board.IsTemplate {
if err := a.addBoardsToDefaultCategory(userID, newBoard.TeamID, []*model.Board{newBoard}); err != nil {
return nil, err
}
}
return newBoard, nil
}
func (a *App) addBoardsToDefaultCategory(userID, teamID string, boards []*model.Board) error {
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
defaultCategoryID := ""
for _, categoryBoard := range userCategoryBoards {
if categoryBoard.Name == defaultCategoryBoards {
defaultCategoryID = categoryBoard.ID
break
}
}
if defaultCategoryID == "" {
return fmt.Errorf("%w userID: %s", errNoDefaultCategoryFound, userID)
}
boardIDs := make([]string, len(boards))
for i := range boards {
boardIDs[i] = boards[i].ID
}
if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil {
return err
}
return nil
}
func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) {
var oldChannelID string
var isTemplate bool
var oldMembers []*model.BoardMember
if patch.Type != nil || patch.ChannelID != nil {
testChannel := ""
if patch.ChannelID != nil && *patch.ChannelID == "" {
var err error
oldMembers, err = a.GetMembersForBoard(boardID)
if err != nil {
a.logger.Error("Unable to get the board members", mlog.Err(err))
}
} else if patch.ChannelID != nil && *patch.ChannelID != "" {
testChannel = *patch.ChannelID
}
board, err := a.store.GetBoard(boardID)
if model.IsErrNotFound(err) {
return nil, model.NewErrNotFound("board ID=" + boardID)
}
if err != nil {
return nil, err
}
oldChannelID = board.ChannelID
isTemplate = board.IsTemplate
if testChannel == "" {
testChannel = oldChannelID
}
if testChannel != "" {
if !a.permissions.HasPermissionToChannel(userID, testChannel, model.PermissionCreatePost) {
return nil, model.NewErrPermission("access denied to channel")
}
}
}
updatedBoard, err := a.store.PatchBoard(boardID, patch, userID)
if err != nil {
return nil, err
}
// Post message to channel if linked/unlinked
if patch.ChannelID != nil {
var username string
user, err := a.store.GetUserByID(userID)
if err != nil {
a.logger.Error("Unable to get the board updater", mlog.Err(err))
username = "unknown"
} else {
username = user.Username
}
boardLink := utils.MakeBoardLink(a.config.ServerRoot, updatedBoard.TeamID, updatedBoard.ID)
title := updatedBoard.Title
if title == "" {
title = "Untitled board" // todo: localize this when server has i18n
}
if *patch.ChannelID != "" {
a.postChannelMessage(fmt.Sprintf(linkBoardMessage, username, title, boardLink), updatedBoard.ChannelID)
} else if *patch.ChannelID == "" {
a.postChannelMessage(fmt.Sprintf(unlinkBoardMessage, username, title, boardLink), oldChannelID)
}
}
// Broadcast Messages to affected users
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard)
if patch.ChannelID != nil {
if *patch.ChannelID != "" {
members, err := a.GetMembersForBoard(updatedBoard.ID)
if err != nil {
a.logger.Error("Unable to get the board members", mlog.Err(err))
}
for _, member := range members {
if member.Synthetic {
a.wsAdapter.BroadcastMemberChange(updatedBoard.TeamID, member.BoardID, member)
}
}
} else {
for _, oldMember := range oldMembers {
if oldMember.Synthetic {
a.wsAdapter.BroadcastMemberDelete(updatedBoard.TeamID, boardID, oldMember.UserID)
}
}
}
}
if patch.Type != nil && isTemplate {
members, err := a.GetMembersForBoard(updatedBoard.ID)
if err != nil {
a.logger.Error("Unable to get the board members", mlog.Err(err))
}
a.broadcastTeamUsers(updatedBoard.TeamID, updatedBoard.ID, *patch.Type, members)
}
return nil
})
return updatedBoard, nil
}
func (a *App) postChannelMessage(message, channelID string) {
err := a.store.PostMessage(message, "", channelID)
if err != nil {
a.logger.Error("Unable to post the link message to channel", mlog.Err(err))
}
}
// broadcastTeamUsers notifies the members of a team when a template changes its type
// from public to private or viceversa.
func (a *App) broadcastTeamUsers(teamID, boardID string, boardType model.BoardType, members []*model.BoardMember) {
users, err := a.GetTeamUsers(teamID, "")
if err != nil {
a.logger.Error("Unable to get the team users", mlog.Err(err))
}
for _, user := range users {
isMember := false
for _, member := range members {
if member.UserID == user.ID {
isMember = true
break
}
}
if !isMember {
if boardType == model.BoardTypePrivate {
a.wsAdapter.BroadcastMemberDelete(teamID, boardID, user.ID)
} else if boardType == model.BoardTypeOpen {
a.wsAdapter.BroadcastMemberChange(teamID, boardID, &model.BoardMember{UserID: user.ID, BoardID: boardID, SchemeViewer: true, Synthetic: true})
}
}
}
}
func (a *App) DeleteBoard(boardID, userID string) error {
board, err := a.store.GetBoard(boardID)
if model.IsErrNotFound(err) {
return nil
}
if err != nil {
return err
}
if err := a.store.DeleteBoard(boardID, userID); err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID)
return nil
})
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after deleting a board",
mlog.Err(err),
)
}
}()
return nil
}
func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
members, err := a.store.GetMembersForBoard(boardID)
if err != nil {
return nil, err
}
board, err := a.store.GetBoard(boardID)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
if board != nil {
for i, m := range members {
if !m.SchemeAdmin {
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
members[i].SchemeAdmin = true
}
}
}
}
return members, nil
}
func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
members, err := a.store.GetMembersForUser(userID)
if err != nil {
return nil, err
}
for i, m := range members {
if !m.SchemeAdmin {
board, err := a.store.GetBoard(m.BoardID)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
if board != nil {
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
// if system/team admin
members[i].SchemeAdmin = true
}
}
}
}
return members, nil
}
func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) {
return a.store.GetMemberForBoard(boardID, userID)
}
func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
board, err := a.store.GetBoard(member.BoardID)
if model.IsErrNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
if existingMembership != nil && !existingMembership.Synthetic {
return existingMembership, nil
}
newMember, err := a.store.SaveMember(member)
if err != nil {
return nil, err
}
if !newMember.SchemeAdmin {
if board != nil {
if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) {
newMember.SchemeAdmin = true
}
}
}
if !board.IsTemplate {
if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil {
return nil, err
}
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
return nil
})
return newMember, nil
}
func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) {
board, bErr := a.store.GetBoard(member.BoardID)
if model.IsErrNotFound(bErr) {
return nil, nil
}
if bErr != nil {
return nil, bErr
}
oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
if model.IsErrNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
// if we're updating an admin, we need to check that there is at
// least still another admin on the board
if oldMember.SchemeAdmin && !member.SchemeAdmin {
isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID)
if err2 != nil {
return nil, err2
}
if isLastAdmin {
return nil, model.ErrBoardMemberIsLastAdmin
}
}
newMember, err := a.store.SaveMember(member)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
return nil
})
return newMember, nil
}
func (a *App) isLastAdmin(userID, boardID string) (bool, error) {
members, err := a.store.GetMembersForBoard(boardID)
if err != nil {
return false, err
}
for _, m := range members {
if m.SchemeAdmin && m.UserID != userID {
return false, nil
}
}
return true, nil
}
func (a *App) DeleteBoardMember(boardID, userID string) error {
board, bErr := a.store.GetBoard(boardID)
if model.IsErrNotFound(bErr) {
return nil
}
if bErr != nil {
return bErr
}
oldMember, err := a.store.GetMemberForBoard(boardID, userID)
if model.IsErrNotFound(err) {
return nil
}
if err != nil {
return err
}
// if we're removing an admin, we need to check that there is at
// least still another admin on the board
if oldMember.SchemeAdmin {
isLastAdmin, err := a.isLastAdmin(userID, boardID)
if err != nil {
return err
}
if isLastAdmin {
return model.ErrBoardMemberIsLastAdmin
}
}
if err := a.store.DeleteMember(boardID, userID); err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
if syntheticMember, _ := a.GetMemberForBoard(boardID, userID); syntheticMember != nil {
a.wsAdapter.BroadcastMemberChange(board.TeamID, boardID, syntheticMember)
} else {
a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID)
}
return nil
})
return nil
}
func (a *App) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) {
return a.store.SearchBoardsForUser(term, searchField, userID, includePublicBoards)
}
func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {
return a.store.SearchBoardsForUserInTeam(teamID, term, userID)
}
func (a *App) UndeleteBoard(boardID string, modifiedBy string) error {
boards, err := a.store.GetBoardHistory(boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return err
}
if len(boards) == 0 {
// undeleting non-existing board not considered an error
return nil
}
err = a.store.UndeleteBoard(boardID, modifiedBy)
if err != nil {
return err
}
board, err := a.store.GetBoard(boardID)
if err != nil {
return err
}
if board == nil {
a.logger.Error("Error loading the board after undelete, not propagating through websockets or notifications")
return nil
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBoardChange(board.TeamID, board)
return nil
})
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after undeleting a board",
mlog.Err(err),
)
}
}()
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) {
var newBab *model.BoardsAndBlocks
var members []*model.BoardMember
var err error
if addMember {
newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID)
} else {
newBab, err = a.store.CreateBoardsAndBlocks(bab, userID)
}
if err != nil {
return nil, err
}
// all new boards should belong to the same team
teamID := newBab.Boards[0].TeamID
// This can be synchronous because this action is not common
for _, board := range newBab.Boards {
a.wsAdapter.BroadcastBoardChange(teamID, board)
}
for _, block := range newBab.Blocks {
b := block
a.wsAdapter.BroadcastBlockChange(teamID, b)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(b)
a.notifyBlockChanged(notify.Add, b, nil, userID)
}
if addMember {
for _, member := range members {
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
}
}
if len(newBab.Blocks) != 0 {
go func() {
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after creating boards and blocks",
mlog.Err(uErr),
)
}
}()
}
for _, board := range newBab.Boards {
if !board.IsTemplate {
if err := a.addBoardsToDefaultCategory(userID, board.TeamID, []*model.Board{board}); err != nil {
return nil, err
}
}
}
return newBab, nil
}
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
oldBlocks, err := a.store.GetBlocksByIDs(pbab.BlockIDs)
if err != nil {
return nil, err
}
if a.IsCloudLimited() {
containsLimitedBlocks, cErr := a.ContainsLimitedBlocks(oldBlocks)
if cErr != nil {
return nil, cErr
}
if containsLimitedBlocks {
return nil, model.ErrPatchUpdatesLimitedCards
}
}
oldBlocksMap := map[string]*model.Block{}
for _, block := range oldBlocks {
oldBlocksMap[block.ID] = block
}
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
teamID := bab.Boards[0].TeamID
for _, block := range bab.Blocks {
oldBlock, ok := oldBlocksMap[block.ID]
if !ok {
a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID))
continue
}
b := block
a.metrics.IncrementBlocksPatched(1)
a.wsAdapter.BroadcastBlockChange(teamID, b)
a.webhook.NotifyUpdate(b)
a.notifyBlockChanged(notify.Update, b, oldBlock, userID)
}
for _, board := range bab.Boards {
a.wsAdapter.BroadcastBoardChange(board.TeamID, board)
}
return nil
})
return bab, nil
}
func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error {
firstBoard, err := a.store.GetBoard(dbab.Boards[0])
if err != nil {
return err
}
// we need the block entity to notify of the block changes, so we
// fetch and store the blocks first
blocks := []*model.Block{}
for _, blockID := range dbab.Blocks {
block, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
blocks = append(blocks, block)
}
if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
for _, block := range blocks {
a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID)
a.metrics.IncrementBlocksDeleted(1)
a.notifyBlockChanged(notify.Update, block, block, userID)
}
for _, boardID := range dbab.Boards {
a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID)
}
return nil
})
if len(dbab.Blocks) != 0 {
go func() {
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after deleting boards and blocks",
mlog.Err(uErr),
)
}
}()
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func (a *App) CreateCard(card *model.Card, boardID string, userID string, disableNotify bool) (*model.Card, error) {
// Convert the card struct to a block and insert the block.
now := utils.GetMillis()
card.ID = utils.NewID(utils.IDTypeCard)
card.BoardID = boardID
card.CreatedBy = userID
card.ModifiedBy = userID
card.CreateAt = now
card.UpdateAt = now
card.DeleteAt = 0
block := model.Card2Block(card)
newBlocks, err := a.InsertBlocksAndNotify([]*model.Block{block}, userID, disableNotify)
if err != nil {
return nil, fmt.Errorf("cannot create card: %w", err)
}
newCard, err := model.Block2Card(newBlocks[0])
if err != nil {
return nil, err
}
return newCard, nil
}
func (a *App) GetCardsForBoard(boardID string, page int, perPage int) ([]*model.Card, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
BlockType: model.TypeCard,
Page: page,
PerPage: perPage,
}
blocks, err := a.store.GetBlocks(opts)
if err != nil {
return nil, err
}
cards := make([]*model.Card, 0, len(blocks))
var card *model.Card
for _, blk := range blocks {
b := blk
if card, err = model.Block2Card(b); err != nil {
return nil, fmt.Errorf("Block2Card fail: %w", err)
}
cards = append(cards, card)
}
return cards, nil
}
func (a *App) PatchCard(cardPatch *model.CardPatch, cardID string, userID string, disableNotify bool) (*model.Card, error) {
blockPatch, err := model.CardPatch2BlockPatch(cardPatch)
if err != nil {
return nil, err
}
newBlock, err := a.PatchBlockAndNotify(cardID, blockPatch, userID, disableNotify)
if err != nil {
return nil, fmt.Errorf("cannot patch card %s: %w", cardID, err)
}
newCard, err := model.Block2Card(newBlock)
if err != nil {
return nil, err
}
return newCard, nil
}
func (a *App) GetCardByID(cardID string) (*model.Card, error) {
cardBlock, err := a.GetBlockByID(cardID)
if err != nil {
return nil, err
}
card, err := model.Block2Card(cardBlock)
if err != nil {
return nil, err
}
return card, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var errCategoryNotFound = errors.New("category ID specified in input does not exist for user")
var errCategoriesLengthMismatch = errors.New("cannot update category order, passed list of categories different size than in database")
var ErrCannotDeleteSystemCategory = errors.New("cannot delete a system category")
var ErrCannotUpdateSystemCategory = errors.New("cannot update a system category")
func (a *App) GetCategory(categoryID string) (*model.Category, error) {
return a.store.GetCategory(categoryID)
}
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
category.Hydrate()
if err := category.IsValid(); err != nil {
return nil, err
}
if err := a.store.CreateCategory(*category); err != nil {
return nil, err
}
createdCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*createdCategory)
}()
return createdCategory, nil
}
func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) {
category.Hydrate()
if err := category.IsValid(); err != nil {
return nil, err
}
// verify if category belongs to the user
existingCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
if existingCategory.DeleteAt != 0 {
return nil, model.ErrCategoryDeleted
}
if existingCategory.UserID != category.UserID {
return nil, model.ErrCategoryPermissionDenied
}
if existingCategory.TeamID != category.TeamID {
return nil, model.ErrCategoryPermissionDenied
}
// in case type was defaulted above, set to existingCategory.Type
category.Type = existingCategory.Type
if existingCategory.Type == model.CategoryTypeSystem {
// You cannot rename or delete a system category,
// So restoring its name and undeleting it if set so.
category.Name = existingCategory.Name
category.DeleteAt = 0
}
category.UpdateAt = utils.GetMillis()
if err = category.IsValid(); err != nil {
return nil, err
}
if err = a.store.UpdateCategory(*category); err != nil {
return nil, err
}
updatedCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*updatedCategory)
}()
return updatedCategory, nil
}
func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) {
existingCategory, err := a.store.GetCategory(categoryID)
if err != nil {
return nil, err
}
// category is already deleted. This avoids
// overriding the original deleted at timestamp
if existingCategory.DeleteAt != 0 {
return existingCategory, nil
}
// verify if category belongs to the user
if existingCategory.UserID != userID {
return nil, model.ErrCategoryPermissionDenied
}
// verify if category belongs to the team
if existingCategory.TeamID != teamID {
return nil, model.NewErrInvalidCategory("category doesn't belong to the team")
}
if existingCategory.Type == model.CategoryTypeSystem {
return nil, ErrCannotDeleteSystemCategory
}
if err = a.moveBoardsToDefaultCategory(userID, teamID, categoryID); err != nil {
return nil, err
}
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
return nil, err
}
deletedCategory, err := a.store.GetCategory(categoryID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*deletedCategory)
}()
return deletedCategory, nil
}
func (a *App) moveBoardsToDefaultCategory(userID, teamID, sourceCategoryID string) error {
// we need a list of boards associated to this category
// so we can move them to user's default Boards category
categoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var sourceCategoryBoards *model.CategoryBoards
defaultCategoryID := ""
// iterate user's categories to find the source category
// and the default category.
// We need source category to get the list of its board
// and the default category to know its ID to
// move source category's boards to.
for i := range categoryBoards {
if categoryBoards[i].ID == sourceCategoryID {
sourceCategoryBoards = &categoryBoards[i]
}
if categoryBoards[i].Name == defaultCategoryBoards {
defaultCategoryID = categoryBoards[i].ID
}
// if both categories are found, no need to iterate furthur.
if sourceCategoryBoards != nil && defaultCategoryID != "" {
break
}
}
if sourceCategoryBoards == nil {
return errCategoryNotFound
}
if defaultCategoryID == "" {
return fmt.Errorf("moveBoardsToDefaultCategory: %w", errNoDefaultCategoryFound)
}
boardIDs := make([]string, len(sourceCategoryBoards.BoardMetadata))
for i := range sourceCategoryBoards.BoardMetadata {
boardIDs[i] = sourceCategoryBoards.BoardMetadata[i].BoardID
}
if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil {
return fmt.Errorf("moveBoardsToDefaultCategory: %w", err)
}
return nil
}
func (a *App) ReorderCategories(userID, teamID string, newCategoryOrder []string) ([]string, error) {
if err := a.verifyNewCategoriesMatchExisting(userID, teamID, newCategoryOrder); err != nil {
return nil, err
}
newOrder, err := a.store.ReorderCategories(userID, teamID, newCategoryOrder)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryReorder(teamID, userID, newOrder)
}()
return newOrder, nil
}
func (a *App) verifyNewCategoriesMatchExisting(userID, teamID string, newCategoryOrder []string) error {
existingCategories, err := a.store.GetUserCategories(userID, teamID)
if err != nil {
return err
}
if len(newCategoryOrder) != len(existingCategories) {
return fmt.Errorf(
"%w length new categories: %d, length existing categories: %d, userID: %s, teamID: %s",
errCategoriesLengthMismatch,
len(newCategoryOrder),
len(existingCategories),
userID,
teamID,
)
}
existingCategoriesMap := map[string]bool{}
for _, category := range existingCategories {
existingCategoriesMap[category.ID] = true
}
for _, newCategoryID := range newCategoryOrder {
if _, found := existingCategoriesMap[newCategoryID]; !found {
return fmt.Errorf(
"%w specified category ID: %s, userID: %s, teamID: %s",
errCategoryNotFound,
newCategoryID,
userID,
teamID,
)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
const defaultCategoryBoards = "Boards"
var errCategoryBoardsLengthMismatch = errors.New("cannot update category boards order, passed list of categories boards different size than in database")
var errBoardNotFoundInCategory = errors.New("specified board ID not found in specified category ID")
var errBoardMembershipNotFound = errors.New("board membership not found for user's board")
func (a *App) GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) {
categoryBoards, err := a.store.GetUserCategoryBoards(userID, teamID)
if err != nil {
return nil, err
}
createdCategoryBoards, err := a.createDefaultCategoriesIfRequired(categoryBoards, userID, teamID)
if err != nil {
return nil, err
}
categoryBoards = append(categoryBoards, createdCategoryBoards...)
return categoryBoards, nil
}
func (a *App) createDefaultCategoriesIfRequired(existingCategoryBoards []model.CategoryBoards, userID, teamID string) ([]model.CategoryBoards, error) {
createdCategories := []model.CategoryBoards{}
boardsCategoryExist := false
for _, categoryBoard := range existingCategoryBoards {
if categoryBoard.Name == defaultCategoryBoards {
boardsCategoryExist = true
}
}
if !boardsCategoryExist {
createdCategoryBoards, err := a.createBoardsCategory(userID, teamID, existingCategoryBoards)
if err != nil {
return nil, err
}
createdCategories = append(createdCategories, *createdCategoryBoards)
}
return createdCategories, nil
}
func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards []model.CategoryBoards) (*model.CategoryBoards, error) {
// create the category
category := model.Category{
Name: defaultCategoryBoards,
UserID: userID,
TeamID: teamID,
Collapsed: false,
Type: model.CategoryTypeSystem,
SortOrder: len(existingCategoryBoards) * model.CategoryBoardsSortOrderGap,
}
createdCategory, err := a.CreateCategory(&category)
if err != nil {
return nil, fmt.Errorf("createBoardsCategory default category creation failed: %w", err)
}
// once the category is created, we need to move all boards which do not
// belong to any category, into this category.
boardMembers, err := a.GetMembersForUser(userID)
if err != nil {
return nil, fmt.Errorf("createBoardsCategory error fetching user's board memberships: %w", err)
}
boardMemberByBoardID := map[string]*model.BoardMember{}
for _, boardMember := range boardMembers {
boardMemberByBoardID[boardMember.BoardID] = boardMember
}
createdCategoryBoards := &model.CategoryBoards{
Category: *createdCategory,
BoardMetadata: []model.CategoryBoardMetadata{},
}
// get user's current team's baords
userTeamBoards, err := a.GetBoardsForUserAndTeam(userID, teamID, false)
if err != nil {
return nil, fmt.Errorf("createBoardsCategory error fetching user's team's boards: %w", err)
}
boardIDsToAdd := []string{}
for _, board := range userTeamBoards {
boardMembership, ok := boardMemberByBoardID[board.ID]
if !ok {
return nil, fmt.Errorf("createBoardsCategory: %w", errBoardMembershipNotFound)
}
// boards with implicit access (aka synthetic membership),
// should show up in LHS only when openign them explicitelly.
// So we don't process any synthetic membership boards
// and only add boards with explicit access to, to the the LHS,
// for example, if a user explicitelly added another user to a board.
if boardMembership.Synthetic {
continue
}
belongsToCategory := false
for _, categoryBoard := range existingCategoryBoards {
for _, metadata := range categoryBoard.BoardMetadata {
if metadata.BoardID == board.ID {
belongsToCategory = true
break
}
}
// stop looking into other categories if
// the board was found in a category
if belongsToCategory {
break
}
}
if !belongsToCategory {
boardIDsToAdd = append(boardIDsToAdd, board.ID)
newBoardMetadata := model.CategoryBoardMetadata{
BoardID: board.ID,
Hidden: false,
}
createdCategoryBoards.BoardMetadata = append(createdCategoryBoards.BoardMetadata, newBoardMetadata)
}
}
if len(boardIDsToAdd) > 0 {
if err := a.AddUpdateUserCategoryBoard(teamID, userID, createdCategory.ID, boardIDsToAdd); err != nil {
return nil, fmt.Errorf("createBoardsCategory failed to add category-less board to the default category, defaultCategoryID: %s, error: %w", createdCategory.ID, err)
}
}
return createdCategoryBoards, nil
}
func (a *App) AddUpdateUserCategoryBoard(teamID, userID, categoryID string, boardIDs []string) error {
if len(boardIDs) == 0 {
return nil
}
err := a.store.AddUpdateCategoryBoard(userID, categoryID, boardIDs)
if err != nil {
return err
}
userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var updatedCategory *model.CategoryBoards
for i := range userCategoryBoards {
if userCategoryBoards[i].ID == categoryID {
updatedCategory = &userCategoryBoards[i]
break
}
}
if updatedCategory == nil {
return errCategoryNotFound
}
wsPayload := make([]*model.BoardCategoryWebsocketData, len(updatedCategory.BoardMetadata))
i := 0
for _, categoryBoardMetadata := range updatedCategory.BoardMetadata {
wsPayload[i] = &model.BoardCategoryWebsocketData{
BoardID: categoryBoardMetadata.BoardID,
CategoryID: categoryID,
Hidden: categoryBoardMetadata.Hidden,
}
i++
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastCategoryBoardChange(
teamID,
userID,
wsPayload,
)
return nil
})
return nil
}
func (a *App) ReorderCategoryBoards(userID, teamID, categoryID string, newBoardsOrder []string) ([]string, error) {
if err := a.verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID, newBoardsOrder); err != nil {
return nil, err
}
newOrder, err := a.store.ReorderCategoryBoards(categoryID, newBoardsOrder)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryBoardsReorder(teamID, userID, categoryID, newOrder)
}()
return newOrder, nil
}
func (a *App) verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID string, newBoardsOrder []string) error {
// this function is to ensure that we don't miss specifying
// all boards of the category while reordering.
existingCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
if err != nil {
return err
}
var targetCategoryBoards *model.CategoryBoards
for i := range existingCategoryBoards {
if existingCategoryBoards[i].Category.ID == categoryID {
targetCategoryBoards = &existingCategoryBoards[i]
break
}
}
if targetCategoryBoards == nil {
return fmt.Errorf("%w categoryID: %s", errCategoryNotFound, categoryID)
}
if len(targetCategoryBoards.BoardMetadata) != len(newBoardsOrder) {
return fmt.Errorf(
"%w length new category boards: %d, length existing category boards: %d, userID: %s, teamID: %s, categoryID: %s",
errCategoryBoardsLengthMismatch,
len(newBoardsOrder),
len(targetCategoryBoards.BoardMetadata),
userID,
teamID,
categoryID,
)
}
existingBoardMap := map[string]bool{}
for _, metadata := range targetCategoryBoards.BoardMetadata {
existingBoardMap[metadata.BoardID] = true
}
for _, boardID := range newBoardsOrder {
if _, found := existingBoardMap[boardID]; !found {
return fmt.Errorf(
"%w board ID: %s, category ID: %s, userID: %s, teamID: %s",
errBoardNotFoundInCategory,
boardID,
categoryID,
userID,
teamID,
)
}
}
return nil
}
func (a *App) SetBoardVisibility(teamID, userID, categoryID, boardID string, visible bool) error {
if err := a.store.SetBoardVisibility(userID, categoryID, boardID, visible); err != nil {
return fmt.Errorf("SetBoardVisibility: failed to update board visibility: %w", err)
}
a.wsAdapter.BroadcastCategoryBoardChange(teamID, userID, []*model.BoardCategoryWebsocketData{
{
BoardID: boardID,
CategoryID: categoryID,
Hidden: !visible,
},
})
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *App) GetClientConfig() *model.ClientConfig {
return &model.ClientConfig{
Telemetry: a.config.Telemetry,
TelemetryID: a.config.TelemetryID,
EnablePublicSharedBoards: a.config.EnablePublicSharedBoards,
TeammateNameDisplay: a.config.TeammateNameDisplay,
FeatureFlags: a.config.FeatureFlags,
MaxFileSize: a.config.MaxFileSize,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var ErrNilPluginAPI = errors.New("server not running in plugin mode")
// GetBoardsCloudLimits returns the limits of the server, and an empty
// limits struct if there are no limits set.
func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) {
// ToDo: Cloud Limits have been disabled by design. We should
// revisit the decision and update the related code accordingly
/*
if !a.IsCloud() {
return &model.BoardsCloudLimits{}, nil
}
productLimits, err := a.store.GetCloudLimits()
if err != nil {
return nil, err
}
usedCards, err := a.store.GetUsedCardsCount()
if err != nil {
return nil, err
}
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return nil, err
}
boardsCloudLimits := &model.BoardsCloudLimits{
UsedCards: usedCards,
CardLimitTimestamp: cardLimitTimestamp,
}
if productLimits != nil && productLimits.Boards != nil {
if productLimits.Boards.Cards != nil {
boardsCloudLimits.Cards = *productLimits.Boards.Cards
}
if productLimits.Boards.Views != nil {
boardsCloudLimits.Views = *productLimits.Boards.Views
}
}
return boardsCloudLimits, nil
*/
return &model.BoardsCloudLimits{}, nil
}
func (a *App) GetUsedCardsCount() (int, error) {
return a.store.GetUsedCardsCount()
}
// IsCloud returns true if the server is running as a plugin in a
// cloud licensed server.
func (a *App) IsCloud() bool {
return utils.IsCloudLicense(a.store.GetLicense())
}
// IsCloudLimited returns true if the server is running in cloud mode
// and the card limit has been set.
func (a *App) IsCloudLimited() bool {
// ToDo: Cloud Limits have been disabled by design. We should
// revisit the decision and update the related code accordingly
// return a.CardLimit() != 0 && a.IsCloud()
return false
}
// SetCloudLimits sets the limits of the server.
func (a *App) SetCloudLimits(limits *mm_model.ProductLimits) error {
oldCardLimit := a.CardLimit()
// if the limit object doesn't come complete, we assume limits are
// being disabled
cardLimit := 0
if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil {
cardLimit = *limits.Boards.Cards
}
if oldCardLimit != cardLimit {
a.logger.Info(
"setting new cloud limits",
mlog.Int("oldCardLimit", oldCardLimit),
mlog.Int("cardLimit", cardLimit),
)
a.SetCardLimit(cardLimit)
return a.doUpdateCardLimitTimestamp()
}
a.logger.Info(
"setting new cloud limits, equivalent to the existing ones",
mlog.Int("cardLimit", cardLimit),
)
return nil
}
// doUpdateCardLimitTimestamp performs the update without running any
// checks.
func (a *App) doUpdateCardLimitTimestamp() error {
cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit())
if err != nil {
return err
}
a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp)
return nil
}
// UpdateCardLimitTimestamp checks if the server is a cloud instance
// with limits applied, and if that's true, recalculates the card
// limit timestamp and propagates the new one to the connected
// clients.
func (a *App) UpdateCardLimitTimestamp() error {
if !a.IsCloudLimited() {
return nil
}
return a.doUpdateCardLimitTimestamp()
}
// getTemplateMapForBlocks gets all board ids for the blocks, and
// builds a map with the board IDs as the key and their isTemplate
// field as the value.
func (a *App) getTemplateMapForBlocks(blocks []*model.Block) (map[string]bool, error) {
boardMap := map[string]*model.Board{}
for _, block := range blocks {
if _, ok := boardMap[block.BoardID]; !ok {
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return nil, err
}
boardMap[block.BoardID] = board
}
}
templateMap := map[string]bool{}
for boardID, board := range boardMap {
templateMap[boardID] = board.IsTemplate
}
return templateMap, nil
}
// ApplyCloudLimits takes a set of blocks and, if the server is cloud
// limited, limits those that are outside of the card limit and don't
// belong to a template.
func (a *App) ApplyCloudLimits(blocks []*model.Block) ([]*model.Block, error) {
// if there is no limit currently being applied, return
if !a.IsCloudLimited() {
return blocks, nil
}
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return nil, err
}
templateMap, err := a.getTemplateMapForBlocks(blocks)
if err != nil {
return nil, err
}
limitedBlocks := make([]*model.Block, len(blocks))
for i, block := range blocks {
// if the block belongs to a template, it will never be
// limited
if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate {
limitedBlocks[i] = block
continue
}
if block.ShouldBeLimited(cardLimitTimestamp) {
limitedBlocks[i] = block.GetLimited()
} else {
limitedBlocks[i] = block
}
}
return limitedBlocks, nil
}
// ContainsLimitedBlocks checks if a list of blocks contain any block
// that references a limited card.
func (a *App) ContainsLimitedBlocks(blocks []*model.Block) (bool, error) {
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return false, err
}
if cardLimitTimestamp == 0 {
return false, nil
}
cards := []*model.Block{}
cardIDMap := map[string]bool{}
for _, block := range blocks {
switch block.Type {
case model.TypeCard:
cards = append(cards, block)
default:
cardIDMap[block.ParentID] = true
}
}
cardIDs := []string{}
// if the card is already present on the set, we don't need to
// fetch it from the database
for cardID := range cardIDMap {
alreadyPresent := false
for _, card := range cards {
if card.ID == cardID {
alreadyPresent = true
break
}
}
if !alreadyPresent {
cardIDs = append(cardIDs, cardID)
}
}
if len(cardIDs) > 0 {
fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs)
if fErr != nil {
return false, fErr
}
cards = append(cards, fetchedCards...)
}
templateMap, err := a.getTemplateMapForBlocks(cards)
if err != nil {
return false, err
}
for _, card := range cards {
isTemplate, ok := templateMap[card.BoardID]
if !ok {
return false, newErrBoardNotFoundInTemplateMap(card.BoardID)
}
// if the block belongs to a template, it will never be
// limited
if isTemplate {
continue
}
if card.ShouldBeLimited(cardLimitTimestamp) {
return true, nil
}
}
return false, nil
}
type errBoardNotFoundInTemplateMap struct {
id string
}
func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap {
return &errBoardNotFoundInTemplateMap{id}
}
func (eb *errBoardNotFoundInTemplateMap) Error() string {
return fmt.Sprintf("board %q not found in template map", eb.id)
}
func (a *App) NotifyPortalAdminsUpgradeRequest(teamID string) error {
if a.servicesAPI == nil {
return ErrNilPluginAPI
}
team, err := a.store.GetTeam(teamID)
if err != nil {
return err
}
var ofWhat string
if team == nil {
ofWhat = "your organization"
} else {
ofWhat = team.Title
}
message := fmt.Sprintf("A member of %s has notified you to upgrade this workspace before the trial ends.", ofWhat)
page := 0
getUsersOptions := &mm_model.UserGetOptions{
Active: true,
Role: mm_model.SystemAdminRoleId,
PerPage: 50,
Page: page,
}
for ; true; page++ {
getUsersOptions.Page = page
systemAdmins, appErr := a.servicesAPI.GetUsersFromProfiles(getUsersOptions)
if appErr != nil {
a.logger.Error("failed to fetch system admins", mlog.Int("page_size", getUsersOptions.PerPage), mlog.Int("page", page), mlog.Err(appErr))
return appErr
}
if len(systemAdmins) == 0 {
break
}
receiptUserIDs := []string{}
for _, systemAdmin := range systemAdmins {
receiptUserIDs = append(receiptUserIDs, systemAdmin.Id)
}
if err := a.store.SendMessage(message, "custom_cloud_upgrade_nudge", receiptUserIDs); err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "github.com/mattermost/mattermost-server/v6/server/boards/model"
func (a *App) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
return a.store.GetBoardsForCompliance(opts)
}
func (a *App) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
return a.store.GetBoardsComplianceHistory(opts)
}
func (a *App) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
return a.store.GetBlocksComplianceHistory(opts)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *App) MoveContentBlock(block *model.Block, dstBlock *model.Block, where string, userID string) error {
if block.ParentID != dstBlock.ParentID {
message := fmt.Sprintf("not matching parent %s and %s", block.ParentID, dstBlock.ParentID)
return model.NewErrBadRequest(message)
}
card, err := a.GetBlockByID(block.ParentID)
if err != nil {
return err
}
contentOrderData, ok := card.Fields["contentOrder"]
var contentOrder []interface{}
if ok {
contentOrder = contentOrderData.([]interface{})
}
newContentOrder := []interface{}{}
foundDst := false
foundSrc := false
for _, id := range contentOrder {
stringID, ok := id.(string)
if !ok {
newContentOrder = append(newContentOrder, id)
continue
}
if dstBlock.ID == stringID {
foundDst = true
if where == "after" {
newContentOrder = append(newContentOrder, id)
newContentOrder = append(newContentOrder, block.ID)
} else {
newContentOrder = append(newContentOrder, block.ID)
newContentOrder = append(newContentOrder, id)
}
continue
}
if block.ID == stringID {
foundSrc = true
continue
}
newContentOrder = append(newContentOrder, id)
}
if !foundSrc {
message := fmt.Sprintf("source block %s not found", block.ID)
return model.NewErrBadRequest(message)
}
if !foundDst {
message := fmt.Sprintf("destination block %s not found", dstBlock.ID)
return model.NewErrBadRequest(message)
}
patch := &model.BlockPatch{
UpdatedFields: map[string]interface{}{
"contentOrder": newContentOrder,
},
}
_, err = a.PatchBlock(block.ParentID, patch, userID)
if errors.Is(err, model.ErrPatchUpdatesLimitedCards) {
return err
}
if err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
newline = []byte{'\n'}
)
func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
boards, err := a.getBoardsForArchive(opt.BoardIDs)
if err != nil {
return err
}
merr := merror.New()
defer func() {
errs = merr.ErrorOrNil()
}()
// wrap the writer in a zip.
zw := zip.NewWriter(w)
defer func() {
merr.Append(zw.Close())
}()
if err := a.writeArchiveVersion(zw); err != nil {
merr.Append(err)
return
}
for _, board := range boards {
if err := a.writeArchiveBoard(zw, board, opt); err != nil {
merr.Append(fmt.Errorf("cannot export board %s: %w", board.ID, err))
return
}
}
return nil
}
// writeArchiveVersion writes a version file to the zip.
func (a *App) writeArchiveVersion(zw *zip.Writer) error {
archiveHeader := model.ArchiveHeader{
Version: archiveVersion,
Date: model.GetMillis(),
}
b, _ := json.Marshal(&archiveHeader)
w, err := zw.Create("version.json")
if err != nil {
return fmt.Errorf("cannot write archive header: %w", err)
}
if _, err := w.Write(b); err != nil {
return fmt.Errorf("cannot write archive header: %w", err)
}
return nil
}
// writeArchiveBoard writes a single board to the archive in a zip directory.
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
// create a directory per board
w, err := zw.Create(board.ID + "/board.jsonl")
if err != nil {
return err
}
// write the board block first
if err = a.writeArchiveBoardLine(w, board); err != nil {
return err
}
var files []string
// write the board's blocks
// TODO: paginate this
blocks, err := a.GetBlocksForBoard(board.ID)
if err != nil {
return err
}
for _, block := range blocks {
if err = a.writeArchiveBlockLine(w, block); err != nil {
return err
}
if block.Type == model.TypeImage {
filename, err2 := extractImageFilename(block)
if err2 != nil {
return err
}
files = append(files, filename)
}
}
boardMembers, err := a.GetMembersForBoard(board.ID)
if err != nil {
return err
}
for _, boardMember := range boardMembers {
if err = a.writeArchiveBoardMemberLine(w, boardMember); err != nil {
return err
}
}
// write the files
for _, filename := range files {
if err := a.writeArchiveFile(zw, filename, board.ID, opt); err != nil {
return fmt.Errorf("cannot write file %s to archive: %w", filename, err)
}
}
return nil
}
// writeArchiveBoardMemberLine writes a single boardMember to the archive.
func (a *App) writeArchiveBoardMemberLine(w io.Writer, boardMember *model.BoardMember) error {
bm, err := json.Marshal(&boardMember)
if err != nil {
return err
}
line := model.ArchiveLine{
Type: "boardMember",
Data: bm,
}
bm, err = json.Marshal(&line)
if err != nil {
return err
}
_, err = w.Write(bm)
if err != nil {
return err
}
_, err = w.Write(newline)
return err
}
// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBlockLine(w io.Writer, block *model.Block) error {
b, err := json.Marshal(&block)
if err != nil {
return err
}
line := model.ArchiveLine{
Type: "block",
Data: b,
}
b, err = json.Marshal(&line)
if err != nil {
return err
}
_, err = w.Write(b)
if err != nil {
return err
}
// jsonl files need a newline
_, err = w.Write(newline)
return err
}
// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error {
b, err := json.Marshal(&board)
if err != nil {
return err
}
line := model.ArchiveLine{
Type: "board",
Data: b,
}
b, err = json.Marshal(&line)
if err != nil {
return err
}
_, err = w.Write(b)
if err != nil {
return err
}
// jsonl files need a newline
_, err = w.Write(newline)
return err
}
// writeArchiveFile writes a single file to the archive.
func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error {
dest, err := zw.Create(boardID + "/" + filename)
if err != nil {
return err
}
src, err := a.GetFileReader(opt.TeamID, boardID, filename)
if err != nil {
// just log this; image file is missing but we'll still export an equivalent board
a.logger.Error("image file missing for export",
mlog.String("filename", filename),
mlog.String("team_id", opt.TeamID),
mlog.String("board_id", boardID),
)
return nil
}
defer src.Close()
_, err = io.Copy(dest, src)
return err
}
// getBoardsForArchive fetches all the specified boards.
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
boards := make([]model.Board, 0, len(boardIDs))
for _, id := range boardIDs {
b, err := a.GetBoard(id)
if err != nil {
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
}
boards = append(boards, *b)
}
return boards, nil
}
func extractImageFilename(imageBlock *model.Block) (string, error) {
f, ok := imageBlock.Fields["fileId"]
if !ok {
return "", model.ErrInvalidImageBlock
}
filename, ok := f.(string)
if !ok {
return "", model.ErrInvalidImageBlock
}
return filename, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const emptyString = "empty"
var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed")
var ErrFileNotFound = errors.New("file not found")
func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) {
// NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == ".jpeg" {
fileExtension = ".jpg"
}
createdFilename := utils.NewID(utils.IDTypeNone)
fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename)
fileSize, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil {
return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr)
}
now := utils.GetMillis()
fileInfo := &mm_model.FileInfo{
Id: createdFilename[1:],
CreatorId: "boards",
PostId: emptyString,
ChannelId: emptyString,
CreateAt: now,
UpdateAt: now,
DeleteAt: 0,
Path: filePath,
ThumbnailPath: emptyString,
PreviewPath: emptyString,
Name: filename,
Extension: fileExtension,
Size: fileSize,
MimeType: emptyString,
Width: 0,
Height: 0,
HasPreviewImage: false,
MiniPreview: nil,
Content: "",
RemoteId: nil,
}
err := a.store.SaveFileInfo(fileInfo)
if err != nil {
return "", err
}
return fullFilename, nil
}
func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) {
if filename == "" {
return nil, errEmptyFilename
}
// filename is in the format 7<some-alphanumeric-string>.<extension>
// we want to extract the <some-alphanumeric-string> part of this as this
// will be the fileinfo id.
parts := strings.Split(filename, ".")
fileInfoID := parts[0][1:]
fileInfo, err := a.store.GetFileInfo(fileInfoID)
if err != nil {
return nil, err
}
return fileInfo, nil
}
func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, filestore.ReadCloseSeeker, error) {
fileInfo, err := a.GetFileInfo(fileName)
if err != nil && !model.IsErrNotFound(err) {
a.logger.Error("111")
return nil, nil, err
}
var filePath string
if fileInfo != nil && fileInfo.Path != "" {
filePath = fileInfo.Path
} else {
filePath = filepath.Join(teamID, rootID, fileName)
}
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to check if file exists as path. Path: %s, error: %e", filePath, err))
return nil, nil, err
}
if !exists {
return nil, nil, ErrFileNotFound
}
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err))
return nil, nil, err
}
return fileInfo, reader, nil
}
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
filePath := filepath.Join(teamID, rootID, filename)
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
return nil, err
}
// FIXUP: Check the deprecated old location
if teamID == "0" && !exists {
oldExists, err2 := a.filesBackend.FileExists(filename)
if err2 != nil {
return nil, err2
}
if oldExists {
err2 := a.filesBackend.MoveFile(filename, filePath)
if err2 != nil {
a.logger.Error("ERROR moving file",
mlog.String("old", filename),
mlog.String("new", filePath),
mlog.Err(err2),
)
} else {
a.logger.Debug("Moved file",
mlog.String("old", filename),
mlog.String("new", filePath),
)
}
}
} else if !exists {
return nil, ErrFileNotFound
}
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
return nil, err
}
return reader, nil
}
func (a *App) MoveFile(channelID, teamID, boardID, filename string) error {
oldPath := filepath.Join(channelID, boardID, filename)
newPath := filepath.Join(teamID, boardID, filename)
err := a.filesBackend.MoveFile(oldPath, newPath)
if err != nil {
a.logger.Error("ERROR moving file",
mlog.String("old", oldPath),
mlog.String("new", newPath),
mlog.Err(err),
)
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"path/filepath"
"strings"
"github.com/krolaw/zipstream"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
archiveVersion = 2
legacyFileBegin = "{\"version\":1"
)
var (
errBlockIsNotABoard = errors.New("block is not a board")
)
// ImportArchive imports an archive containing zero or more boards, plus all
// associated content, including cards, content blocks, views, and images.
//
// Archives are ZIP files containing a `version.json` file and zero or more
// directories, each containing a `board.jsonl` and zero or more image files.
func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
// peek at the first bytes to see if this is a legacy archive format
br := bufio.NewReader(r)
peek, err := br.Peek(len(legacyFileBegin))
if err == nil && string(peek) == legacyFileBegin {
a.logger.Debug("importing legacy archive")
_, errImport := a.ImportBoardJSONL(br, opt)
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after importing a legacy file",
mlog.Err(err),
)
}
}()
return errImport
}
a.logger.Debug("importing archive")
zr := zipstream.NewReader(br)
boardMap := make(map[string]string) // maps old board ids to new
for {
hdr, err := zr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
a.logger.Debug("import archive - done", mlog.Int("boards_imported", len(boardMap)))
return nil
}
return err
}
dir, filename := filepath.Split(hdr.Name)
dir = path.Clean(dir)
switch filename {
case "version.json":
ver, errVer := parseVersionFile(zr)
if errVer != nil {
return errVer
}
if ver != archiveVersion {
return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion)
}
case "board.jsonl":
boardID, err := a.ImportBoardJSONL(zr, opt)
if err != nil {
return fmt.Errorf("cannot import board %s: %w", dir, err)
}
boardMap[dir] = boardID
default:
// import file/image; dir is the old board id
boardID, ok := boardMap[dir]
if !ok {
a.logger.Warn("skipping orphan image in archive",
mlog.String("dir", dir),
mlog.String("filename", filename),
)
continue
}
// save file with original filename so it matches name in image block.
filePath := filepath.Join(opt.TeamID, boardID, filename)
_, err := a.filesBackend.WriteFile(zr, filePath)
if err != nil {
return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err)
}
}
a.logger.Trace("import archive file",
mlog.String("dir", dir),
mlog.String("filename", filename),
)
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after importing an archive",
mlog.Err(err),
)
}
}()
}
}
// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting
// board id is returned.
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) {
// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks.
// We don't want to load the whole file in memory, even though it's a single board.
boardsAndBlocks := &model.BoardsAndBlocks{
Blocks: make([]*model.Block, 0, 10),
Boards: make([]*model.Board, 0, 10),
}
lineReader := bufio.NewReader(r)
userID := opt.ModifiedBy
if userID == model.SingleUser {
userID = ""
}
now := utils.GetMillis()
var boardID string
var boardMembers []*model.BoardMember
lineNum := 1
firstLine := true
for {
line, errRead := readLine(lineReader)
if len(line) != 0 {
var skip bool
if firstLine {
// first line might be a header tag (old archive format)
if strings.HasPrefix(string(line), legacyFileBegin) {
skip = true
}
}
if !skip {
var archiveLine model.ArchiveLine
if err := json.Unmarshal(line, &archiveLine); err != nil {
return "", fmt.Errorf("error parsing archive line %d: %w", lineNum, err)
}
// first line must be a board
if firstLine && archiveLine.Type == "block" {
archiveLine.Type = "board_block"
}
switch archiveLine.Type {
case "board":
var board model.Board
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
return "", fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2)
}
board.ModifiedBy = userID
board.UpdateAt = now
board.TeamID = opt.TeamID
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board)
boardID = board.ID
case "board_block":
// legacy archives encoded boards as blocks; we need to convert them to real boards.
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return "", fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2)
}
block.ModifiedBy = userID
block.UpdateAt = now
board, err := a.blockToBoard(block, opt)
if err != nil {
return "", fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err)
}
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board)
boardID = board.ID
case "block":
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
}
block.ModifiedBy = userID
block.UpdateAt = now
block.BoardID = boardID
boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block)
case "boardMember":
var boardMember *model.BoardMember
if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil {
return "", fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
}
boardMembers = append(boardMembers, boardMember)
default:
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
firstLine = false
}
}
if errRead != nil {
if errors.Is(errRead, io.EOF) {
break
}
return "", fmt.Errorf("error reading archive line %d: %w", lineNum, errRead)
}
lineNum++
}
// loop to remove the people how are not part of the team and system
for i := len(boardMembers) - 1; i >= 0; i-- {
if _, err := a.GetUser(boardMembers[i].UserID); err != nil {
boardMembers = append(boardMembers[:i], boardMembers[i+1:]...)
}
}
a.fixBoardsandBlocks(boardsAndBlocks, opt)
var err error
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
if err != nil {
return "", fmt.Errorf("error generating archive block IDs: %w", err)
}
boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false)
if err != nil {
return "", fmt.Errorf("error inserting archive blocks: %w", err)
}
// add users to all the new boards (if not the fake system user).
for _, board := range boardsAndBlocks.Boards {
// make sure an admin user gets added
adminMember := &model.BoardMember{
BoardID: board.ID,
UserID: opt.ModifiedBy,
SchemeAdmin: true,
}
if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil {
return "", fmt.Errorf("cannot add adminMember to board: %w", err2)
}
for _, boardMember := range boardMembers {
bm := &model.BoardMember{
BoardID: board.ID,
UserID: boardMember.UserID,
Roles: boardMember.Roles,
MinimumRole: boardMember.MinimumRole,
SchemeAdmin: boardMember.SchemeAdmin,
SchemeEditor: boardMember.SchemeEditor,
SchemeCommenter: boardMember.SchemeCommenter,
SchemeViewer: boardMember.SchemeViewer,
Synthetic: boardMember.Synthetic,
}
if _, err2 := a.AddMemberToBoard(bm); err2 != nil {
return "", fmt.Errorf("cannot add member to board: %w", err2)
}
}
}
// find new board id
for _, board := range boardsAndBlocks.Boards {
return board.ID, nil
}
return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock)
}
// fixBoardsandBlocks allows the caller of `ImportArchive` to modify or filters boards and blocks being
// imported via callbacks.
func (a *App) fixBoardsandBlocks(boardsAndBlocks *model.BoardsAndBlocks, opt model.ImportArchiveOptions) {
if opt.BlockModifier == nil && opt.BoardModifier == nil {
return
}
modInfoCache := make(map[string]interface{})
modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards))
modBlocks := make([]*model.Block, 0, len(boardsAndBlocks.Blocks))
for _, board := range boardsAndBlocks.Boards {
b := *board
if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) {
a.logger.Debug("skipping insert board per board modifier",
mlog.String("boardID", board.ID),
)
continue
}
modBoards = append(modBoards, &b)
}
for _, block := range boardsAndBlocks.Blocks {
b := block
if opt.BlockModifier != nil && !opt.BlockModifier(b, modInfoCache) {
a.logger.Debug("skipping insert block per block modifier",
mlog.String("blockID", block.ID),
)
continue
}
modBlocks = append(modBlocks, b)
}
boardsAndBlocks.Boards = modBoards
boardsAndBlocks.Blocks = modBlocks
}
// blockToBoard converts a `model.Block` to `model.Board`. Legacy archive formats encode boards as blocks
// and need conversion during import.
func (a *App) blockToBoard(block *model.Block, opt model.ImportArchiveOptions) (*model.Board, error) {
if block.Type != model.TypeBoard {
return nil, errBlockIsNotABoard
}
board := &model.Board{
ID: block.ID,
TeamID: opt.TeamID,
CreatedBy: block.CreatedBy,
ModifiedBy: block.ModifiedBy,
Type: model.BoardTypePrivate,
Title: block.Title,
CreateAt: block.CreateAt,
UpdateAt: block.UpdateAt,
DeleteAt: block.DeleteAt,
Properties: make(map[string]interface{}),
CardProperties: make([]map[string]interface{}, 0),
}
if icon, ok := stringValue(block.Fields, "icon"); ok {
board.Icon = icon
}
if description, ok := stringValue(block.Fields, "description"); ok {
board.Description = description
}
if showDescription, ok := boolValue(block.Fields, "showDescription"); ok {
board.ShowDescription = showDescription
}
if isTemplate, ok := boolValue(block.Fields, "isTemplate"); ok {
board.IsTemplate = isTemplate
}
if templateVer, ok := intValue(block.Fields, "templateVer"); ok {
board.TemplateVersion = templateVer
}
if properties, ok := mapValue(block.Fields, "properties"); ok {
board.Properties = properties
}
if cardProperties, ok := arrayMapsValue(block.Fields, "cardProperties"); ok {
board.CardProperties = cardProperties
}
return board, nil
}
func stringValue(m map[string]interface{}, key string) (string, bool) {
v, ok := m[key]
if !ok {
return "", false
}
s, ok := v.(string)
if !ok {
return "", false
}
return s, true
}
func boolValue(m map[string]interface{}, key string) (bool, bool) {
v, ok := m[key]
if !ok {
return false, false
}
b, ok := v.(bool)
if !ok {
return false, false
}
return b, true
}
func intValue(m map[string]interface{}, key string) (int, bool) {
v, ok := m[key]
if !ok {
return 0, false
}
i, ok := v.(int)
if !ok {
return 0, false
}
return i, true
}
func mapValue(m map[string]interface{}, key string) (map[string]interface{}, bool) {
v, ok := m[key]
if !ok {
return nil, false
}
mm, ok := v.(map[string]interface{})
if !ok {
return nil, false
}
return mm, true
}
func arrayMapsValue(m map[string]interface{}, key string) ([]map[string]interface{}, bool) {
v, ok := m[key]
if !ok {
return nil, false
}
ai, ok := v.([]interface{})
if !ok {
return nil, false
}
arr := make([]map[string]interface{}, 0, len(ai))
for _, mi := range ai {
mm, ok := mi.(map[string]interface{})
if !ok {
return nil, false
}
arr = append(arr, mm)
}
return arr, true
}
func parseVersionFile(r io.Reader) (int, error) {
file, err := io.ReadAll(r)
if err != nil {
return 0, fmt.Errorf("cannot read version.json: %w", err)
}
var header model.ArchiveHeader
if err := json.Unmarshal(file, &header); err != nil {
return 0, fmt.Errorf("cannot parse version.json: %w", err)
}
return header.Version, nil
}
func readLine(r *bufio.Reader) ([]byte, error) {
line, err := r.ReadBytes('\n')
line = bytes.TrimSpace(line)
return line, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// initialize is called when the App is first created.
func (a *App) initialize(skipTemplateInit bool) {
if !skipTemplateInit {
if err := a.InitTemplates(); err != nil {
a.logger.Error(`InitializeTemplates failed`, mlog.Err(err))
}
}
}
func (a *App) Shutdown() {
if a.blockChangeNotifier != nil {
ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout)
defer cancel()
if !a.blockChangeNotifier.Shutdown(ctx) {
a.logger.Warn("blockChangeNotifier shutdown timed out")
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/pkg/errors"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *App) GetTeamBoardsInsights(userID string, teamID string, opts *mm_model.InsightsOpts) (*model.BoardInsightsList, error) {
// check if server is properly licensed, and user is not a guest
userPermitted, err := insightPermissionGate(a, userID, false)
if err != nil {
return nil, err
}
if !userPermitted {
return nil, errors.New("User isn't authorized to access insights.")
}
boardIDs, err := getUserBoards(userID, teamID, a)
if err != nil {
return nil, err
}
return a.store.GetTeamBoardsInsights(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs)
}
func (a *App) GetUserBoardsInsights(userID string, teamID string, opts *mm_model.InsightsOpts) (*model.BoardInsightsList, error) {
// check if server is properly licensed, and user is not a guest
userPermitted, err := insightPermissionGate(a, userID, true)
if err != nil {
return nil, err
}
if !userPermitted {
return nil, errors.New("User isn't authorized to access insights.")
}
boardIDs, err := getUserBoards(userID, teamID, a)
if err != nil {
return nil, err
}
return a.store.GetUserBoardsInsights(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs)
}
func insightPermissionGate(a *App, userID string, isMyInsights bool) (bool, error) {
licenseError := errors.New("invalid license/authorization to use insights API")
guestError := errors.New("guests aren't authorized to use insights API")
lic := a.store.GetLicense()
user, err := a.store.GetUserByID(userID)
if err != nil {
return false, err
}
if user.IsGuest {
return false, guestError
}
if lic == nil && !isMyInsights {
a.logger.Debug("Deployment doesn't have a license")
return false, licenseError
}
if !isMyInsights && (lic.SkuShortName != mm_model.LicenseShortSkuProfessional && lic.SkuShortName != mm_model.LicenseShortSkuEnterprise) {
return false, licenseError
}
return true, nil
}
func (a *App) GetUserTimezone(userID string) (string, error) {
return a.store.GetUserTimezone(userID)
}
func getUserBoards(userID string, teamID string, a *App) ([]string, error) {
// get boards accessible by user and filter boardIDs
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID, true)
if err != nil {
return nil, errors.New("error getting boards for user")
}
boardIDs := make([]string, 0, len(boards))
for _, board := range boards {
boardIDs = append(boardIDs, board.ID)
}
return boardIDs, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
const (
KeyOnboardingTourStarted = "onboardingTourStarted"
KeyOnboardingTourCategory = "tourCategory"
KeyOnboardingTourStep = "onboardingTourStep"
ValueOnboardingFirstStep = "0"
ValueTourCategoryOnboarding = "onboarding"
WelcomeBoardTitle = "Welcome to Boards!"
)
var (
errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks")
errCannotCreateBoard = errors.New("new board wasn't created")
)
func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) {
// copy the welcome board into this workspace
boardID, err := a.createWelcomeBoard(userID, teamID)
if err != nil {
return "", "", err
}
// set user's tour state to initial state
userPreferencesPatch := model.UserPreferencesPatch{
UpdatedFields: map[string]string{
KeyOnboardingTourStarted: "1",
KeyOnboardingTourStep: ValueOnboardingFirstStep,
KeyOnboardingTourCategory: ValueTourCategoryOnboarding,
},
}
if _, err := a.store.PatchUserPreferences(userID, userPreferencesPatch); err != nil {
return "", "", err
}
return teamID, boardID, nil
}
func (a *App) getOnboardingBoardID() (string, error) {
boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "")
if err != nil {
return "", err
}
var onboardingBoardID string
for _, block := range boards {
if block.Title == WelcomeBoardTitle && block.TeamID == model.GlobalTeamID {
onboardingBoardID = block.ID
break
}
}
if onboardingBoardID == "" {
return "", errUnableToFindWelcomeBoard
}
return onboardingBoardID, nil
}
func (a *App) createWelcomeBoard(userID, teamID string) (string, error) {
onboardingBoardID, err := a.getOnboardingBoardID()
if err != nil {
return "", err
}
bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false)
if err != nil {
return "", err
}
if len(bab.Boards) != 1 {
return "", errCannotCreateBoard
}
// need variable for this to
// get reference for board patch
newType := model.BoardTypePrivate
patch := &model.BoardPatch{
Type: &newType,
}
if _, err := a.PatchBoard(patch, bab.Boards[0].ID, userID); err != nil {
return "", err
}
return bab.Boards[0].ID, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool {
return a.permissions.HasPermissionToBoard(userID, boardID, permission)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"runtime"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
type ServerMetadata struct {
Version string `json:"version"`
BuildNumber string `json:"build_number"`
BuildDate string `json:"build_date"`
Commit string `json:"commit"`
Edition string `json:"edition"`
DBType string `json:"db_type"`
DBVersion string `json:"db_version"`
OSType string `json:"os_type"`
OSArch string `json:"os_arch"`
SKU string `json:"sku"`
}
func (a *App) GetServerMetadata() *ServerMetadata {
var dbType string
var dbVersion string
if a != nil && a.store != nil {
dbType = a.store.DBType()
dbVersion = a.store.DBVersion()
}
return &ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: dbType,
DBVersion: dbVersion,
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_server",
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *App) GetSharing(boardID string) (*model.Sharing, error) {
sharing, err := a.store.GetSharing(boardID)
if err != nil {
return nil, err
}
return sharing, nil
}
func (a *App) UpsertSharing(sharing model.Sharing) error {
return a.store.UpsertSharing(sharing)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
sub, err := a.store.CreateSubscription(sub)
if err != nil {
return nil, err
}
a.notifySubscriptionChanged(sub)
return sub, nil
}
func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) {
sub, err := a.store.GetSubscription(blockID, subscriberID)
if err != nil {
return nil, err
}
if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil {
return nil, err
}
sub.DeleteAt = utils.GetMillis()
a.notifySubscriptionChanged(sub)
return sub, nil
}
func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) {
return a.store.GetSubscriptions(subscriberID)
}
func (a *App) notifySubscriptionChanged(subscription *model.Subscription) {
if a.notifications == nil {
return
}
board, err := a.getBoardForBlock(subscription.BlockID)
if err != nil {
a.logger.Error("Error notifying subscription change",
mlog.String("subscriber_id", subscription.SubscriberID),
mlog.String("block_id", subscription.BlockID),
mlog.Err(err),
)
}
a.wsAdapter.BroadcastSubscriptionChange(board.TeamID, subscription)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetRootTeam() (*model.Team, error) {
teamID := "0"
team, _ := a.store.GetTeam(teamID)
if team == nil {
team = &model.Team{
ID: teamID,
SignupToken: utils.NewID(utils.IDTypeToken),
}
err := a.store.UpsertTeamSignupToken(*team)
if err != nil {
a.logger.Error("Unable to initialize team", mlog.Err(err))
return nil, err
}
team, err = a.store.GetTeam(teamID)
if err != nil {
a.logger.Error("Unable to get initialized team", mlog.Err(err))
return nil, err
}
a.logger.Info("initialized team")
}
return team, nil
}
func (a *App) GetTeam(id string) (*model.Team, error) {
team, err := a.store.GetTeam(id)
if model.IsErrNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
return team, nil
}
func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) {
return a.store.GetTeamsForUser(userID)
}
func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool {
return a.auth.DoesUserHaveTeamAccess(userID, teamID)
}
func (a *App) UpsertTeamSettings(team model.Team) error {
return a.store.UpsertTeamSettings(team)
}
func (a *App) UpsertTeamSignupToken(team model.Team) error {
return a.store.UpsertTeamSignupToken(team)
}
func (a *App) GetTeamCount() (int64, error) {
return a.store.GetTeamCount()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/assets"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defaultTemplateVersion = 6 // bump this number to force default templates to be re-imported
)
func (a *App) InitTemplates() error {
_, err := a.initializeTemplates()
return err
}
// initializeTemplates imports default templates if the boards table is empty.
func (a *App) initializeTemplates() (bool, error) {
boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "")
if err != nil {
return false, fmt.Errorf("cannot initialize templates: %w", err)
}
a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards)))
isNeeded, reason := a.isInitializationNeeded(boards)
if !isNeeded {
a.logger.Debug("Template import not needed, skipping")
return false, nil
}
a.logger.Debug("Importing new default templates",
mlog.String("reason", reason),
mlog.Int("size", len(assets.DefaultTemplatesArchive)),
)
// Remove in case of newer Templates
if err = a.store.RemoveDefaultTemplates(boards); err != nil {
return false, fmt.Errorf("cannot remove old template boards: %w", err)
}
r := bytes.NewReader(assets.DefaultTemplatesArchive)
opt := model.ImportArchiveOptions{
TeamID: model.GlobalTeamID,
ModifiedBy: model.SystemUserID,
BlockModifier: fixTemplateBlock,
BoardModifier: fixTemplateBoard,
}
if err = a.ImportArchive(r, opt); err != nil {
return false, fmt.Errorf("cannot initialize global templates for team %s: %w", model.GlobalTeamID, err)
}
return true, nil
}
// isInitializationNeeded returns true if the blocks table contains no default templates,
// or contains at least one default template with an old version number.
func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) {
if len(boards) == 0 {
return true, "no default templates found"
}
// look for any built-in template boards with the wrong version number (or no version #).
for _, board := range boards {
// if not built-in board...skip
if board.CreatedBy != model.SystemUserID {
continue
}
if board.TemplateVersion < defaultTemplateVersion {
return true, "template_version too old"
}
}
return false, ""
}
// fixTemplateBlock fixes a block to be inserted as part of a template.
func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool {
// cache contains ids of skipped boards. Ensure their children are skipped as well.
if _, ok := cache[block.BoardID]; ok {
cache[block.ID] = struct{}{}
return false
}
if _, ok := cache[block.ParentID]; ok {
cache[block.ID] = struct{}{}
return false
}
return true
}
// fixTemplateBoard fixes a board to be inserted as part of a template.
func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool {
// filter out template blocks; we only want the non-template
// blocks which we will turn into default template blocks.
if board.IsTemplate {
cache[board.ID] = struct{}{}
return false
}
// remove '(NEW)' from title & force template flag
board.Title = strings.ReplaceAll(board.Title, "(NEW)", "")
board.IsTemplate = true
board.TemplateVersion = defaultTemplateVersion
board.Type = model.BoardTypeOpen
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, error) {
return a.store.GetUsersByTeam(teamID, asGuestID, a.config.ShowEmailAddress, a.config.ShowFullName)
}
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) {
users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName)
if err != nil {
return nil, err
}
for i, u := range users {
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
}
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
}
}
return users, nil
}
func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mm_model.Preference, error) {
updatedPreferences, err := a.store.PatchUserPreferences(userID, patch)
if err != nil {
return nil, err
}
return updatedPreferences, nil
}
func (a *App) GetUserPreferences(userID string) ([]mm_model.Preference, error) {
return a.store.GetUserPreferences(userID)
}
func (a *App) UserIsGuest(userID string) (bool, error) {
user, err := a.store.GetUserByID(userID)
if err != nil {
return false, err
}
return user.IsGuest, nil
}
func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) {
isGuest, err := a.UserIsGuest(seerUser)
if err != nil {
return false, err
}
if isGuest {
hasSharedChannels, err := a.store.CanSeeUser(seerUser, seenUser)
if err != nil {
return false, err
}
return hasSharedChannels, nil
}
return true, nil
}
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mm_model.Channel, error) {
channels, err := a.store.SearchUserChannels(teamID, userID, query)
if err != nil {
return nil, err
}
var writeableChannels []*mm_model.Channel
for _, channel := range channels {
if a.permissions.HasPermissionToChannel(userID, channel.Id, model.PermissionCreatePost) {
writeableChannels = append(writeableChannels, channel)
}
}
return writeableChannels, nil
}
func (a *App) GetChannel(teamID string, channelID string) (*mm_model.Channel, error) {
return a.store.GetChannel(teamID, channelID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
package auth
import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
type AuthInterface interface {
GetSession(token string) (*model.Session, error)
IsValidReadToken(boardID string, readToken string) (bool, error)
DoesUserHaveTeamAccess(userID string, teamID string) bool
}
// Auth authenticates sessions.
type Auth struct {
config *config.Configuration
store store.Store
permissions permissions.PermissionsService
}
// New returns a new Auth.
func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth {
return &Auth{config: config, store: store, permissions: permissions}
}
// GetSession Get a user active session and refresh the session if needed.
func (a *Auth) GetSession(token string) (*model.Session, error) {
if len(token) < 1 {
return nil, errors.New("no session token")
}
session, err := a.store.GetSession(token, a.config.SessionExpireTime)
if err != nil {
return nil, errors.Wrap(err, "unable to get the session for the token")
}
if session.UpdateAt < (utils.GetMillis() - utils.SecondsToMillis(a.config.SessionRefreshTime)) {
_ = a.store.RefreshSession(session)
}
return session, nil
}
// IsValidReadToken validates the read token for a board.
func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) {
sharing, err := a.store.GetSharing(boardID)
if model.IsErrNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) {
return true, nil
}
return false, nil
}
func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool {
return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/services/auth"
)
const (
MinimumPasswordLength = 8
)
func NewErrAuthParam(msg string) *ErrAuthParam {
return &ErrAuthParam{
msg: msg,
}
}
type ErrAuthParam struct {
msg string
}
func (pe *ErrAuthParam) Error() string {
return pe.msg
}
// LoginRequest is a login request
// swagger:model
type LoginRequest struct {
// Type of login, currently must be set to "normal"
// required: true
Type string `json:"type"`
// If specified, login using username
// required: false
Username string `json:"username"`
// If specified, login using email
// required: false
Email string `json:"email"`
// Password
// required: true
Password string `json:"password"`
// MFA token
// required: false
// swagger:ignore
MfaToken string `json:"mfa_token"`
}
// LoginResponse is a login response
// swagger:model
type LoginResponse struct {
// Session token
// required: true
Token string `json:"token"`
}
func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) {
var resp LoginResponse
if err := json.NewDecoder(data).Decode(&resp); err != nil {
return nil, err
}
return &resp, nil
}
// RegisterRequest is a user registration request
// swagger:model
type RegisterRequest struct {
// User name
// required: true
Username string `json:"username"`
// User's email
// required: true
Email string `json:"email"`
// Password
// required: true
Password string `json:"password"`
// Registration authorization token
// required: true
Token string `json:"token"`
}
func (rd *RegisterRequest) IsValid() error {
if strings.TrimSpace(rd.Username) == "" {
return NewErrAuthParam("username is required")
}
if strings.TrimSpace(rd.Email) == "" {
return NewErrAuthParam("email is required")
}
if !auth.IsEmailValid(rd.Email) {
return NewErrAuthParam("invalid email format")
}
if rd.Password == "" {
return NewErrAuthParam("password is required")
}
return isValidPassword(rd.Password)
}
// ChangePasswordRequest is a user password change request
// swagger:model
type ChangePasswordRequest struct {
// Old password
// required: true
OldPassword string `json:"oldPassword"`
// New password
// required: true
NewPassword string `json:"newPassword"`
}
// IsValid validates a password change request.
func (rd *ChangePasswordRequest) IsValid() error {
if rd.OldPassword == "" {
return NewErrAuthParam("old password is required")
}
if rd.NewPassword == "" {
return NewErrAuthParam("new password is required")
}
return isValidPassword(rd.NewPassword)
}
func isValidPassword(password string) error {
if len(password) < MinimumPasswordLength {
return NewErrAuthParam(fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"strconv"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
// Block is the basic data unit
// swagger:model
type Block struct {
// The id for this block
// required: true
ID string `json:"id"`
// The id for this block's parent block. Empty for root blocks
// required: false
ParentID string `json:"parentId"`
// The id for user who created this block
// required: true
CreatedBy string `json:"createdBy"`
// The id for user who last modified this block
// required: true
ModifiedBy string `json:"modifiedBy"`
// The schema version of this block
// required: true
Schema int64 `json:"schema"`
// The block type
// required: true
Type BlockType `json:"type"`
// The display title
// required: false
Title string `json:"title"`
// The block fields
// required: false
Fields map[string]interface{} `json:"fields"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
// Deprecated. The workspace id that the block belongs to
// required: false
WorkspaceID string `json:"-"`
// The board id that the block belongs to
// required: true
BoardID string `json:"boardId"`
// Indicates if the card is limited
// required: false
Limited bool `json:"limited,omitempty"`
}
// BlockPatch is a patch for modify blocks
// swagger:model
type BlockPatch struct {
// The id for this block's parent block. Empty for root blocks
// required: false
ParentID *string `json:"parentId"`
// The schema version of this block
// required: false
Schema *int64 `json:"schema"`
// The block type
// required: false
Type *BlockType `json:"type"`
// The display title
// required: false
Title *string `json:"title"`
// The block updated fields
// required: false
UpdatedFields map[string]interface{} `json:"updatedFields"`
// The block removed fields
// required: false
DeletedFields []string `json:"deletedFields"`
}
// BlockPatchBatch is a batch of IDs and patches for modify blocks
// swagger:model
type BlockPatchBatch struct {
// The id's for of the blocks to patch
BlockIDs []string `json:"block_ids"`
// The BlockPatches to be applied
BlockPatches []BlockPatch `json:"block_patches"`
}
// BoardModifier is a callback that can modify each board during an import.
// A cache of arbitrary data will be passed for each call and any changes
// to the cache will be preserved for the next call.
// Return true to import the block or false to skip import.
type BoardModifier func(board *Board, cache map[string]interface{}) bool
// BlockModifier is a callback that can modify each block during an import.
// A cache of arbitrary data will be passed for each call and any changes
// to the cache will be preserved for the next call.
// Return true to import the block or false to skip import.
type BlockModifier func(block *Block, cache map[string]interface{}) bool
func BlocksFromJSON(data io.Reader) []*Block {
var blocks []*Block
_ = json.NewDecoder(data).Decode(&blocks)
return blocks
}
// LogClone implements the `mlog.LogCloner` interface to provide a subset of Block fields for logging.
func (b *Block) LogClone() interface{} {
return struct {
ID string
ParentID string
BoardID string
Type BlockType
}{
ID: b.ID,
ParentID: b.ParentID,
BoardID: b.BoardID,
Type: b.Type,
}
}
// Patch returns an update version of the block.
func (p *BlockPatch) Patch(block *Block) *Block {
if p.ParentID != nil {
block.ParentID = *p.ParentID
}
if p.Schema != nil {
block.Schema = *p.Schema
}
if p.Type != nil {
block.Type = *p.Type
}
if p.Title != nil {
block.Title = *p.Title
}
for key, field := range p.UpdatedFields {
block.Fields[key] = field
}
for _, key := range p.DeletedFields {
delete(block.Fields, key)
}
return block
}
type QueryBlocksOptions struct {
BoardID string // if not empty then filter for blocks belonging to specified board
ParentID string // if not empty then filter for blocks belonging to specified parent
BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited)
}
// QuerySubtreeOptions are query options that can be passed to GetSubTree methods.
type QuerySubtreeOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
}
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
type QueryBlockHistoryOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
Descending bool // if true then the records are sorted by insert_at in descending order
}
// QueryBoardHistoryOptions are query options that can be passed to GetBoardHistory.
type QueryBoardHistoryOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
Descending bool // if true then the records are sorted by insert_at in descending order
}
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
type QueryBlockHistoryChildOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited)
}
func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) {
if userID == SingleUser {
userID = ""
}
now := GetMillis()
for i := range blocks {
blocks[i].ModifiedBy = userID
blocks[i].UpdateAt = now
if auditRec != nil {
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
}
}
}
func (b *Block) ShouldBeLimited(cardLimitTimestamp int64) bool {
return b.Type == TypeCard &&
b.UpdateAt < cardLimitTimestamp
}
// Returns a limited version of the block that doesn't contain the
// contents of the block, only its IDs and type.
func (b *Block) GetLimited() *Block {
newBlock := &Block{
Title: b.Title,
ID: b.ID,
ParentID: b.ParentID,
BoardID: b.BoardID,
Schema: b.Schema,
Type: b.Type,
CreateAt: b.CreateAt,
UpdateAt: b.UpdateAt,
DeleteAt: b.DeleteAt,
WorkspaceID: b.WorkspaceID,
Limited: true,
}
if iconField, ok := b.Fields["icon"]; ok {
newBlock.Fields = map[string]interface{}{
"icon": iconField,
}
}
return newBlock
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GenerateBlockIDs generates new IDs for all the blocks of the list,
// keeping consistent any references that other blocks would made to
// the original IDs, so a tree of blocks can get new IDs and maintain
// its shape.
func GenerateBlockIDs(blocks []*Block, logger mlog.LoggerIFace) []*Block {
blockIDs := map[string]BlockType{}
referenceIDs := map[string]bool{}
for _, block := range blocks {
if _, ok := blockIDs[block.ID]; !ok {
blockIDs[block.ID] = block.Type
}
if _, ok := referenceIDs[block.BoardID]; !ok {
referenceIDs[block.BoardID] = true
}
if _, ok := referenceIDs[block.ParentID]; !ok {
referenceIDs[block.ParentID] = true
}
if _, ok := block.Fields["contentOrder"]; ok {
contentOrder, typeOk := block.Fields["contentOrder"].([]interface{})
if !typeOk {
logger.Warn(
"type assertion failed for content order when saving reference block IDs",
mlog.String("blockID", block.ID),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields["contentOrder"])),
mlog.String("expectedType", "[]interface{}"),
mlog.String("contentOrder", fmt.Sprintf("%v", block.Fields["contentOrder"])),
)
continue
}
for _, blockID := range contentOrder {
switch v := blockID.(type) {
case []interface{}:
for _, columnBlockID := range v {
referenceIDs[columnBlockID.(string)] = true
}
case string:
referenceIDs[v] = true
default:
}
}
}
if _, ok := block.Fields["defaultTemplateId"]; ok {
defaultTemplateID, typeOk := block.Fields["defaultTemplateId"].(string)
if !typeOk {
logger.Warn(
"type assertion failed for default template ID when saving reference block IDs",
mlog.String("blockID", block.ID),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields["defaultTemplateId"])),
mlog.String("expectedType", "string"),
mlog.String("defaultTemplateId", fmt.Sprintf("%v", block.Fields["defaultTemplateId"])),
)
continue
}
referenceIDs[defaultTemplateID] = true
}
}
newIDs := map[string]string{}
for id, blockType := range blockIDs {
for referenceID := range referenceIDs {
if id == referenceID {
newIDs[id] = utils.NewID(BlockType2IDType(blockType))
continue
}
}
}
getExistingOrOldID := func(id string) string {
if existingID, ok := newIDs[id]; ok {
return existingID
}
return id
}
getExistingOrNewID := func(id string) string {
if existingID, ok := newIDs[id]; ok {
return existingID
}
return utils.NewID(BlockType2IDType(blockIDs[id]))
}
newBlocks := make([]*Block, len(blocks))
for i, block := range blocks {
block.ID = getExistingOrNewID(block.ID)
block.BoardID = getExistingOrOldID(block.BoardID)
block.ParentID = getExistingOrOldID(block.ParentID)
blockMod := block
if _, ok := blockMod.Fields["contentOrder"]; ok {
fixFieldIDs(blockMod, "contentOrder", getExistingOrOldID, logger)
}
if _, ok := blockMod.Fields["cardOrder"]; ok {
fixFieldIDs(blockMod, "cardOrder", getExistingOrOldID, logger)
}
if _, ok := blockMod.Fields["defaultTemplateId"]; ok {
defaultTemplateID, typeOk := blockMod.Fields["defaultTemplateId"].(string)
if !typeOk {
logger.Warn(
"type assertion failed for default template ID when saving reference block IDs",
mlog.String("blockID", blockMod.ID),
mlog.String("actionType", fmt.Sprintf("%T", blockMod.Fields["defaultTemplateId"])),
mlog.String("expectedType", "string"),
mlog.String("defaultTemplateId", fmt.Sprintf("%v", blockMod.Fields["defaultTemplateId"])),
)
} else {
blockMod.Fields["defaultTemplateId"] = getExistingOrOldID(defaultTemplateID)
}
}
newBlocks[i] = blockMod
}
return newBlocks
}
func fixFieldIDs(block *Block, fieldName string, getExistingOrOldID func(string) string, logger mlog.LoggerIFace) {
field, typeOk := block.Fields[fieldName].([]interface{})
if !typeOk {
logger.Warn(
"type assertion failed for JSON field when setting new block IDs",
mlog.String("blockID", block.ID),
mlog.String("fieldName", fieldName),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields[fieldName])),
mlog.String("expectedType", "[]interface{}"),
mlog.String("value", fmt.Sprintf("%v", block.Fields[fieldName])),
)
} else {
for j := range field {
switch v := field[j].(type) {
case string:
field[j] = getExistingOrOldID(v)
case []interface{}:
subOrder := field[j].([]interface{})
for k := range v {
subOrder[k] = getExistingOrOldID(v[k].(string))
}
}
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
// BlockType represents a block type.
type BlockType string
const (
TypeUnknown = "unknown"
TypeBoard = "board"
TypeCard = "card"
TypeView = "view"
TypeText = "text"
TypeCheckbox = "checkbox"
TypeComment = "comment"
TypeImage = "image"
TypeAttachment = "attachment"
TypeDivider = "divider"
)
func (bt BlockType) String() string {
return string(bt)
}
// BlockTypeFromString returns an appropriate BlockType for the specified string.
func BlockTypeFromString(s string) (BlockType, error) {
switch strings.ToLower(s) {
case "board":
return TypeBoard, nil
case "card":
return TypeCard, nil
case "view":
return TypeView, nil
case "text":
return TypeText, nil
case "checkbox":
return TypeCheckbox, nil
case "comment":
return TypeComment, nil
case "image":
return TypeImage, nil
case "attachment":
return TypeAttachment, nil
case "divider":
return TypeDivider, nil
}
return TypeUnknown, ErrInvalidBlockType{s}
}
// BlockType2IDType returns an appropriate IDType for the specified BlockType.
func BlockType2IDType(blockType BlockType) utils.IDType {
switch blockType {
case TypeBoard:
return utils.IDTypeBoard
case TypeCard:
return utils.IDTypeCard
case TypeView:
return utils.IDTypeView
case TypeText, TypeCheckbox, TypeComment, TypeDivider:
return utils.IDTypeBlock
case TypeImage, TypeAttachment:
return utils.IDTypeAttachment
}
return utils.IDTypeNone
}
// ErrInvalidBlockType is returned wherever an invalid block type was provided.
type ErrInvalidBlockType struct {
Type string
}
func (e ErrInvalidBlockType) Error() string {
return e.Type + " is an invalid block type."
}
// IsErrInvalidBlockType returns true if `err` is a IsErrInvalidBlockType or wraps one.
func IsErrInvalidBlockType(err error) bool {
var eibt *ErrInvalidBlockType
return errors.As(err, &eibt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"time"
)
type BoardType string
type BoardRole string
type BoardSearchField string
const (
BoardTypeOpen BoardType = "O"
BoardTypePrivate BoardType = "P"
)
const (
BoardRoleNone BoardRole = ""
BoardRoleViewer BoardRole = "viewer"
BoardRoleCommenter BoardRole = "commenter"
BoardRoleEditor BoardRole = "editor"
BoardRoleAdmin BoardRole = "admin"
)
const (
BoardSearchFieldNone BoardSearchField = ""
BoardSearchFieldTitle BoardSearchField = "title"
BoardSearchFieldPropertyName BoardSearchField = "property_name"
)
// Board groups a set of blocks and its layout
// swagger:model
type Board struct {
// The ID for the board
// required: true
ID string `json:"id"`
// The ID of the team that the board belongs to
// required: true
TeamID string `json:"teamId"`
// The ID of the channel that the board was created from
// required: false
ChannelID string `json:"channelId"`
// The ID of the user that created the board
// required: true
CreatedBy string `json:"createdBy"`
// The ID of the last user that updated the board
// required: true
ModifiedBy string `json:"modifiedBy"`
// The type of the board
// required: true
Type BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: true
MinimumRole BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title string `json:"title"`
// The description of the board
// required: false
Description string `json:"description"`
// The icon of the board
// required: false
Icon string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription bool `json:"showDescription"`
// Marks the template boards
// required: false
IsTemplate bool `json:"isTemplate"`
// Marks the template boards
// required: false
TemplateVersion int `json:"templateVersion"`
// The properties of the board
// required: false
Properties map[string]interface{} `json:"properties"`
// The properties of the board cards
// required: false
CardProperties []map[string]interface{} `json:"cardProperties"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
}
// GetPropertyString returns the value of the specified property as a string,
// or error if the property does not exist or is not of type string.
func (b *Board) GetPropertyString(propName string) (string, error) {
val, ok := b.Properties[propName]
if !ok {
return "", NewErrNotFound(propName)
}
s, ok := val.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
return s, nil
}
// BoardPatch is a patch for modify boards
// swagger:model
type BoardPatch struct {
// The type of the board
// required: false
Type *BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: false
MinimumRole *BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title *string `json:"title"`
// The description of the board
// required: false
Description *string `json:"description"`
// The icon of the board
// required: false
Icon *string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription *bool `json:"showDescription"`
// Indicates if the board shows the description on the interface
// required: false
ChannelID *string `json:"channelId"`
// The board updated properties
// required: false
UpdatedProperties map[string]interface{} `json:"updatedProperties"`
// The board removed properties
// required: false
DeletedProperties []string `json:"deletedProperties"`
// The board updated card properties
// required: false
UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"`
// The board removed card properties
// required: false
DeletedCardProperties []string `json:"deletedCardProperties"`
}
// BoardMember stores the information of the membership of a user on a board
// swagger:model
type BoardMember struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The independent roles of the user on the board
// required: false
Roles string `json:"roles"`
// Minimum role because the board configuration
// required: false
MinimumRole string `json:"minimumRole"`
// Marks the user as an admin of the board
// required: true
SchemeAdmin bool `json:"schemeAdmin"`
// Marks the user as an editor of the board
// required: true
SchemeEditor bool `json:"schemeEditor"`
// Marks the user as an commenter of the board
// required: true
SchemeCommenter bool `json:"schemeCommenter"`
// Marks the user as an viewer of the board
// required: true
SchemeViewer bool `json:"schemeViewer"`
// Marks the membership as generated by an access group
// required: true
Synthetic bool `json:"synthetic"`
}
// BoardMetadata contains metadata for a Board
// swagger:model
type BoardMetadata struct {
// The ID for the board
// required: true
BoardID string `json:"boardId"`
// The most recent time a descendant of this board was added, modified, or deleted
// required: true
DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"`
// The earliest time a descendant of this board was added, modified, or deleted
// required: true
DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"`
// The ID of the user that created the board
// required: true
CreatedBy string `json:"createdBy"`
// The ID of the user that last modified the most recently modified descendant
// required: true
LastModifiedBy string `json:"lastModifiedBy"`
}
func BoardFromJSON(data io.Reader) *Board {
var board *Board
_ = json.NewDecoder(data).Decode(&board)
return board
}
func BoardsFromJSON(data io.Reader) []*Board {
var boards []*Board
_ = json.NewDecoder(data).Decode(&boards)
return boards
}
func BoardMemberFromJSON(data io.Reader) *BoardMember {
var boardMember *BoardMember
_ = json.NewDecoder(data).Decode(&boardMember)
return boardMember
}
func BoardMembersFromJSON(data io.Reader) []*BoardMember {
var boardMembers []*BoardMember
_ = json.NewDecoder(data).Decode(&boardMembers)
return boardMembers
}
func BoardMetadataFromJSON(data io.Reader) *BoardMetadata {
var boardMetadata *BoardMetadata
_ = json.NewDecoder(data).Decode(&boardMetadata)
return boardMetadata
}
// Patch returns an updated version of the board.
func (p *BoardPatch) Patch(board *Board) *Board {
if p.Type != nil {
board.Type = *p.Type
}
if p.Title != nil {
board.Title = *p.Title
}
if p.MinimumRole != nil {
board.MinimumRole = *p.MinimumRole
}
if p.Description != nil {
board.Description = *p.Description
}
if p.Icon != nil {
board.Icon = *p.Icon
}
if p.ShowDescription != nil {
board.ShowDescription = *p.ShowDescription
}
if p.ChannelID != nil {
board.ChannelID = *p.ChannelID
}
for key, property := range p.UpdatedProperties {
board.Properties[key] = property
}
for _, key := range p.DeletedProperties {
delete(board.Properties, key)
}
if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 {
// first we accumulate all properties indexed by, and maintain their order
keyOrder := []string{}
cardPropertyMap := map[string]map[string]interface{}{}
for _, prop := range board.CardProperties {
id, ok := prop["id"].(string)
if !ok {
// bad property, skipping
continue
}
cardPropertyMap[id] = prop
keyOrder = append(keyOrder, id)
}
// if there are properties marked for removal, we delete them
for _, propertyID := range p.DeletedCardProperties {
delete(cardPropertyMap, propertyID)
}
// if there are properties marked for update, we replace the
// existing ones or add them
for _, newprop := range p.UpdatedCardProperties {
id, ok := newprop["id"].(string)
if !ok {
// bad new property, skipping
continue
}
_, exists := cardPropertyMap[id]
if !exists {
keyOrder = append(keyOrder, id)
}
cardPropertyMap[id] = newprop
}
// and finally we flatten and save the updated properties
newCardProperties := []map[string]interface{}{}
for _, key := range keyOrder {
p, exists := cardPropertyMap[key]
if exists {
newCardProperties = append(newCardProperties, p)
}
}
board.CardProperties = newCardProperties
}
return board
}
func IsBoardTypeValid(t BoardType) bool {
return t == BoardTypeOpen || t == BoardTypePrivate
}
func IsBoardMinimumRoleValid(r BoardRole) bool {
return r == BoardRoleNone || r == BoardRoleAdmin || r == BoardRoleEditor || r == BoardRoleCommenter || r == BoardRoleViewer
}
func (p *BoardPatch) IsValid() error {
if p.Type != nil && !IsBoardTypeValid(*p.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if p.MinimumRole != nil && !IsBoardMinimumRoleValid(*p.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}
type InvalidBoardErr struct {
msg string
}
func (ibe InvalidBoardErr) Error() string {
return ibe.msg
}
func (b *Board) IsValid() error {
if b.TeamID == "" {
return InvalidBoardErr{"empty-team-id"}
}
if !IsBoardTypeValid(b.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if !IsBoardMinimumRoleValid(b.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}
// BoardMemberHistoryEntry stores the information of the membership of a user on a board
// swagger:model
type BoardMemberHistoryEntry struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The action that added this history entry (created or deleted)
// required: false
Action string `json:"action"`
// The insertion time
// required: true
InsertAt time.Time `json:"insertAt"`
}
func BoardSearchFieldFromString(field string) (BoardSearchField, error) {
switch field {
case string(BoardSearchFieldTitle):
return BoardSearchFieldTitle, nil
case string(BoardSearchFieldPropertyName):
return BoardSearchFieldPropertyName, nil
}
return BoardSearchFieldNone, ErrInvalidBoardSearchField
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// BoardInsightsList is a response type with pagination support.
type BoardInsightsList struct {
mm_model.InsightsListData
Items []*BoardInsight `json:"items"`
}
// BoardInsight gives insight into activities in a Board
// swagger:model
type BoardInsight struct {
// ID of the board
// required: true
BoardID string `json:"boardID"`
// icon of the board
// required: false
Icon string `json:"icon"`
// Title of the board
// required: false
Title string `json:"title"`
// Metric of how active the board is
// required: true
ActivityCount string `json:"activityCount"`
// IDs of users active on the board
// required: true
ActiveUsers mm_model.StringArray `json:"activeUsers"`
// ID of user who created the board
// required: true
CreatedBy string `json:"createdBy"`
}
func BoardInsightsFromJSON(data io.Reader) []BoardInsight {
var boardInsights []BoardInsight
_ = json.NewDecoder(data).Decode(&boardInsights)
return boardInsights
}
// GetTopBoardInsightsListWithPagination adds a rank to each item in the given list of BoardInsight and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of BoardInsight is assumed to be
// sorted by ActivityCount(score). Returns a BoardInsightsList.
func GetTopBoardInsightsListWithPagination(boards []*BoardInsight, limit int) *BoardInsightsList {
// Add pagination support
var hasNext bool
if limit != 0 && len(boards) == limit+1 {
hasNext = true
boards = boards[:len(boards)-1]
}
return &BoardInsightsList{InsightsListData: mm_model.InsightsListData{HasNext: hasNext}, Items: boards}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required")
var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required")
var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty")
var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match")
var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match")
type BlockDoesntBelongToAnyBoardErr struct {
blockID string
}
func (e BlockDoesntBelongToAnyBoardErr) Error() string {
return fmt.Sprintf("block %s doesn't belong to any board", e.blockID)
}
// BoardsAndBlocks is used to operate over boards and blocks at the
// same time
// swagger:model
type BoardsAndBlocks struct {
// The boards
// required: false
Boards []*Board `json:"boards"`
// The blocks
// required: false
Blocks []*Block `json:"blocks"`
}
func (bab *BoardsAndBlocks) IsValid() error {
if len(bab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(bab.Blocks) == 0 {
return ErrNoBlocksInBoardsAndBlocks
}
boardsMap := map[string]bool{}
for _, board := range bab.Boards {
boardsMap[board.ID] = true
}
for _, block := range bab.Blocks {
if _, ok := boardsMap[block.BoardID]; !ok {
return BlockDoesntBelongToAnyBoardErr{block.ID}
}
}
return nil
}
// DeleteBoardsAndBlocks is used to list the boards and blocks to
// delete on a request
// swagger:model
type DeleteBoardsAndBlocks struct {
// The boards
// required: true
Boards []string `json:"boards"`
// The blocks
// required: true
Blocks []string `json:"blocks"`
}
func NewDeleteBoardsAndBlocksFromBabs(babs *BoardsAndBlocks) *DeleteBoardsAndBlocks {
boardIDs := make([]string, 0, len(babs.Boards))
blockIDs := make([]string, 0, len(babs.Boards))
for _, board := range babs.Boards {
boardIDs = append(boardIDs, board.ID)
}
for _, block := range babs.Blocks {
blockIDs = append(blockIDs, block.ID)
}
return &DeleteBoardsAndBlocks{
Boards: boardIDs,
Blocks: blockIDs,
}
}
func (dbab *DeleteBoardsAndBlocks) IsValid() error {
if len(dbab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
return nil
}
// PatchBoardsAndBlocks is used to patch multiple boards and blocks on
// a single request
// swagger:model
type PatchBoardsAndBlocks struct {
// The board IDs to patch
// required: true
BoardIDs []string `json:"boardIDs"`
// The board patches
// required: true
BoardPatches []*BoardPatch `json:"boardPatches"`
// The block IDs to patch
// required: true
BlockIDs []string `json:"blockIDs"`
// The block patches
// required: true
BlockPatches []*BlockPatch `json:"blockPatches"`
}
func (dbab *PatchBoardsAndBlocks) IsValid() error {
if len(dbab.BoardIDs) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(dbab.BoardIDs) != len(dbab.BoardPatches) {
return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks
}
if len(dbab.BlockIDs) != len(dbab.BlockPatches) {
return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks
}
return nil
}
func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger mlog.LoggerIFace) (*BoardsAndBlocks, error) {
if err := bab.IsValid(); err != nil {
return nil, err
}
blocksByBoard := map[string][]*Block{}
for _, block := range bab.Blocks {
blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block)
}
boards := []*Board{}
blocks := []*Block{}
for _, board := range bab.Boards {
newID := utils.NewID(utils.IDTypeBoard)
for _, block := range blocksByBoard[board.ID] {
block.BoardID = newID
blocks = append(blocks, block)
}
board.ID = newID
boards = append(boards, board)
}
newBab := &BoardsAndBlocks{
Boards: boards,
Blocks: GenerateBlockIDs(blocks, logger),
}
return newBab, nil
}
func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks {
var bab *BoardsAndBlocks
_ = json.NewDecoder(data).Decode(&bab)
return bab
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"errors"
"fmt"
"github.com/rivo/uniseg"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var ErrBoardIDMismatch = errors.New("Board IDs do not match")
type ErrInvalidCard struct {
msg string
}
func NewErrInvalidCard(msg string) ErrInvalidCard {
return ErrInvalidCard{
msg: msg,
}
}
func (e ErrInvalidCard) Error() string {
return fmt.Sprintf("invalid card, %s", e.msg)
}
var ErrNotCardBlock = errors.New("not a card block")
type ErrInvalidFieldType struct {
field string
}
func (e ErrInvalidFieldType) Error() string {
return fmt.Sprintf("invalid type for field '%s'", e.field)
}
// Card represents a group of content blocks and properties.
// swagger:model
type Card struct {
// The id for this card
// required: false
ID string `json:"id"`
// The id for board this card belongs to.
// required: false
BoardID string `json:"boardId"`
// The id for user who created this card
// required: false
CreatedBy string `json:"createdBy"`
// The id for user who last modified this card
// required: false
ModifiedBy string `json:"modifiedBy"`
// The display title
// required: false
Title string `json:"title"`
// An array of content block ids specifying the ordering of content for this card.
// required: false
ContentOrder []string `json:"contentOrder"`
// The icon of the card
// required: false
Icon string `json:"icon"`
// True if this card belongs to a template
// required: false
IsTemplate bool `json:"isTemplate"`
// A map of property ids to property values (option ids, strings, array of option ids)
// required: false
Properties map[string]any `json:"properties"`
// The creation time in milliseconds since the current epoch
// required: false
CreateAt int64 `json:"createAt"`
// The last modified time in milliseconds since the current epoch
// required: false
UpdateAt int64 `json:"updateAt"`
// The deleted time in milliseconds since the current epoch. Set to indicate this card is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
}
// Populate populates a Card with default values.
func (c *Card) Populate() {
if c.ID == "" {
c.ID = utils.NewID(utils.IDTypeCard)
}
if c.ContentOrder == nil {
c.ContentOrder = make([]string, 0)
}
if c.Properties == nil {
c.Properties = make(map[string]any)
}
now := utils.GetMillis()
if c.CreateAt == 0 {
c.CreateAt = now
}
if c.UpdateAt == 0 {
c.UpdateAt = now
}
}
func (c *Card) PopulateWithBoardID(boardID string) {
c.BoardID = boardID
c.Populate()
}
// CheckValid returns an error if the Card has invalid field values.
func (c *Card) CheckValid() error {
if c.ID == "" {
return ErrInvalidCard{"ID is missing"}
}
if c.BoardID == "" {
return ErrInvalidCard{"BoardID is missing"}
}
if c.ContentOrder == nil {
return ErrInvalidCard{"ContentOrder is missing"}
}
if uniseg.GraphemeClusterCount(c.Icon) > 1 {
return ErrInvalidCard{"Icon can have only one grapheme"}
}
if c.Properties == nil {
return ErrInvalidCard{"Properties"}
}
if c.CreateAt == 0 {
return ErrInvalidCard{"CreateAt"}
}
if c.UpdateAt == 0 {
return ErrInvalidCard{"UpdateAt"}
}
return nil
}
// CardPatch is a patch for modifying cards
// swagger:model
type CardPatch struct {
// The display title
// required: false
Title *string `json:"title"`
// An array of content block ids specifying the ordering of content for this card.
// required: false
ContentOrder *[]string `json:"contentOrder"`
// The icon of the card
// required: false
Icon *string `json:"icon"`
// A map of property ids to property option ids to be updated
// required: false
UpdatedProperties map[string]any `json:"updatedProperties"`
}
// Patch returns an updated version of the card.
func (p *CardPatch) Patch(card *Card) *Card {
if p.Title != nil {
card.Title = *p.Title
}
if p.ContentOrder != nil {
card.ContentOrder = *p.ContentOrder
}
if p.Icon != nil {
card.Icon = *p.Icon
}
if card.Properties == nil {
card.Properties = make(map[string]any)
}
// if there are properties marked for update, we replace the
// existing ones or add them
for propID, propVal := range p.UpdatedProperties {
card.Properties[propID] = propVal
}
return card
}
// CheckValid returns an error if the CardPatch has invalid field values.
func (p *CardPatch) CheckValid() error {
if p.Icon != nil && uniseg.GraphemeClusterCount(*p.Icon) > 1 {
return ErrInvalidCard{"Icon can have only one grapheme"}
}
return nil
}
// Card2Block converts a card to block using a shallow copy. Not needed once cards are first class entities.
func Card2Block(card *Card) *Block {
fields := make(map[string]interface{})
fields["contentOrder"] = card.ContentOrder
fields["icon"] = card.Icon
fields["isTemplate"] = card.IsTemplate
fields["properties"] = card.Properties
return &Block{
ID: card.ID,
ParentID: card.BoardID,
CreatedBy: card.CreatedBy,
ModifiedBy: card.ModifiedBy,
Schema: 1,
Type: TypeCard,
Title: card.Title,
Fields: fields,
CreateAt: card.CreateAt,
UpdateAt: card.UpdateAt,
DeleteAt: card.DeleteAt,
BoardID: card.BoardID,
}
}
// Block2Card converts a block to a card. Not needed once cards are first class entities.
func Block2Card(block *Block) (*Card, error) {
if block.Type != TypeCard {
return nil, fmt.Errorf("cannot convert block to card: %w", ErrNotCardBlock)
}
contentOrder := make([]string, 0)
icon := ""
isTemplate := false
properties := make(map[string]any)
if co, ok := block.Fields["contentOrder"]; ok {
switch arr := co.(type) {
case []any:
for _, str := range arr {
if id, ok := str.(string); ok {
contentOrder = append(contentOrder, id)
} else {
return nil, ErrInvalidFieldType{"contentOrder item"}
}
}
case []string:
contentOrder = append(contentOrder, arr...)
default:
return nil, ErrInvalidFieldType{"contentOrder"}
}
}
if iconAny, ok := block.Fields["icon"]; ok {
if id, ok := iconAny.(string); ok {
icon = id
} else {
return nil, ErrInvalidFieldType{"icon"}
}
}
if isTemplateAny, ok := block.Fields["isTemplate"]; ok {
if b, ok := isTemplateAny.(bool); ok {
isTemplate = b
} else {
return nil, ErrInvalidFieldType{"isTemplate"}
}
}
if props, ok := block.Fields["properties"]; ok {
if propMap, ok := props.(map[string]any); ok {
for k, v := range propMap {
properties[k] = v
}
} else {
return nil, ErrInvalidFieldType{"properties"}
}
}
card := &Card{
ID: block.ID,
BoardID: block.BoardID,
CreatedBy: block.CreatedBy,
ModifiedBy: block.ModifiedBy,
Title: block.Title,
ContentOrder: contentOrder,
Icon: icon,
IsTemplate: isTemplate,
Properties: properties,
CreateAt: block.CreateAt,
UpdateAt: block.UpdateAt,
DeleteAt: block.DeleteAt,
}
card.Populate()
return card, nil
}
// CardPatch2BlockPatch converts a CardPatch to a BlockPatch. Not needed once cards are first class entities.
func CardPatch2BlockPatch(cardPatch *CardPatch) (*BlockPatch, error) {
if err := cardPatch.CheckValid(); err != nil {
return nil, err
}
blockPatch := &BlockPatch{
Title: cardPatch.Title,
}
updatedFields := make(map[string]any, 0)
if cardPatch.ContentOrder != nil {
updatedFields["contentOrder"] = cardPatch.ContentOrder
}
if cardPatch.Icon != nil {
updatedFields["icon"] = cardPatch.Icon
}
properties := make(map[string]any)
for k, v := range cardPatch.UpdatedProperties {
properties[k] = v
}
if len(properties) != 0 {
updatedFields["properties"] = cardPatch.UpdatedProperties
}
blockPatch.UpdatedFields = updatedFields
return blockPatch, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
const (
CategoryTypeSystem = "system"
CategoryTypeCustom = "custom"
)
// Category is a board category
// swagger:model
type Category struct {
// The id for this category
// required: true
ID string `json:"id"`
// The name for this category
// required: true
Name string `json:"name"`
// The user's id for this category
// required: true
UserID string `json:"userID"`
// The team id for this category
// required: true
TeamID string `json:"teamID"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this category is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
// Category's state in client side
// required: true
Collapsed bool `json:"collapsed"`
// Inter-category sort order per user
// required: true
SortOrder int `json:"sortOrder"`
// The sorting method applied on this category
// required: true
Sorting string `json:"sorting"`
// Category's type
// required: true
Type string `json:"type"`
}
func (c *Category) Hydrate() {
if c.ID == "" {
c.ID = utils.NewID(utils.IDTypeNone)
}
if c.CreateAt == 0 {
c.CreateAt = utils.GetMillis()
}
if c.UpdateAt == 0 {
c.UpdateAt = c.CreateAt
}
if c.SortOrder < 0 {
c.SortOrder = 0
}
if strings.TrimSpace(c.Type) == "" {
c.Type = CategoryTypeCustom
}
}
func (c *Category) IsValid() error {
if strings.TrimSpace(c.ID) == "" {
return NewErrInvalidCategory("category ID cannot be empty")
}
if strings.TrimSpace(c.Name) == "" {
return NewErrInvalidCategory("category name cannot be empty")
}
if strings.TrimSpace(c.UserID) == "" {
return NewErrInvalidCategory("category user ID cannot be empty")
}
if strings.TrimSpace(c.TeamID) == "" {
return NewErrInvalidCategory("category team id ID cannot be empty")
}
if c.Type != CategoryTypeCustom && c.Type != CategoryTypeSystem {
return NewErrInvalidCategory(fmt.Sprintf("category type is invalid. Allowed types: %s and %s", CategoryTypeSystem, CategoryTypeCustom))
}
return nil
}
func CategoryFromJSON(data io.Reader) *Category {
var category *Category
_ = json.NewDecoder(data).Decode(&category)
return category
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"database/sql"
"errors"
"fmt"
"net/http"
"strings"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
var (
ErrViewsLimitReached = errors.New("views limit reached for board")
ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
ErrInsufficientLicense = errors.New("appropriate license required")
ErrCategoryPermissionDenied = errors.New("category doesn't belong to user")
ErrCategoryDeleted = errors.New("category is deleted")
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
ErrRequestEntityTooLarge = errors.New("request entity too large")
ErrInvalidBoardSearchField = errors.New("invalid board search field")
)
// ErrNotFound is an error type that can be returned by store APIs
// when a query unexpectedly fetches no records.
type ErrNotFound struct {
entity string
}
// NewErrNotFound creates a new ErrNotFound instance.
func NewErrNotFound(entity string) *ErrNotFound {
return &ErrNotFound{
entity: entity,
}
}
func (nf *ErrNotFound) Error() string {
return fmt.Sprintf("{%s} not found", nf.entity)
}
// ErrNotAllFound is an error type that can be returned by store APIs
// when a query that should fetch a certain amount of records
// unexpectedly fetches less.
type ErrNotAllFound struct {
entity string
resources []string
}
func NewErrNotAllFound(entity string, resources []string) *ErrNotAllFound {
return &ErrNotAllFound{
entity: entity,
resources: resources,
}
}
func (naf *ErrNotAllFound) Error() string {
return fmt.Sprintf("not all instances of {%s} in {%s} found", naf.entity, strings.Join(naf.resources, ", "))
}
// ErrBadRequest can be returned when the API handler receives a
// malformed request.
type ErrBadRequest struct {
reason string
}
// NewErrNotFound creates a new ErrNotFound instance.
func NewErrBadRequest(reason string) *ErrBadRequest {
return &ErrBadRequest{
reason: reason,
}
}
func (br *ErrBadRequest) Error() string {
return br.reason
}
// ErrUnauthorized can be returned when requester has provided an
// invalid authorization for a given resource or has not provided any.
type ErrUnauthorized struct {
reason string
}
// NewErrUnauthorized creates a new ErrUnauthorized instance.
func NewErrUnauthorized(reason string) *ErrUnauthorized {
return &ErrUnauthorized{
reason: reason,
}
}
func (br *ErrUnauthorized) Error() string {
return br.reason
}
// ErrPermission can be returned when requester lacks a permission for
// a given resource.
type ErrPermission struct {
reason string
}
// NewErrPermission creates a new ErrPermission instance.
func NewErrPermission(reason string) *ErrPermission {
return &ErrPermission{
reason: reason,
}
}
func (br *ErrPermission) Error() string {
return br.reason
}
// ErrForbidden can be returned when requester doesn't have access to
// a given resource.
type ErrForbidden struct {
reason string
}
// NewErrForbidden creates a new ErrForbidden instance.
func NewErrForbidden(reason string) *ErrForbidden {
return &ErrForbidden{
reason: reason,
}
}
func (br *ErrForbidden) Error() string {
return br.reason
}
type ErrInvalidCategory struct {
msg string
}
func NewErrInvalidCategory(msg string) *ErrInvalidCategory {
return &ErrInvalidCategory{
msg: msg,
}
}
func (e *ErrInvalidCategory) Error() string {
return e.msg
}
type ErrNotImplemented struct {
msg string
}
func NewErrNotImplemented(msg string) *ErrNotImplemented {
return &ErrNotImplemented{
msg: msg,
}
}
func (ni *ErrNotImplemented) Error() string {
return ni.msg
}
// IsErrBadRequest returns true if `err` is or wraps one of:
// - model.ErrBadRequest
// - model.ErrViewsLimitReached
// - model.ErrAuthParam
// - model.ErrInvalidCategory
// - model.ErrBoardMemberIsLastAdmin
// - model.ErrBoardIDMismatch.
func IsErrBadRequest(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrBadRequest
var br *ErrBadRequest
if errors.As(err, &br) {
return true
}
// check if this is a model.ErrAuthParam
var ap *ErrAuthParam
if errors.As(err, &ap) {
return true
}
// check if this is a model.ErrViewsLimitReached
if errors.Is(err, ErrViewsLimitReached) {
return true
}
// check if this is a model.ErrInvalidCategory
var ic *ErrInvalidCategory
if errors.As(err, &ic) {
return true
}
// check if this is a model.ErrBoardIDMismatch
if errors.Is(err, ErrBoardMemberIsLastAdmin) {
return true
}
// check if this is a model.ErrBoardMemberIsLastAdmin
return errors.Is(err, ErrBoardIDMismatch)
}
// IsErrUnauthorized returns true if `err` is or wraps one of:
// - model.ErrUnauthorized.
func IsErrUnauthorized(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrUnauthorized
var u *ErrUnauthorized
return errors.As(err, &u)
}
// IsErrForbidden returns true if `err` is or wraps one of:
// - model.ErrForbidden
// - model.ErrPermission
// - model.ErrPatchUpdatesLimitedCards
// - model.ErrorCategoryPermissionDenied.
func IsErrForbidden(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrForbidden
var f *ErrForbidden
if errors.As(err, &f) {
return true
}
// check if this is a model.ErrPermission
var p *ErrPermission
if errors.As(err, &p) {
return true
}
// check if this is a model.ErrPatchUpdatesLimitedCards
if errors.Is(err, ErrPatchUpdatesLimitedCards) {
return true
}
// check if this is a model.ErrCategoryPermissionDenied
return errors.Is(err, ErrCategoryPermissionDenied)
}
// IsErrNotFound returns true if `err` is or wraps one of:
// - model.ErrNotFound
// - model.ErrNotAllFound
// - sql.ErrNoRows
// - mattermost-plugin-api/ErrNotFound.
// - model.ErrCategoryDeleted.
func IsErrNotFound(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrNotFound
var nf *ErrNotFound
if errors.As(err, &nf) {
return true
}
// check if this is a model.ErrNotAllFound
var naf *ErrNotAllFound
if errors.As(err, &naf) {
return true
}
// check if this is a sql.ErrNotFound
if errors.Is(err, sql.ErrNoRows) {
return true
}
// check if this is a Mattermost AppError with a Not Found status
var appErr *mm_model.AppError
if errors.As(err, &appErr) {
if appErr.StatusCode == http.StatusNotFound {
return true
}
}
// check if this is a model.ErrCategoryDeleted
return errors.Is(err, ErrCategoryDeleted)
}
// IsErrRequestEntityTooLarge returns true if `err` is or wraps one of:
// - model.ErrRequestEntityTooLarge.
func IsErrRequestEntityTooLarge(err error) bool {
// check if this is a model.ErrRequestEntityTooLarge
return errors.Is(err, ErrRequestEntityTooLarge)
}
// IsErrNotImplemented returns true if `err` is or wraps one of:
// - model.ErrNotImplemented
// - model.ErrInsufficientLicense.
func IsErrNotImplemented(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrNotImplemented
var eni *ErrNotImplemented
if errors.As(err, &eni) {
return true
}
// check if this is a model.ErrInsufficientLicense
return errors.Is(err, ErrInsufficientLicense)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"fmt"
)
var (
ErrInvalidImageBlock = errors.New("invalid image block")
)
// Archive is an import / export archive.
// TODO: remove once default templates are converted to new archive format.
type Archive struct {
Version int64 `json:"version"`
Date int64 `json:"date"`
Blocks []Block `json:"blocks"`
}
// ArchiveHeader is the content of the first file (`version.json`) within an archive.
type ArchiveHeader struct {
Version int `json:"version"`
Date int64 `json:"date"`
}
// ArchiveLine is any line in an archive.
type ArchiveLine struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// ExportArchiveOptions provides options when exporting one or more boards
// to an archive.
type ExportArchiveOptions struct {
TeamID string
// BoardIDs is the list of boards to include in the archive.
// Empty slice means export all boards from workspace/team.
BoardIDs []string
}
// ImportArchiveOptions provides options when importing an archive.
type ImportArchiveOptions struct {
TeamID string
ModifiedBy string
BoardModifier BoardModifier
BlockModifier BlockModifier
}
// ErrUnsupportedArchiveVersion is an error returned when trying to import an
// archive with a version that this server does not support.
type ErrUnsupportedArchiveVersion struct {
got int
want int
}
// NewErrUnsupportedArchiveVersion creates a ErrUnsupportedArchiveVersion error.
func NewErrUnsupportedArchiveVersion(got int, want int) ErrUnsupportedArchiveVersion {
return ErrUnsupportedArchiveVersion{
got: got,
want: want,
}
}
func (e ErrUnsupportedArchiveVersion) Error() string {
return fmt.Sprintf("unsupported archive version; got %d, want %d", e.got, e.want)
}
// ErrUnsupportedArchiveLineType is an error returned when trying to import an
// archive containing an unsupported line type.
type ErrUnsupportedArchiveLineType struct {
line int
got string
}
// NewErrUnsupportedArchiveLineType creates a ErrUnsupportedArchiveLineType error.
func NewErrUnsupportedArchiveLineType(line int, got string) ErrUnsupportedArchiveLineType {
return ErrUnsupportedArchiveLineType{
line: line,
got: got,
}
}
func (e ErrUnsupportedArchiveLineType) Error() string {
return fmt.Sprintf("unsupported archive line type; got %s, line %d", e.got, e.line)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// NotificationHint provides a hint that a block has been modified and has subscribers that
// should be notified.
// swagger:model
type NotificationHint struct {
// BlockType is the block type of the entity (e.g. board, card) that was updated
// required: true
BlockType BlockType `json:"block_type"`
// BlockID is id of the entity that was updated
// required: true
BlockID string `json:"block_id"`
// ModifiedByID is the id of the user who made the block change
ModifiedByID string `json:"modified_by_id"`
// CreatedAt is the timestamp this notification hint was created in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"create_at"`
// NotifyAt is the timestamp this notification should be scheduled in miliseconds since the current epoch
// required: true
NotifyAt int64 `json:"notify_at"`
}
func (s *NotificationHint) IsValid() error {
if s == nil {
return ErrInvalidNotificationHint{"cannot be nil"}
}
if s.BlockID == "" {
return ErrInvalidNotificationHint{"missing block id"}
}
if s.BlockType == "" {
return ErrInvalidNotificationHint{"missing block type"}
}
if s.ModifiedByID == "" {
return ErrInvalidNotificationHint{"missing modified_by id"}
}
return nil
}
func (s *NotificationHint) Copy() *NotificationHint {
return &NotificationHint{
BlockType: s.BlockType,
BlockID: s.BlockID,
ModifiedByID: s.ModifiedByID,
CreateAt: s.CreateAt,
NotifyAt: s.NotifyAt,
}
}
func (s *NotificationHint) LogClone() interface{} {
return struct {
BlockType BlockType `json:"block_type"`
BlockID string `json:"block_id"`
ModifiedByID string `json:"modified_by_id"`
CreateAt string `json:"create_at"`
NotifyAt string `json:"notify_at"`
}{
BlockType: s.BlockType,
BlockID: s.BlockID,
ModifiedByID: s.ModifiedByID,
CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli),
NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli),
}
}
type ErrInvalidNotificationHint struct {
msg string
}
func (e ErrInvalidNotificationHint) Error() string {
return e.msg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/propValueResolverMock.go -package mocks . PropValueResolver
package model
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var ErrInvalidBoardBlock = errors.New("invalid board block")
var ErrInvalidPropSchema = errors.New("invalid property schema")
var ErrInvalidProperty = errors.New("invalid property")
var ErrInvalidPropertyValue = errors.New("invalid property value")
var ErrInvalidPropertyValueType = errors.New("invalid property value type")
var ErrInvalidDate = errors.New("invalid date property")
// PropValueResolver allows PropDef.GetValue to further decode property values, such as
// looking up usernames from ids.
type PropValueResolver interface {
GetUserByID(userID string) (*User, error)
}
// BlockProperties is a map of Prop's keyed by property id.
type BlockProperties map[string]BlockProp
// BlockProp represent a property attached to a block (typically a card).
type BlockProp struct {
ID string `json:"id"`
Index int `json:"index"`
Name string `json:"name"`
Value string `json:"value"`
}
// PropSchema is a map of PropDef's keyed by property id.
type PropSchema map[string]PropDef
// PropDefOption represents an option within a property definition.
type PropDefOption struct {
ID string `json:"id"`
Index int `json:"index"`
Color string `json:"color"`
Value string `json:"value"`
}
// PropDef represents a property definition as defined in a board's Fields member.
type PropDef struct {
ID string `json:"id"`
Index int `json:"index"`
Name string `json:"name"`
Type string `json:"type"`
Options map[string]PropDefOption `json:"options"`
}
// GetValue resolves the value of a property if the passed value is an ID for an option,
// otherwise returns the original value.
func (pd PropDef) GetValue(v interface{}, resolver PropValueResolver) (string, error) {
switch pd.Type {
case "select":
// v is the id of an option
id, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
opt, ok := pd.Options[id]
if !ok {
return "", ErrInvalidPropertyValue
}
return strings.ToUpper(opt.Value), nil
case "date":
// v is a JSON string
date, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
return pd.ParseDate(date)
case "person":
// v is a userid
userID, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
if resolver != nil {
user, err := resolver.GetUserByID(userID)
if err != nil {
return "", err
}
if user == nil {
return userID, nil
}
return user.Username, nil
}
return userID, nil
case "multiPerson":
// v is a slice of user IDs
userIDs, ok := v.([]interface{})
if !ok {
return "", fmt.Errorf("multiPerson property type: %w", ErrInvalidPropertyValueType)
}
if resolver != nil {
usernames := make([]string, len(userIDs))
for i, userIDInterface := range userIDs {
userID := userIDInterface.(string)
user, err := resolver.GetUserByID(userID)
if err != nil {
return "", err
}
if user == nil {
usernames[i] = userID
} else {
usernames[i] = user.Username
}
}
return strings.Join(usernames, ", "), nil
}
case "multiSelect":
// v is a slice of strings containing option ids
ms, ok := v.([]interface{})
if !ok {
return "", ErrInvalidPropertyValueType
}
var sb strings.Builder
prefix := ""
for _, optid := range ms {
id, ok := optid.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
opt, ok := pd.Options[id]
if !ok {
return "", ErrInvalidPropertyValue
}
sb.WriteString(prefix)
prefix = ", "
sb.WriteString(strings.ToUpper(opt.Value))
}
return sb.String(), nil
}
return fmt.Sprintf("%v", v), nil
}
func (pd PropDef) ParseDate(s string) (string, error) {
// s is a JSON snippet of the form: {"from":1642161600000, "to":1642161600000} in milliseconds UTC
// The UI does not yet support date ranges.
var m map[string]int64
if err := json.Unmarshal([]byte(s), &m); err != nil {
return s, err
}
tsFrom, ok := m["from"]
if !ok {
return s, ErrInvalidDate
}
date := utils.GetTimeForMillis(tsFrom).Format("January 02, 2006")
tsTo, ok := m["to"]
if ok {
date += " -> " + utils.GetTimeForMillis(tsTo).Format("January 02, 2006")
}
return date, nil
}
// ParsePropertySchema parses a board block's `Fields` to extract the properties
// schema for all cards within the board.
// The result is provided as a map for quick lookup, and the original order is
// preserved via the `Index` field.
func ParsePropertySchema(board *Board) (PropSchema, error) {
schema := make(map[string]PropDef)
for i, prop := range board.CardProperties {
pd := PropDef{
ID: getMapString("id", prop),
Index: i,
Name: getMapString("name", prop),
Type: getMapString("type", prop),
Options: make(map[string]PropDefOption),
}
optsIface, ok := prop["options"]
if ok {
opts, ok := optsIface.([]interface{})
if !ok {
return nil, ErrInvalidPropSchema
}
for j, propOptIface := range opts {
propOpt, ok := propOptIface.(map[string]interface{})
if !ok {
return nil, ErrInvalidPropSchema
}
po := PropDefOption{
ID: getMapString("id", propOpt),
Index: j,
Value: getMapString("value", propOpt),
Color: getMapString("color", propOpt),
}
pd.Options[po.ID] = po
}
}
schema[pd.ID] = pd
}
return schema, nil
}
func getMapString(key string, m map[string]interface{}) string {
iface, ok := m[key]
if !ok {
return ""
}
s, ok := iface.(string)
if !ok {
return ""
}
return s
}
// ParseProperties parses a block's `Fields` to extract the properties. Properties typically exist on
// card blocks. A resolver can optionally be provided to fetch usernames for `person` prop type.
func ParseProperties(block *Block, schema PropSchema, resolver PropValueResolver) (BlockProperties, error) {
props := make(map[string]BlockProp)
if block == nil {
return props, nil
}
// `properties` contains a map (untyped at this point).
propsIface, ok := block.Fields["properties"]
if !ok {
return props, nil // this is expected for blocks that don't have any properties.
}
blockProps, ok := propsIface.(map[string]interface{})
if !ok {
return props, fmt.Errorf("`properties` field wrong type: %w", ErrInvalidProperty)
}
if len(blockProps) == 0 {
return props, nil
}
for k, v := range blockProps {
s := fmt.Sprintf("%v", v)
prop := BlockProp{
ID: k,
Name: k,
Value: s,
}
def, ok := schema[k]
if ok {
val, err := def.GetValue(v, resolver)
if err != nil {
return props, fmt.Errorf("could not parse property value (%s): %w", fmt.Sprintf("%v", v), err)
}
prop.Name = def.Name
prop.Value = val
prop.Index = def.Index
}
props[k] = prop
}
return props, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
// Sharing is sharing information for a root block
// swagger:model
type Sharing struct {
// ID of the root block
// required: true
ID string `json:"id"`
// Is sharing enabled
// required: true
Enabled bool `json:"enabled"`
// Access token
// required: true
Token string `json:"token"`
// ID of the user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"update_at,omitempty"`
}
func SharingFromJSON(data io.Reader) Sharing {
var sharing Sharing
_ = json.NewDecoder(data).Decode(&sharing)
return sharing
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
SubTypeUser = "user"
SubTypeChannel = "channel"
)
type SubscriberType string
func (st SubscriberType) IsValid() bool {
switch st {
case SubTypeUser, SubTypeChannel:
return true
}
return false
}
// Subscription is a subscription to a board, card, etc, for a user or channel.
// swagger:model
type Subscription struct {
// BlockType is the block type of the entity (e.g. board, card) subscribed to
// required: true
BlockType BlockType `json:"blockType"`
// BlockID is id of the entity being subscribed to
// required: true
BlockID string `json:"blockId"`
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
// required: true
SubscriberType SubscriberType `json:"subscriberType"`
// SubscriberID is the id of the entity that is subscribing
// required: true
SubscriberID string `json:"subscriberId"`
// NotifiedAt is the timestamp of the last notification sent for this subscription
// required: true
NotifiedAt int64 `json:"notifiedAt,omitempty"`
// CreatedAt is the timestamp this subscription was created in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// DeleteAt is the timestamp this subscription was deleted in miliseconds since the current epoch, or zero if not deleted
// required: true
DeleteAt int64 `json:"deleteAt"`
}
func (s *Subscription) IsValid() error {
if s == nil {
return ErrInvalidSubscription{"cannot be nil"}
}
if s.BlockID == "" {
return ErrInvalidSubscription{"missing block id"}
}
if s.BlockType == "" {
return ErrInvalidSubscription{"missing block type"}
}
if s.SubscriberID == "" {
return ErrInvalidSubscription{"missing subscriber id"}
}
if !s.SubscriberType.IsValid() {
return ErrInvalidSubscription{"invalid subscriber type"}
}
return nil
}
func SubscriptionFromJSON(data io.Reader) (*Subscription, error) {
var subscription Subscription
if err := json.NewDecoder(data).Decode(&subscription); err != nil {
return nil, err
}
return &subscription, nil
}
type ErrInvalidSubscription struct {
msg string
}
func (e ErrInvalidSubscription) Error() string {
return e.msg
}
// Subscriber is an entity (e.g. user, channel) that can subscribe to events from boards, cards, etc
// swagger:model
type Subscriber struct {
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
// required: true
SubscriberType SubscriberType `json:"subscriber_type"`
// SubscriberID is the id of the entity that is subscribing
// required: true
SubscriberID string `json:"subscriber_id"`
// NotifiedAt is the timestamp this subscriber was last notified
NotifiedAt int64 `json:"notified_at"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
// Team is information global to a team
// swagger:model
type Team struct {
// ID of the team
// required: true
ID string `json:"id"`
// Title of the team
// required: false
Title string `json:"title"`
// Token required to register new users
// required: true
SignupToken string `json:"signupToken"`
// Team settings
// required: false
Settings map[string]interface{} `json:"settings"`
// ID of user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
}
func TeamFromJSON(data io.Reader) *Team {
var team *Team
_ = json.NewDecoder(data).Decode(&team)
return team
}
func TeamsFromJSON(data io.Reader) []*Team {
var teams []*Team
_ = json.NewDecoder(data).Decode(&teams)
return teams
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
SingleUser = "single-user"
GlobalTeamID = "0"
SystemUserID = "system"
PreferencesCategoryFocalboard = "focalboard"
)
// User is a user
// swagger:model
type User struct {
// The user ID
// required: true
ID string `json:"id"`
// The user name
// required: true
Username string `json:"username"`
// The user's email
// required: true
Email string `json:"-"`
// The user's nickname
Nickname string `json:"nickname"`
// The user's first name
FirstName string `json:"firstname"`
// The user's last name
LastName string `json:"lastname"`
// swagger:ignore
Password string `json:"-"`
// swagger:ignore
MfaSecret string `json:"-"`
// swagger:ignore
AuthService string `json:"-"`
// swagger:ignore
AuthData string `json:"-"`
// Created time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"create_at,omitempty"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"update_at,omitempty"`
// Deleted time in miliseconds since the current epoch, set to indicate user is deleted
// required: true
DeleteAt int64 `json:"delete_at"`
// If the user is a bot or not
// required: true
IsBot bool `json:"is_bot"`
// If the user is a guest or not
// required: true
IsGuest bool `json:"is_guest"`
// Special Permissions the user may have
Permissions []string `json:"permissions,omitempty"`
Roles string `json:"roles"`
}
// UserPreferencesPatch is a user property patch
// swagger:model
type UserPreferencesPatch struct {
// The user preference updated fields
// required: false
UpdatedFields map[string]string `json:"updatedFields"`
// The user preference removed fields
// required: false
DeletedFields []string `json:"deletedFields"`
}
type Session struct {
ID string `json:"id"`
Token string `json:"token"`
UserID string `json:"user_id"`
AuthService string `json:"authService"`
Props map[string]interface{} `json:"props"`
CreateAt int64 `json:"create_at,omitempty"`
UpdateAt int64 `json:"update_at,omitempty"`
}
func UserFromJSON(data io.Reader) (*User, error) {
var user User
if err := json.NewDecoder(data).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"time"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
return mm_model.GetMillis()
}
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
return mm_model.GetMillisForTime(thisTime)
}
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
func GetTimeForMillis(millis int64) time.Time {
return mm_model.GetTimeForMillis(millis)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// This is a list of all the current versions including any patches.
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
"7.9.0",
"7.8.0",
"7.7.0",
"7.6.0",
"7.5.0",
"7.4.0",
"7.3.0",
"7.2.0",
"7.0.0",
"0.16.0",
"0.15.0",
"0.14.0",
"0.12.0",
"0.11.0",
"0.10.0",
"0.9.4",
"0.9.3",
"0.9.2",
"0.9.1",
"0.9.0",
"0.8.2",
"0.8.1",
"0.8.0",
"0.7.3",
"0.7.2",
"0.7.1",
"0.7.0",
"0.6.7",
"0.6.6",
"0.6.5",
"0.6.2",
"0.6.1",
"0.6.0",
"0.5.0",
}
var (
CurrentVersion = versions[0]
BuildNumber string
BuildDate string
BuildHash string
Edition string
)
// LogServerInfo logs information about the server instance.
func LogServerInfo(logger mlog.LoggerIFace) {
logger.Info("Focalboard server",
mlog.String("version", CurrentVersion),
mlog.String("edition", Edition),
mlog.String("build_number", BuildNumber),
mlog.String("build_date", BuildDate),
mlog.String("build_hash", BuildHash),
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"database/sql"
"github.com/gorilla/mux"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
// normalizeAppError returns a truly nil error if appErr is nil
// See https://golang.org/doc/faq#nil_error for more details.
func normalizeAppErr(appErr *mm_model.AppError) error {
if appErr == nil {
return nil
}
return appErr
}
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
// be used as per the Plugin API.
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
// can be modified to use the services in modular fashion.
type serviceAPIAdapter struct {
api *boardsProduct
ctx *request.Context
}
func newServiceAPIAdapter(api *boardsProduct) *serviceAPIAdapter {
return &serviceAPIAdapter{
api: api,
ctx: request.EmptyContext(api.logger),
}
}
//
// Channels service.
//
func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetChannelByID(channelID)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) {
member, appErr := a.api.channelService.GetChannelMember(channelID, userID)
return member, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) {
opts := &mm_model.ChannelSearchOpts{
IncludeDeleted: includeDeleted,
}
channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts)
return channels, normalizeAppErr(appErr)
}
//
// Post service.
//
func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
post, appErr := a.api.postService.CreatePost(a.ctx, post)
return post, normalizeAppErr(appErr)
}
//
// User service.
//
func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUser(userID)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUserByUsername(name)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUserByEmail(email)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) {
user, appErr := a.api.userService.UpdateUser(a.ctx, user, true)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) {
user, appErr := a.api.userService.GetUsersFromProfiles(options)
return user, normalizeAppErr(appErr)
}
//
// Team service.
//
func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) {
member, appErr := a.api.teamService.GetMember(teamID, userID)
return member, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) {
member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID)
return member, normalizeAppErr(appErr)
}
//
// Permissions service.
//
func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionTo(userID, permission)
}
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
}
func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission)
}
//
// Bot service.
//
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
return a.api.botService.EnsureBot(a.ctx, boardsProductID, bot)
}
//
// License service.
//
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
return a.api.licenseService.GetLicense()
}
//
// FileInfoStore service.
//
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
return fi, normalizeAppErr(appErr)
}
//
// Cluster store.
//
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
a.api.clusterService.PublishWebSocketEvent(boardsProductName, event, payload, broadcast)
}
func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error {
return a.api.clusterService.PublishPluginClusterEvent(boardsProductName, ev, opts)
}
//
// Cloud service.
//
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
return a.api.cloudService.GetCloudLimits()
}
//
// Config service.
//
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
return a.api.configService.Config()
}
//
// Logger service.
//
func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace {
return a.api.logger
}
//
// KVStore service.
//
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductName, key, value, options)
return b, normalizeAppErr(appErr)
}
//
// Store service.
//
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
return a.api.storeService.GetMasterDB(), nil
}
//
// System service.
//
func (a *serviceAPIAdapter) GetDiagnosticID() string {
return a.api.systemService.GetDiagnosticId()
}
//
// Router service.
//
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
a.api.routerService.RegisterRouter(boardsProductName, sub)
}
//
// Preferences service.
//
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
return p, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
// Ensure the adapter implements ServicesAPI.
var _ model.ServicesAPI = &serviceAPIAdapter{}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/server"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
boardsProductName = "boards"
boardsProductID = "com.mattermost.boards"
)
var errServiceTypeAssert = errors.New("type assertion failed")
func init() {
product.RegisterProduct(boardsProductName, product.Manifest{
Initializer: newBoardsProduct,
Dependencies: map[product.ServiceKey]struct{}{
product.TeamKey: {},
product.ChannelKey: {},
product.UserKey: {},
product.PostKey: {},
product.PermissionsKey: {},
product.BotKey: {},
product.ClusterKey: {},
product.ConfigKey: {},
product.LogKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
product.FileInfoStoreKey: {},
product.RouterKey: {},
product.CloudKey: {},
product.KVStoreKey: {},
product.StoreKey: {},
product.SystemKey: {},
product.PreferencesKey: {},
product.HooksKey: {},
},
})
}
type boardsProduct struct {
teamService product.TeamService
channelService product.ChannelService
userService product.UserService
postService product.PostService
permissionsService product.PermissionService
botService product.BotService
clusterService product.ClusterService
configService product.ConfigService
logger mlog.LoggerIFace
licenseService product.LicenseService
filestoreService product.FilestoreService
fileInfoStoreService product.FileInfoStoreService
routerService product.RouterService
cloudService product.CloudService
kvStoreService product.KVStoreService
storeService product.StoreService
systemService product.SystemService
preferencesService product.PreferencesService
hooksService product.HooksService
boardsApp *server.BoardsService
}
func newBoardsProduct(services map[product.ServiceKey]interface{}) (product.Product, error) {
boardsProd := &boardsProduct{}
if err := populateServices(boardsProd, services); err != nil {
return nil, err
}
boardsProd.logger.Info("Creating boards service")
adapter := newServiceAPIAdapter(boardsProd)
boardsApp, err := server.NewBoardsService(adapter)
if err != nil {
return nil, fmt.Errorf("failed to create Boards service: %w", err)
}
boardsProd.boardsApp = boardsApp
// Add the Boards services API to the services map so other products can access Boards functionality.
boardsAPI := server.NewBoardsServiceAPI(boardsApp)
services[product.BoardsKey] = boardsAPI
return boardsProd, nil
}
// populateServices populates the boardProduct with all the services needed from the suite.
func populateServices(boardsProd *boardsProduct, services map[product.ServiceKey]interface{}) error {
for key, service := range services {
switch key {
case product.TeamKey:
teamService, ok := service.(product.TeamService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.teamService = teamService
case product.ChannelKey:
channelService, ok := service.(product.ChannelService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.channelService = channelService
case product.UserKey:
userService, ok := service.(product.UserService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.userService = userService
case product.PostKey:
postService, ok := service.(product.PostService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.postService = postService
case product.PermissionsKey:
permissionsService, ok := service.(product.PermissionService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.permissionsService = permissionsService
case product.BotKey:
botService, ok := service.(product.BotService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.botService = botService
case product.ClusterKey:
clusterService, ok := service.(product.ClusterService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.clusterService = clusterService
case product.ConfigKey:
configService, ok := service.(product.ConfigService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.configService = configService
case product.LogKey:
logger, ok := service.(mlog.LoggerIFace)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.logger = logger.With(mlog.String("product", boardsProductName))
case product.LicenseKey:
licenseService, ok := service.(product.LicenseService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.licenseService = licenseService
case product.FilestoreKey:
filestoreService, ok := service.(product.FilestoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.filestoreService = filestoreService
case product.FileInfoStoreKey:
fileInfoStoreService, ok := service.(product.FileInfoStoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.fileInfoStoreService = fileInfoStoreService
case product.RouterKey:
routerService, ok := service.(product.RouterService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.routerService = routerService
case product.CloudKey:
cloudService, ok := service.(product.CloudService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.cloudService = cloudService
case product.KVStoreKey:
kvStoreService, ok := service.(product.KVStoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.kvStoreService = kvStoreService
case product.StoreKey:
storeService, ok := service.(product.StoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.storeService = storeService
case product.SystemKey:
systemService, ok := service.(product.SystemService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.systemService = systemService
case product.PreferencesKey:
preferencesService, ok := service.(product.PreferencesService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.preferencesService = preferencesService
case product.HooksKey:
hooksService, ok := service.(product.HooksService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boardsProd.hooksService = hooksService
}
}
return nil
}
func (bp *boardsProduct) Start() error {
if !bp.configService.Config().FeatureFlags.BoardsProduct {
bp.logger.Info("Boards product disabled via feature flag")
return nil
}
bp.logger.Info("Starting boards service")
adapter := newServiceAPIAdapter(bp)
boardsApp, err := server.NewBoardsService(adapter)
if err != nil {
return fmt.Errorf("failed to create Boards service: %w", err)
}
model.LogServerInfo(bp.logger)
if err := bp.hooksService.RegisterHooks(boardsProductName, bp); err != nil {
return fmt.Errorf("failed to register hooks: %w", err)
}
bp.boardsApp = boardsApp
if err := bp.boardsApp.Start(); err != nil {
return fmt.Errorf("failed to start Boards service: %w", err)
}
return nil
}
func (bp *boardsProduct) Stop() error {
bp.logger.Info("Stopping boards service")
if bp.boardsApp == nil {
return nil
}
if err := bp.boardsApp.Stop(); err != nil {
return fmt.Errorf("error while stopping Boards service: %w", err)
}
return nil
}
//
// These callbacks are called by the suite automatically
//
func (bp *boardsProduct) OnConfigurationChange() error {
if bp.boardsApp == nil {
return nil
}
return bp.boardsApp.OnConfigurationChange()
}
func (bp *boardsProduct) OnWebSocketConnect(webConnID, userID string) {
if bp.boardsApp == nil {
return
}
bp.boardsApp.OnWebSocketConnect(webConnID, userID)
}
func (bp *boardsProduct) OnWebSocketDisconnect(webConnID, userID string) {
if bp.boardsApp == nil {
return
}
bp.boardsApp.OnWebSocketDisconnect(webConnID, userID)
}
func (bp *boardsProduct) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
if bp.boardsApp == nil {
return
}
bp.boardsApp.WebSocketMessageHasBeenPosted(webConnID, userID, req)
}
func (bp *boardsProduct) OnPluginClusterEvent(ctx *plugin.Context, ev mm_model.PluginClusterEvent) {
if bp.boardsApp == nil {
return
}
bp.boardsApp.OnPluginClusterEvent(ctx, ev)
}
func (bp *boardsProduct) MessageWillBePosted(ctx *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) {
if bp.boardsApp == nil {
return post, ""
}
return bp.boardsApp.MessageWillBePosted(ctx, post)
}
func (bp *boardsProduct) MessageWillBeUpdated(ctx *plugin.Context, newPost, oldPost *mm_model.Post) (*mm_model.Post, string) {
if bp.boardsApp == nil {
return newPost, ""
}
return bp.boardsApp.MessageWillBeUpdated(ctx, newPost, oldPost)
}
func (bp *boardsProduct) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) {
if bp.boardsApp == nil {
return
}
bp.boardsApp.OnCloudLimitsUpdated(limits)
}
func (bp *boardsProduct) RunDataRetention(nowTime, batchSize int64) (int64, error) {
if bp.boardsApp == nil {
return 0, nil
}
return bp.boardsApp.RunDataRetention(nowTime, batchSize)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"fmt"
"net/http"
"sync"
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions/mmpermissions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store/mattermostauthlayer"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/boards/ws"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
boardsFeatureFlagName = "BoardsFeatureFlags"
PluginName = "focalboard"
SharedBoardsName = "enablepublicsharedboards"
notifyFreqCardSecondsKey = "notify_freq_card_seconds"
notifyFreqBoardSecondsKey = "notify_freq_board_seconds"
)
type BoardsEmbed struct {
OriginalPath string `json:"originalPath"`
TeamID string `json:"teamID"`
ViewID string `json:"viewID"`
BoardID string `json:"boardID"`
CardID string `json:"cardID"`
ReadToken string `json:"readToken,omitempty"`
}
type BoardsService struct {
// configurationLock synchronizes access to the configuration.
configurationLock sync.RWMutex
// configuration is the active plugin configuration. Consult getConfiguration and
// setConfiguration for usage.
configuration *configuration
server *Server
wsPluginAdapter ws.PluginAdapterInterface
servicesAPI model.ServicesAPI
logger mlog.LoggerIFace
}
func NewBoardsServiceForTest(server *Server, wsPluginAdapter ws.PluginAdapterInterface,
api model.ServicesAPI, logger mlog.LoggerIFace) *BoardsService {
return &BoardsService{
server: server,
wsPluginAdapter: wsPluginAdapter,
servicesAPI: api,
logger: logger,
}
}
func NewBoardsService(api model.ServicesAPI) (*BoardsService, error) {
mmconfig := api.GetConfig()
logger := api.GetLogger()
baseURL := ""
if mmconfig.ServiceSettings.SiteURL != nil {
baseURL = *mmconfig.ServiceSettings.SiteURL
}
serverID := api.GetDiagnosticID()
cfg := CreateBoardsConfig(*mmconfig, baseURL, serverID)
sqlDB, err := api.GetMasterDB()
if err != nil {
return nil, fmt.Errorf("cannot access database while initializing Boards: %w", err)
}
storeParams := sqlstore.Params{
DBType: cfg.DBType,
ConnectionString: cfg.DBConfigString,
TablePrefix: cfg.DBTablePrefix,
Logger: logger,
DB: sqlDB,
IsPlugin: true,
ServicesAPI: api,
ConfigFn: api.GetConfig,
}
var db store.Store
db, err = sqlstore.New(storeParams)
if err != nil {
return nil, fmt.Errorf("error initializing the DB: %w", err)
}
if cfg.AuthMode == MattermostAuthMod {
layeredStore, err2 := mattermostauthlayer.New(cfg.DBType, sqlDB, db, logger, api, storeParams.TablePrefix)
if err2 != nil {
return nil, fmt.Errorf("error initializing the DB: %w", err2)
}
db = layeredStore
}
permissionsService := mmpermissions.New(db, api, logger)
wsPluginAdapter := ws.NewPluginAdapter(api, auth.New(cfg, db, permissionsService), db, logger)
backendParams := notifyBackendParams{
cfg: cfg,
servicesAPI: api,
appAPI: &appAPI{store: db},
permissions: permissionsService,
serverRoot: baseURL + "/boards",
logger: logger,
}
var notifyBackends []notify.Backend
mentionsBackend, err := createMentionsNotifyBackend(backendParams)
if err != nil {
return nil, fmt.Errorf("error creating mention notifications backend: %w", err)
}
notifyBackends = append(notifyBackends, mentionsBackend)
subscriptionsBackend, err2 := createSubscriptionsNotifyBackend(backendParams)
if err2 != nil {
return nil, fmt.Errorf("error creating subscription notifications backend: %w", err2)
}
notifyBackends = append(notifyBackends, subscriptionsBackend)
mentionsBackend.AddListener(subscriptionsBackend)
params := Params{
Cfg: cfg,
SingleUserToken: "",
DBStore: db,
Logger: logger,
ServerID: serverID,
WSAdapter: wsPluginAdapter,
NotifyBackends: notifyBackends,
PermissionsService: permissionsService,
IsPlugin: true,
}
server, err := New(params)
if err != nil {
return nil, fmt.Errorf("error initializing the server: %w", err)
}
backendParams.appAPI.init(db, server.App())
// ToDo: Cloud Limits have been disabled by design. We should
// revisit the decision and update the related code accordingly
/*
if utils.IsCloudLicense(api.GetLicense()) {
limits, err := api.GetCloudLimits()
if err != nil {
return nil, fmt.Errorf("error fetching cloud limits when starting Boards: %w", err)
}
if err := server.App().SetCloudLimits(limits); err != nil {
return nil, fmt.Errorf("error setting cloud limits when starting Boards: %w", err)
}
}
*/
return &BoardsService{
server: server,
wsPluginAdapter: wsPluginAdapter,
servicesAPI: api,
logger: logger,
}, nil
}
func (b *BoardsService) Start() error {
if err := b.server.Start(); err != nil {
return fmt.Errorf("error starting Boards server: %w", err)
}
b.servicesAPI.RegisterRouter(b.server.GetRootRouter())
b.logger.Info("Boards product successfully started.")
return nil
}
func (b *BoardsService) Stop() error {
return b.server.Shutdown()
}
//
// These callbacks are called automatically by the suite server.
//
func (b *BoardsService) MessageWillBePosted(_ *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) {
return postWithBoardsEmbed(post), ""
}
func (b *BoardsService) MessageWillBeUpdated(_ *plugin.Context, newPost, _ *mm_model.Post) (*mm_model.Post, string) {
return postWithBoardsEmbed(newPost), ""
}
func (b *BoardsService) OnWebSocketConnect(webConnID, userID string) {
b.wsPluginAdapter.OnWebSocketConnect(webConnID, userID)
}
func (b *BoardsService) OnWebSocketDisconnect(webConnID, userID string) {
b.wsPluginAdapter.OnWebSocketDisconnect(webConnID, userID)
}
func (b *BoardsService) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
b.wsPluginAdapter.WebSocketMessageHasBeenPosted(webConnID, userID, req)
}
func (b *BoardsService) OnPluginClusterEvent(_ *plugin.Context, ev mm_model.PluginClusterEvent) {
b.wsPluginAdapter.HandleClusterEvent(ev)
}
func (b *BoardsService) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) {
if err := b.server.App().SetCloudLimits(limits); err != nil {
b.logger.Error("Error setting the cloud limits for Boards", mlog.Err(err))
}
}
func (b *BoardsService) Config() *config.Configuration {
return b.server.Config()
}
func (b *BoardsService) ClientConfig() *model.ClientConfig {
return b.server.App().GetClientConfig()
}
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
func (b *BoardsService) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) {
router := b.server.GetRootRouter()
router.ServeHTTP(w, r)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
// boardsServiceAPI provides a service API for other products such as Channels.
type boardsServiceAPI struct {
app *app.App
}
func NewBoardsServiceAPI(app *BoardsService) *boardsServiceAPI {
return &boardsServiceAPI{
app: app.server.App(),
}
}
func (bs *boardsServiceAPI) GetTemplates(teamID string, userID string) ([]*model.Board, error) {
return bs.app.GetTemplateBoards(teamID, userID)
}
func (bs *boardsServiceAPI) GetBoard(boardID string) (*model.Board, error) {
return bs.app.GetBoard(boardID)
}
func (bs *boardsServiceAPI) CreateBoard(board *model.Board, userID string, addmember bool) (*model.Board, error) {
return bs.app.CreateBoard(board, userID, addmember)
}
func (bs *boardsServiceAPI) PatchBoard(boardPatch *model.BoardPatch, boardID string, userID string) (*model.Board, error) {
return bs.app.PatchBoard(boardPatch, boardID, userID)
}
func (bs *boardsServiceAPI) DeleteBoard(boardID string, userID string) error {
return bs.app.DeleteBoard(boardID, userID)
}
func (bs *boardsServiceAPI) SearchBoards(searchTerm string, searchField model.BoardSearchField,
userID string, includePublicBoards bool) ([]*model.Board, error) {
return bs.app.SearchBoardsForUser(searchTerm, searchField, userID, includePublicBoards)
}
func (bs *boardsServiceAPI) LinkBoardToChannel(boardID string, channelID string, userID string) (*model.Board, error) {
patch := &model.BoardPatch{
ChannelID: &channelID,
}
return bs.app.PatchBoard(patch, boardID, userID)
}
func (bs *boardsServiceAPI) GetCards(boardID string) ([]*model.Card, error) {
return bs.app.GetCardsForBoard(boardID, 0, 0)
}
func (bs *boardsServiceAPI) GetCard(cardID string) (*model.Card, error) {
return bs.app.GetCardByID(cardID)
}
func (bs *boardsServiceAPI) CreateCard(card *model.Card, boardID string, userID string) (*model.Card, error) {
return bs.app.CreateCard(card, boardID, userID, false)
}
func (bs *boardsServiceAPI) PatchCard(cardPatch *model.CardPatch, cardID string, userID string) (*model.Card, error) {
return bs.app.PatchCard(cardPatch, cardID, userID, false)
}
func (bs *boardsServiceAPI) DeleteCard(cardID string, userID string) error {
return bs.app.DeleteBlock(cardID, userID)
}
func (bs *boardsServiceAPI) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool {
return bs.app.HasPermissionToBoard(userID, boardID, permission)
}
func (bs *boardsServiceAPI) DuplicateBoard(boardID string, userID string,
toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
return bs.app.DuplicateBoard(boardID, userID, toTeam, asTemplate)
}
// Ensure boardsServiceAPI implements product.BoardsService interface.
var _ product.BoardsService = (*boardsServiceAPI)(nil)
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"math"
"path"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
const defaultS3Timeout = 60 * 1000 // 60 seconds
func CreateBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID string) *config.Configuration {
filesS3Config := config.AmazonS3Config{}
if mmconfig.FileSettings.AmazonS3AccessKeyId != nil {
filesS3Config.AccessKeyID = *mmconfig.FileSettings.AmazonS3AccessKeyId
}
if mmconfig.FileSettings.AmazonS3SecretAccessKey != nil {
filesS3Config.SecretAccessKey = *mmconfig.FileSettings.AmazonS3SecretAccessKey
}
if mmconfig.FileSettings.AmazonS3Bucket != nil {
filesS3Config.Bucket = *mmconfig.FileSettings.AmazonS3Bucket
}
if mmconfig.FileSettings.AmazonS3PathPrefix != nil {
filesS3Config.PathPrefix = *mmconfig.FileSettings.AmazonS3PathPrefix
}
if mmconfig.FileSettings.AmazonS3Region != nil {
filesS3Config.Region = *mmconfig.FileSettings.AmazonS3Region
}
if mmconfig.FileSettings.AmazonS3Endpoint != nil {
filesS3Config.Endpoint = *mmconfig.FileSettings.AmazonS3Endpoint
}
if mmconfig.FileSettings.AmazonS3SSL != nil {
filesS3Config.SSL = *mmconfig.FileSettings.AmazonS3SSL
}
if mmconfig.FileSettings.AmazonS3SignV2 != nil {
filesS3Config.SignV2 = *mmconfig.FileSettings.AmazonS3SignV2
}
if mmconfig.FileSettings.AmazonS3SSE != nil {
filesS3Config.SSE = *mmconfig.FileSettings.AmazonS3SSE
}
if mmconfig.FileSettings.AmazonS3Trace != nil {
filesS3Config.Trace = *mmconfig.FileSettings.AmazonS3Trace
}
if mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds != nil && *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds > 0 {
filesS3Config.Timeout = *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds
} else {
filesS3Config.Timeout = defaultS3Timeout
}
enableTelemetry := false
if mmconfig.LogSettings.EnableDiagnostics != nil {
enableTelemetry = *mmconfig.LogSettings.EnableDiagnostics
}
enablePublicSharedBoards := false
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
enablePublicSharedBoards = true
}
enableBoardsDeletion := false
if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil {
enableBoardsDeletion = true
}
featureFlags := parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
showEmailAddress := false
if mmconfig.PrivacySettings.ShowEmailAddress != nil {
showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress
}
showFullName := false
if mmconfig.PrivacySettings.ShowFullName != nil {
showFullName = *mmconfig.PrivacySettings.ShowFullName
}
serverRoot := baseURL + "/plugins/focalboard"
if mmconfig.FeatureFlags.BoardsProduct {
serverRoot = baseURL + "/boards"
}
return &config.Configuration{
ServerRoot: serverRoot,
Port: -1,
DBType: *mmconfig.SqlSettings.DriverName,
DBConfigString: *mmconfig.SqlSettings.DataSource,
DBTablePrefix: "focalboard_",
UseSSL: false,
SecureCookie: true,
WebPath: path.Join(*mmconfig.PluginSettings.Directory, "focalboard", "pack"),
FilesDriver: *mmconfig.FileSettings.DriverName,
FilesPath: *mmconfig.FileSettings.Directory,
FilesS3Config: filesS3Config,
MaxFileSize: *mmconfig.FileSettings.MaxFileSize,
Telemetry: enableTelemetry,
TelemetryID: serverID,
WebhookUpdate: []string{},
SessionExpireTime: 2592000,
SessionRefreshTime: 18000,
LocalOnly: false,
EnableLocalMode: false,
LocalModeSocketLocation: "",
AuthMode: "mattermost",
EnablePublicSharedBoards: enablePublicSharedBoards,
FeatureFlags: featureFlags,
NotifyFreqCardSeconds: getPluginSettingInt(mmconfig, notifyFreqCardSecondsKey, 120),
NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400),
EnableDataRetention: enableBoardsDeletion,
DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays,
TeammateNameDisplay: *mmconfig.TeamSettings.TeammateNameDisplay,
ShowEmailAddress: showEmailAddress,
ShowFullName: showFullName,
}
}
func getPluginSetting(mmConfig mm_model.Config, key string) (interface{}, bool) {
plugin, ok := mmConfig.PluginSettings.Plugins[PluginName]
if !ok {
return nil, false
}
val, ok := plugin[key]
if !ok {
return nil, false
}
return val, true
}
func getPluginSettingInt(mmConfig mm_model.Config, key string, def int) int {
val, ok := getPluginSetting(mmConfig, key)
if !ok {
return def
}
valFloat, ok := val.(float64)
if !ok {
return def
}
return int(math.Round(valFloat))
}
func parseFeatureFlags(configFeatureFlags map[string]string) map[string]string {
featureFlags := make(map[string]string)
for key, value := range configFeatureFlags {
// Break out FeatureFlags and pass remaining
if key == boardsFeatureFlagName {
for _, flag := range strings.Split(value, "-") {
featureFlags[flag] = "true"
}
} else {
featureFlags[key] = value
}
}
return featureFlags
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"reflect"
)
// configuration captures the plugin's external configuration as exposed in the Mattermost server
// configuration, as well as values computed from the configuration. Any public fields will be
// deserialized from the Mattermost server configuration in OnConfigurationChange.
//
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
// configuration can change at any time, access to the configuration must be synchronized. The
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
// struct whenever it changes. You may replace this with whatever strategy you choose.
//
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
// copy appropriate for your types.
type configuration struct {
EnablePublicSharedBoards bool
}
// Clone shallow copies the configuration. Your implementation may require a deep copy if
// your configuration has reference types.
func (c *configuration) Clone() *configuration {
var clone = *c
return &clone
}
// getConfiguration retrieves the active configuration under lock, making it safe to use
// concurrently. The active configuration may change underneath the client of this method, but
// the struct returned by this API call is considered immutable.
/*
func (b *BoardsService) getConfiguration() *configuration {
b.configurationLock.RLock()
defer b.configurationLock.RUnlock()
if b.configuration == nil {
return &configuration{}
}
return b.configuration
}
*/
// setConfiguration replaces the active configuration under lock.
//
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
//
// This method panics if setConfiguration is called with the existing configuration. This almost
// certainly means that the configuration was modified without being cloned and may result in
// an unsafe access.
func (b *BoardsService) setConfiguration(configuration *configuration) {
b.configurationLock.Lock()
defer b.configurationLock.Unlock()
if configuration != nil && b.configuration == configuration {
// Ignore assignment if the configuration struct is empty. Go will optimize the
// allocation for same to point at the same memory address, breaking the check
// above.
if reflect.ValueOf(*configuration).NumField() == 0 {
return
}
panic("setConfiguration called with the existing configuration")
}
b.configuration = configuration
}
// OnConfigurationChange is invoked when configuration changes may have been made.
func (b *BoardsService) OnConfigurationChange() error {
// Have we been setup by OnActivate?
if b.server == nil {
return nil
}
mmconfig := b.servicesAPI.GetConfig()
// handle plugin configuration settings
enableShareBoards := false
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
enableShareBoards = true
}
if mmconfig.ProductSettings.EnablePublicSharedBoards != nil {
enableShareBoards = *mmconfig.ProductSettings.EnablePublicSharedBoards
}
configuration := &configuration{
EnablePublicSharedBoards: enableShareBoards,
}
b.setConfiguration(configuration)
b.server.Config().EnablePublicSharedBoards = enableShareBoards
// handle feature flags
b.server.Config().FeatureFlags = parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
// handle Data Retention settings
enableBoardsDeletion := false
if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil {
enableBoardsDeletion = true
}
b.server.Config().EnableDataRetention = enableBoardsDeletion
b.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays
b.server.Config().TeammateNameDisplay = *mmconfig.TeamSettings.TeammateNameDisplay
showEmailAddress := false
if mmconfig.PrivacySettings.ShowEmailAddress != nil {
showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress
}
b.server.Config().ShowEmailAddress = showEmailAddress
showFullName := false
if mmconfig.PrivacySettings.ShowFullName != nil {
showFullName = *mmconfig.PrivacySettings.ShowFullName
}
b.server.Config().ShowFullName = showFullName
maxFileSize := int64(0)
if mmconfig.FileSettings.MaxFileSize != nil {
maxFileSize = *mmconfig.FileSettings.MaxFileSize
}
b.server.Config().MaxFileSize = maxFileSize
b.server.UpdateAppConfig()
b.wsPluginAdapter.BroadcastConfigChange(*b.server.App().GetClientConfig())
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"errors"
"time"
)
var ErrInsufficientLicense = errors.New("appropriate license required")
func (b *BoardsService) RunDataRetention(nowTime, batchSize int64) (int64, error) {
b.logger.Debug("Boards RunDataRetention")
license := b.server.Store().GetLicense()
if license == nil || !(*license.Features.DataRetention) {
return 0, ErrInsufficientLicense
}
if b.server.Config().EnableDataRetention {
boardsRetentionDays := b.server.Config().DataRetentionDays
endTimeBoards := convertDaysToCutoff(boardsRetentionDays, time.Unix(nowTime/1000, 0))
return b.server.Store().RunDataRetention(endTimeBoards, batchSize)
}
return 0, nil
}
func convertDaysToCutoff(days int, now time.Time) int64 {
upToStartOfDay := now.AddDate(0, 0, -days)
cutoffDate := time.Date(upToStartOfDay.Year(), upToStartOfDay.Month(), upToStartOfDay.Day(), 0, 0, 0, 0, time.Local)
return cutoffDate.UnixNano() / int64(time.Millisecond)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
func (s *Server) initHandlers() {
cfg := s.config
s.api.MattermostAuth = cfg.AuthMode == MattermostAuthMod
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify/notifymentions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify/notifysubscriptions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify/plugindelivery"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type notifyBackendParams struct {
cfg *config.Configuration
servicesAPI model.ServicesAPI
permissions permissions.PermissionsService
appAPI *appAPI
serverRoot string
logger mlog.LoggerIFace
}
func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) {
delivery, err := createDelivery(params.servicesAPI, params.serverRoot)
if err != nil {
return nil, err
}
backendParams := notifymentions.BackendParams{
AppAPI: params.appAPI,
Permissions: params.permissions,
Delivery: delivery,
Logger: params.logger,
}
backend := notifymentions.New(backendParams)
return backend, nil
}
func createSubscriptionsNotifyBackend(params notifyBackendParams) (*notifysubscriptions.Backend, error) {
delivery, err := createDelivery(params.servicesAPI, params.serverRoot)
if err != nil {
return nil, err
}
backendParams := notifysubscriptions.BackendParams{
ServerRoot: params.serverRoot,
AppAPI: params.appAPI,
Permissions: params.permissions,
Delivery: delivery,
Logger: params.logger,
NotifyFreqCardSeconds: params.cfg.NotifyFreqCardSeconds,
NotifyFreqBoardSeconds: params.cfg.NotifyFreqBoardSeconds,
}
backend := notifysubscriptions.New(backendParams)
return backend, nil
}
func createDelivery(servicesAPI model.ServicesAPI, serverRoot string) (*plugindelivery.PluginDelivery, error) {
bot := model.FocalboardBot
botID, err := servicesAPI.EnsureBot(bot)
if err != nil {
return nil, fmt.Errorf("failed to ensure %s bot: %w", bot.DisplayName, err)
}
return plugindelivery.New(botID, serverRoot, servicesAPI), nil
}
type appIface interface {
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error)
}
// appAPI provides app and store APIs for notification services. Where appropriate calls are made to the
// app layer to leverage the additional websocket notification logic present there, and other times the
// store APIs are called directly.
type appAPI struct {
store store.Store
app appIface
}
func (a *appAPI) init(store store.Store, app appIface) {
a.store = store
a.app = app
}
func (a *appAPI) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
return a.store.GetBlockHistory(blockID, opts)
}
func (a *appAPI) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
return a.store.GetBlockHistoryNewestChildren(parentID, opts)
}
func (a *appAPI) GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) {
return a.store.GetBoardAndCardByID(blockID)
}
func (a *appAPI) GetUserByID(userID string) (*model.User, error) {
return a.store.GetUserByID(userID)
}
func (a *appAPI) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
return a.app.CreateSubscription(sub)
}
func (a *appAPI) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) {
return a.store.GetSubscribersForBlock(blockID)
}
func (a *appAPI) UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error {
return a.store.UpdateSubscribersNotifiedAt(blockID, notifyAt)
}
func (a *appAPI) UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) {
return a.store.UpsertNotificationHint(hint, notificationFreq)
}
func (a *appAPI) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) {
return a.store.GetNextNotificationHint(remove)
}
func (a *appAPI) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) {
return a.store.GetMemberForBoard(boardID, userID)
}
func (a *appAPI) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
return a.app.AddMemberToBoard(member)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/ws"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Params struct {
Cfg *config.Configuration
SingleUserToken string
DBStore store.Store
Logger mlog.LoggerIFace
ServerID string
WSAdapter ws.Adapter
NotifyBackends []notify.Backend
PermissionsService permissions.PermissionsService
ServicesAPI model.ServicesAPI
IsPlugin bool
}
func (p Params) CheckValid() error {
if p.Cfg == nil {
return ErrServerParam{name: "Cfg", issue: "cannot be nil"}
}
if p.DBStore == nil {
return ErrServerParam{name: "DbStore", issue: "cannot be nil"}
}
if p.Logger == nil {
return ErrServerParam{name: "Logger", issue: "cannot be nil"}
}
if p.PermissionsService == nil {
return ErrServerParam{name: "Permissions", issue: "cannot be nil"}
}
return nil
}
type ErrServerParam struct {
name string
issue string
}
func (e ErrServerParam) Error() string {
return fmt.Sprintf("invalid server params: %s %s", e.name, e.issue)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"encoding/json"
"fmt"
"net/url"
"strings"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
)
func postWithBoardsEmbed(post *mm_model.Post) *mm_model.Post {
if _, ok := post.GetProps()["boards"]; ok {
post.AddProp("boards", nil)
}
firstLink, newPostMessage := getFirstLinkAndShortenAllBoardsLink(post.Message)
post.Message = newPostMessage
if firstLink == "" {
return post
}
u, err := url.Parse(firstLink)
if err != nil {
return post
}
// Trim away the first / because otherwise after we split the string, the first element in the array is a empty element
urlPath := u.Path
urlPath = strings.TrimPrefix(urlPath, "/")
urlPath = strings.TrimSuffix(urlPath, "/")
pathSplit := strings.Split(strings.ToLower(urlPath), "/")
queryParams := u.Query()
if len(pathSplit) == 0 {
return post
}
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
if teamID != "" && boardID != "" && viewID != "" && cardID != "" {
b, _ := json.Marshal(BoardsEmbed{
TeamID: teamID,
BoardID: boardID,
ViewID: viewID,
CardID: cardID,
ReadToken: queryParams.Get("r"),
OriginalPath: u.RequestURI(),
})
BoardsPostEmbed := &mm_model.PostEmbed{
Type: mm_model.PostEmbedBoards,
Data: string(b),
}
if post.Metadata == nil {
post.Metadata = &mm_model.PostMetadata{}
}
post.Metadata.Embeds = []*mm_model.PostEmbed{BoardsPostEmbed}
post.AddProp("boards", string(b))
}
return post
}
func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPostMessage string) {
newPostMessage = postMessage
seenLinks := make(map[string]bool)
markdown.Inspect(postMessage, func(blockOrInline interface{}) bool {
if autoLink, ok := blockOrInline.(*markdown.Autolink); ok {
link := autoLink.Destination()
if firstLink == "" {
firstLink = link
}
if seen := seenLinks[link]; !seen && isBoardsLink(link) {
// TODO: Make sure that <Jump To Card> is Internationalized and translated to the Users Language preference
markdownFormattedLink := fmt.Sprintf("[%s](%s)", "<Jump To Card>", link)
newPostMessage = strings.ReplaceAll(newPostMessage, link, markdownFormattedLink)
seenLinks[link] = true
}
}
if inlineLink, ok := blockOrInline.(*markdown.InlineLink); ok {
if link := inlineLink.Destination(); firstLink == "" {
firstLink = link
}
}
return true
})
return firstLink, newPostMessage
}
func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) {
// The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths
index := -1
for i := 0; i < len(pathArray); i++ {
if pathArray[i] == "boards" || pathArray[i] == "plugins" {
index = i
break
}
}
if index == -1 {
return teamID, boardID, viewID, cardID
}
// If at index, the parameter in the path is boards,
// then we've copied this directly as logged in user of that board
// If at index, the parameter in the path is plugins,
// then we've copied this from a shared board
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID
// For card links copied on a shared board, the path looks like
// {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token
// This is a non-shared board card link
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" {
teamID = pathArray[index+2]
boardID = pathArray[index+3]
viewID = pathArray[index+4]
cardID = pathArray[index+5]
} else if len(pathArray)-index == 8 && pathArray[index] == "plugins" &&
pathArray[index+1] == "focalboard" &&
pathArray[index+2] == "team" &&
pathArray[index+4] == "shared" { // This is a shared board card link
teamID = pathArray[index+3]
boardID = pathArray[index+5]
viewID = pathArray[index+6]
cardID = pathArray[index+7]
}
return teamID, boardID, viewID, cardID
}
func isBoardsLink(link string) bool {
u, err := url.Parse(link)
if err != nil {
return false
}
urlPath := u.Path
urlPath = strings.TrimPrefix(urlPath, "/")
urlPath = strings.TrimSuffix(urlPath, "/")
pathSplit := strings.Split(strings.ToLower(urlPath), "/")
if len(pathSplit) == 0 {
return false
}
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package server
import (
"database/sql"
"fmt"
"net"
"net/http"
"os"
"runtime"
"sync"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/oklog/run"
"github.com/mattermost/mattermost-server/v6/server/boards/api"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
appModel "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/boards/services/metrics"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify/notifylogger"
"github.com/mattermost/mattermost-server/v6/server/boards/services/scheduler"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/boards/services/telemetry"
"github.com/mattermost/mattermost-server/v6/server/boards/services/webhook"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/boards/web"
"github.com/mattermost/mattermost-server/v6/server/boards/ws"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
cleanupSessionTaskFrequency = 10 * time.Minute
updateMetricsTaskFrequency = 15 * time.Minute
minSessionExpiryTime = int64(60 * 60 * 24 * 31) // 31 days
MattermostAuthMod = "mattermost"
)
type Server struct {
config *config.Configuration
wsAdapter ws.Adapter
webServer *web.Server
store store.Store
filesBackend filestore.FileBackend
telemetry *telemetry.Service
logger mlog.LoggerIFace
cleanUpSessionsTask *scheduler.ScheduledTask
metricsServer *metrics.Service
metricsService *metrics.Metrics
metricsUpdaterTask *scheduler.ScheduledTask
auditService *audit.Audit
notificationService *notify.Service
servicesStartStopMutex sync.Mutex
localRouter *mux.Router
localModeServer *http.Server
api *api.API
app *app.App
}
func New(params Params) (*Server, error) {
if err := params.CheckValid(); err != nil {
return nil, err
}
authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService)
// if no ws adapter is provided, we spin up a websocket server
wsAdapter := params.WSAdapter
if wsAdapter == nil {
wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore)
}
filesBackendSettings := filestore.FileBackendSettings{}
filesBackendSettings.DriverName = params.Cfg.FilesDriver
filesBackendSettings.Directory = params.Cfg.FilesPath
filesBackendSettings.AmazonS3AccessKeyId = params.Cfg.FilesS3Config.AccessKeyID
filesBackendSettings.AmazonS3SecretAccessKey = params.Cfg.FilesS3Config.SecretAccessKey
filesBackendSettings.AmazonS3Bucket = params.Cfg.FilesS3Config.Bucket
filesBackendSettings.AmazonS3PathPrefix = params.Cfg.FilesS3Config.PathPrefix
filesBackendSettings.AmazonS3Region = params.Cfg.FilesS3Config.Region
filesBackendSettings.AmazonS3Endpoint = params.Cfg.FilesS3Config.Endpoint
filesBackendSettings.AmazonS3SSL = params.Cfg.FilesS3Config.SSL
filesBackendSettings.AmazonS3SignV2 = params.Cfg.FilesS3Config.SignV2
filesBackendSettings.AmazonS3SSE = params.Cfg.FilesS3Config.SSE
filesBackendSettings.AmazonS3Trace = params.Cfg.FilesS3Config.Trace
filesBackendSettings.AmazonS3RequestTimeoutMilliseconds = params.Cfg.FilesS3Config.Timeout
filesBackend, appErr := filestore.NewFileBackend(filesBackendSettings)
if appErr != nil {
params.Logger.Error("Unable to initialize the files storage", mlog.Err(appErr))
return nil, errors.New("unable to initialize the files storage")
}
webhookClient := webhook.NewClient(params.Cfg, params.Logger)
// Init metrics
instanceInfo := metrics.InstanceInfo{
Version: appModel.CurrentVersion,
BuildNum: appModel.BuildNumber,
Edition: appModel.Edition,
InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"),
}
metricsService := metrics.NewMetrics(instanceInfo)
// Init audit
auditService, errAudit := audit.NewAudit()
if errAudit != nil {
return nil, fmt.Errorf("unable to create the audit service: %w", errAudit)
}
if err := auditService.Configure(params.Cfg.AuditCfgFile, params.Cfg.AuditCfgJSON); err != nil {
return nil, fmt.Errorf("unable to initialize the audit service: %w", err)
}
// Init notification services
notificationService, errNotify := initNotificationService(params.NotifyBackends, params.Logger)
if errNotify != nil {
return nil, fmt.Errorf("cannot initialize notification service(s): %w", errNotify)
}
appServices := app.Services{
Auth: authenticator,
Store: params.DBStore,
FilesBackend: filesBackend,
Webhook: webhookClient,
Metrics: metricsService,
Notifications: notificationService,
Logger: params.Logger,
Permissions: params.PermissionsService,
ServicesAPI: params.ServicesAPI,
SkipTemplateInit: utils.IsRunningUnitTests(),
}
app := app.New(params.Cfg, wsAdapter, appServices)
focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService, params.IsPlugin)
// Local router for admin APIs
localRouter := mux.NewRouter()
focalboardAPI.RegisterAdminRoutes(localRouter)
// Init team
if _, err := app.GetRootTeam(); err != nil {
params.Logger.Error("Unable to get root team", mlog.Err(err))
return nil, err
}
webServer := web.NewServer(params.Cfg.WebPath, params.Cfg.ServerRoot, params.Cfg.Port,
params.Cfg.UseSSL, params.Cfg.LocalOnly, params.Logger)
// if the adapter is a routed service, register it before the API
if routedService, ok := wsAdapter.(web.RoutedService); ok {
webServer.AddRoutes(routedService)
}
webServer.AddRoutes(focalboardAPI)
settings, err := params.DBStore.GetSystemSettings()
if err != nil {
return nil, err
}
// Init telemetry
telemetryID := settings["TelemetryID"]
if telemetryID == "" {
telemetryID = utils.NewID(utils.IDTypeNone)
if err = params.DBStore.SetSystemSetting("TelemetryID", telemetryID); err != nil {
return nil, err
}
}
telemetryOpts := telemetryOptions{
app: app,
cfg: params.Cfg,
telemetryID: telemetryID,
serverID: params.ServerID,
logger: params.Logger,
singleUser: params.SingleUserToken != "",
}
telemetryService := initTelemetry(telemetryOpts)
server := Server{
config: params.Cfg,
wsAdapter: wsAdapter,
webServer: webServer,
store: params.DBStore,
filesBackend: filesBackend,
telemetry: telemetryService,
metricsServer: metrics.NewMetricsServer(params.Cfg.PrometheusAddress, metricsService, params.Logger),
metricsService: metricsService,
auditService: auditService,
notificationService: notificationService,
logger: params.Logger,
localRouter: localRouter,
api: focalboardAPI,
app: app,
}
server.initHandlers()
return &server, nil
}
func NewStore(config *config.Configuration, isSingleUser bool, logger mlog.LoggerIFace) (store.Store, error) {
sqlDB, err := sql.Open(config.DBType, config.DBConfigString)
if err != nil {
logger.Error("connectDatabase failed", mlog.Err(err))
return nil, err
}
err = sqlDB.Ping()
if err != nil {
logger.Error(`Database Ping failed`, mlog.Err(err))
return nil, err
}
storeParams := sqlstore.Params{
DBType: config.DBType,
ConnectionString: config.DBConfigString,
TablePrefix: config.DBTablePrefix,
Logger: logger,
DB: sqlDB,
IsPlugin: false,
IsSingleUser: isSingleUser,
}
var db store.Store
db, err = sqlstore.New(storeParams)
if err != nil {
return nil, err
}
return db, nil
}
func (s *Server) Start() error {
s.logger.Info("Server.Start")
s.webServer.Start()
s.servicesStartStopMutex.Lock()
defer s.servicesStartStopMutex.Unlock()
if s.config.EnableLocalMode {
if err := s.startLocalModeServer(); err != nil {
return err
}
}
if s.config.AuthMode != MattermostAuthMod {
s.cleanUpSessionsTask = scheduler.CreateRecurringTask("cleanUpSessions", func() {
secondsAgo := minSessionExpiryTime
if secondsAgo < s.config.SessionExpireTime {
secondsAgo = s.config.SessionExpireTime
}
if err := s.store.CleanUpSessions(secondsAgo); err != nil {
s.logger.Error("Unable to clean up the sessions", mlog.Err(err))
}
}, cleanupSessionTaskFrequency)
}
metricsUpdater := func() {
blockCounts, err := s.store.GetBlockCountsByType()
if err != nil {
s.logger.Error("Error updating metrics", mlog.String("group", "blocks"), mlog.Err(err))
return
}
s.logger.Log(mlog.LvlFBMetrics, "Block metrics collected", mlog.Map("block_counts", blockCounts))
for blockType, count := range blockCounts {
s.metricsService.ObserveBlockCount(blockType, count)
}
boardCount, err := s.store.GetBoardCount()
if err != nil {
s.logger.Error("Error updating metrics", mlog.String("group", "boards"), mlog.Err(err))
return
}
s.logger.Log(mlog.LvlFBMetrics, "Board metrics collected", mlog.Int64("board_count", boardCount))
s.metricsService.ObserveBoardCount(boardCount)
teamCount, err := s.store.GetTeamCount()
if err != nil {
s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err))
return
}
s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount))
s.metricsService.ObserveTeamCount(teamCount)
}
// metricsUpdater() Calling this immediately causes integration unit tests to fail.
s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency)
if s.config.Telemetry {
firstRun := utils.GetMillis()
s.telemetry.RunTelemetryJob(firstRun)
}
var group run.Group
if s.config.PrometheusAddress != "" {
group.Add(func() error {
if err := s.metricsServer.Run(); err != nil {
return errors.Wrap(err, "PromServer Run")
}
return nil
}, func(error) {
_ = s.metricsServer.Shutdown()
})
if err := group.Run(); err != nil {
return err
}
}
return nil
}
func (s *Server) Shutdown() error {
if err := s.webServer.Shutdown(); err != nil {
return err
}
s.stopLocalModeServer()
s.servicesStartStopMutex.Lock()
defer s.servicesStartStopMutex.Unlock()
if s.cleanUpSessionsTask != nil {
s.cleanUpSessionsTask.Cancel()
}
if s.metricsUpdaterTask != nil {
s.metricsUpdaterTask.Cancel()
}
if err := s.telemetry.Shutdown(); err != nil {
s.logger.Warn("Error occurred when shutting down telemetry", mlog.Err(err))
}
if err := s.auditService.Shutdown(); err != nil {
s.logger.Warn("Error occurred when shutting down audit service", mlog.Err(err))
}
if err := s.notificationService.Shutdown(); err != nil {
s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err))
}
s.app.Shutdown()
defer s.logger.Info("Server.Shutdown")
return s.store.Shutdown()
}
func (s *Server) Config() *config.Configuration {
return s.config
}
func (s *Server) Logger() mlog.LoggerIFace {
return s.logger
}
func (s *Server) App() *app.App {
return s.app
}
func (s *Server) Store() store.Store {
return s.store
}
func (s *Server) UpdateAppConfig() {
s.app.SetConfig(s.config)
}
// Local server
func (s *Server) startLocalModeServer() error {
s.localModeServer = &http.Server{ //nolint:gosec
Handler: s.localRouter,
ConnContext: api.SetContextConn,
}
// TODO: Close and delete socket file on shutdown
// Delete existing socket if it exists
if _, err := os.Stat(s.config.LocalModeSocketLocation); err == nil {
if err := syscall.Unlink(s.config.LocalModeSocketLocation); err != nil {
s.logger.Error("Unable to unlink socket.", mlog.Err(err))
}
}
socket := s.config.LocalModeSocketLocation
unixListener, err := net.Listen("unix", socket)
if err != nil {
return err
}
if err = os.Chmod(socket, 0600); err != nil {
return err
}
go func() {
s.logger.Info("Starting unix socket server")
err = s.localModeServer.Serve(unixListener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("Error starting unix socket server", mlog.Err(err))
}
}()
return nil
}
func (s *Server) stopLocalModeServer() {
if s.localModeServer != nil {
_ = s.localModeServer.Close()
s.localModeServer = nil
}
}
func (s *Server) GetRootRouter() *mux.Router {
return s.webServer.Router()
}
type telemetryOptions struct {
app *app.App
cfg *config.Configuration
telemetryID string
serverID string
logger mlog.LoggerIFace
singleUser bool
}
func initTelemetry(opts telemetryOptions) *telemetry.Service {
telemetryService := telemetry.New(opts.telemetryID, opts.logger)
telemetryService.RegisterTracker("server", func() (telemetry.Tracker, error) {
return map[string]interface{}{
"version": appModel.CurrentVersion,
"build_number": appModel.BuildNumber,
"build_hash": appModel.BuildHash,
"edition": appModel.Edition,
"operating_system": runtime.GOOS,
"server_id": opts.serverID,
}, nil
})
telemetryService.RegisterTracker("config", func() (telemetry.Tracker, error) {
return map[string]interface{}{
"serverRoot": opts.cfg.ServerRoot == config.DefaultServerRoot,
"port": opts.cfg.Port == config.DefaultPort,
"useSSL": opts.cfg.UseSSL,
"dbType": opts.cfg.DBType,
"single_user": opts.singleUser,
"allow_public_shared_boards": opts.cfg.EnablePublicSharedBoards,
}, nil
})
telemetryService.RegisterTracker("activity", func() (telemetry.Tracker, error) {
m := make(map[string]interface{})
var count int
var err error
if count, err = opts.app.GetRegisteredUserCount(); err != nil {
return nil, err
}
m["registered_users"] = count
if count, err = opts.app.GetDailyActiveUsers(); err != nil {
return nil, err
}
m["daily_active_users"] = count
if count, err = opts.app.GetWeeklyActiveUsers(); err != nil {
return nil, err
}
m["weekly_active_users"] = count
if count, err = opts.app.GetMonthlyActiveUsers(); err != nil {
return nil, err
}
m["monthly_active_users"] = count
return m, nil
})
telemetryService.RegisterTracker("blocks", func() (telemetry.Tracker, error) {
blockCounts, err := opts.app.GetBlockCountsByType()
if err != nil {
return nil, err
}
m := make(map[string]interface{})
for k, v := range blockCounts {
m[k] = v
}
return m, nil
})
telemetryService.RegisterTracker("boards", func() (telemetry.Tracker, error) {
boardCount, err := opts.app.GetBoardCount()
if err != nil {
return nil, err
}
m := map[string]interface{}{
"boards": boardCount,
}
return m, nil
})
telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) {
count, err := opts.app.GetTeamCount()
if err != nil {
return nil, err
}
m := map[string]interface{}{
"teams": count,
}
return m, nil
})
return telemetryService
}
func initNotificationService(backends []notify.Backend, logger mlog.LoggerIFace) (*notify.Service, error) {
loggerBackend := notifylogger.New(logger, mlog.LvlDebug)
backends = append(backends, loggerBackend)
service, err := notify.New(logger, backends...)
return service, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DefMaxQueueSize = 1000
KeyAPIPath = "api_path"
KeyEvent = "event"
KeyStatus = "status"
KeyUserID = "user_id"
KeySessionID = "session_id"
KeyClient = "client"
KeyIPAddress = "ip_address"
KeyClusterID = "cluster_id"
KeyTeamID = "team_id"
Success = "success"
Attempt = "attempt"
Fail = "fail"
)
var (
LevelAuth = mlog.Level{ID: 1000, Name: "auth"}
LevelModify = mlog.Level{ID: 1001, Name: "mod"}
LevelRead = mlog.Level{ID: 1002, Name: "read"}
)
// Audit provides auditing service.
type Audit struct {
auditLogger *mlog.Logger
}
// NewAudit creates a new Audit instance which can be configured via `(*Audit).Configure`.
func NewAudit(options ...mlog.Option) (*Audit, error) {
logger, err := mlog.NewLogger(options...)
if err != nil {
return nil, err
}
return &Audit{
auditLogger: logger,
}, nil
}
// Configure provides a new configuration for this audit service.
// Zero or more sources of config can be provided:
//
// cfgFile - path to file containing JSON
// cfgEscaped - JSON string probably from ENV var
//
// For each case JSON containing log targets is provided. Target name collisions are resolved
// using the following precedence:
//
// cfgFile > cfgEscaped
func (a *Audit) Configure(cfgFile string, cfgEscaped string) error {
return a.auditLogger.Configure(cfgFile, cfgEscaped, nil)
}
// Shutdown shuts down the audit service after making best efforts to flush any
// remaining records.
func (a *Audit) Shutdown() error {
return a.auditLogger.Shutdown()
}
// LogRecord emits an audit record with complete info.
func (a *Audit) LogRecord(level mlog.Level, rec *Record) {
fields := make([]mlog.Field, 0, 7+len(rec.Meta))
fields = append(fields, mlog.String(KeyAPIPath, rec.APIPath))
fields = append(fields, mlog.String(KeyEvent, rec.Event))
fields = append(fields, mlog.String(KeyStatus, rec.Status))
fields = append(fields, mlog.String(KeyUserID, rec.UserID))
fields = append(fields, mlog.String(KeySessionID, rec.SessionID))
fields = append(fields, mlog.String(KeyClient, rec.Client))
fields = append(fields, mlog.String(KeyIPAddress, rec.IPAddress))
for _, meta := range rec.Meta {
fields = append(fields, mlog.Any(meta.K, meta.V))
}
a.auditLogger.Log(level, "audit "+rec.Event, fields...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
import "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
// Meta represents metadata that can be added to a audit record as name/value pairs.
type Meta struct {
K string
V interface{}
}
// FuncMetaTypeConv defines a function that can convert meta data types into something
// that serializes well for audit records.
type FuncMetaTypeConv func(val interface{}) (newVal interface{}, converted bool)
// Record provides a consistent set of fields used for all audit logging.
type Record struct {
APIPath string
Event string
Status string
UserID string
SessionID string
Client string
IPAddress string
Meta []Meta
metaConv []FuncMetaTypeConv
}
// Success marks the audit record status as successful.
func (rec *Record) Success() {
rec.Status = Success
}
// Success marks the audit record status as failed.
func (rec *Record) Fail() {
rec.Status = Fail
}
// AddMeta adds a single name/value pair to this audit record's metadata.
func (rec *Record) AddMeta(name string, val interface{}) {
if rec.Meta == nil {
rec.Meta = []Meta{}
}
// possibly convert val to something better suited for serializing
// via zero or more conversion functions.
for _, conv := range rec.metaConv {
converted, wasConverted := conv(val)
if wasConverted {
val = converted
break
}
}
lc, ok := val.(mlog.LogCloner)
if ok {
val = lc.LogClone()
}
rec.Meta = append(rec.Meta, Meta{K: name, V: val})
}
// AddMetaTypeConverter adds a function capable of converting meta field types
// into something more suitable for serialization.
func (rec *Record) AddMetaTypeConverter(f FuncMetaTypeConv) {
rec.metaConv = append(rec.metaConv, f)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package auth
import "regexp"
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
// IsEmailValid checks if the email provided passes the required structure and length.
func IsEmailValid(e string) bool {
if len(e) < 3 || len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package auth
import (
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
)
const (
PasswordMaximumLength = 64
PasswordSpecialChars = "!\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" //nolint:gosec
PasswordNumbers = "0123456789"
PasswordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
PasswordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz"
PasswordAllChars = PasswordSpecialChars + PasswordNumbers + PasswordUpperCaseLetters + PasswordLowerCaseLetters
InvalidLowercasePassword = "lowercase"
InvalidMinLengthPassword = "min-length"
InvalidMaxLengthPassword = "max-length"
InvalidNumberPassword = "number"
InvalidUppercasePassword = "uppercase"
InvalidSymbolPassword = "symbol"
)
var PasswordHashStrength = 10
// HashPassword generates a hash using the bcrypt.GenerateFromPassword.
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), PasswordHashStrength)
if err != nil {
panic(err)
}
return string(hash)
}
// ComparePassword compares the hash.
func ComparePassword(hash, password string) bool {
if password == "" || hash == "" {
return false
}
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
type InvalidPasswordError struct {
FailingCriterias []string
}
func (ipe *InvalidPasswordError) Error() string {
return fmt.Sprintf("invalid password, failing criteria: %s", strings.Join(ipe.FailingCriterias, ", "))
}
type PasswordSettings struct {
MinimumLength int
Lowercase bool
Number bool
Uppercase bool
Symbol bool
}
func IsPasswordValid(password string, settings PasswordSettings) error {
err := &InvalidPasswordError{
FailingCriterias: []string{},
}
if len(password) < settings.MinimumLength {
err.FailingCriterias = append(err.FailingCriterias, InvalidMinLengthPassword)
}
if len(password) > PasswordMaximumLength {
err.FailingCriterias = append(err.FailingCriterias, InvalidMaxLengthPassword)
}
if settings.Lowercase {
if !strings.ContainsAny(password, PasswordLowerCaseLetters) {
err.FailingCriterias = append(err.FailingCriterias, InvalidLowercasePassword)
}
}
if settings.Uppercase {
if !strings.ContainsAny(password, PasswordUpperCaseLetters) {
err.FailingCriterias = append(err.FailingCriterias, InvalidUppercasePassword)
}
}
if settings.Number {
if !strings.ContainsAny(password, PasswordNumbers) {
err.FailingCriterias = append(err.FailingCriterias, InvalidNumberPassword)
}
}
if settings.Symbol {
if !strings.ContainsAny(password, PasswordSpecialChars) {
err.FailingCriterias = append(err.FailingCriterias, InvalidSymbolPassword)
}
}
if len(err.FailingCriterias) > 0 {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package auth
import (
"net/http"
"strings"
)
const (
HeaderToken = "token"
HeaderAuth = "Authorization"
HeaderBearer = "BEARER"
SessionCookieToken = "FOCALBOARDAUTHTOKEN"
)
type TokenLocation int
const (
TokenLocationNotFound TokenLocation = iota
TokenLocationHeader
TokenLocationCookie
TokenLocationQueryString
)
func (tl TokenLocation) String() string {
switch tl {
case TokenLocationNotFound:
return "Not Found"
case TokenLocationHeader:
return "Header"
case TokenLocationCookie:
return "Cookie"
case TokenLocationQueryString:
return "QueryString"
default:
return "Unknown"
}
}
func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) {
authHeader := r.Header.Get(HeaderAuth)
// Attempt to parse the token from the cookie
if cookie, err := r.Cookie(SessionCookieToken); err == nil {
return cookie.Value, TokenLocationCookie
}
// Parse the token from the header
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == HeaderBearer {
// Default session token
return authHeader[7:], TokenLocationHeader
}
if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == HeaderToken {
// OAuth token
return authHeader[6:], TokenLocationHeader
}
// Attempt to parse token out of the query string
if token := r.URL.Query().Get("access_token"); token != "" {
return token, TokenLocationQueryString
}
return "", TokenLocationNotFound
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"log"
"github.com/spf13/viper"
)
const (
DefaultServerRoot = "http://localhost:8000"
DefaultPort = 8000
)
type AmazonS3Config struct {
AccessKeyID string
SecretAccessKey string
Bucket string
PathPrefix string
Region string
Endpoint string
SSL bool
SignV2 bool
SSE bool
Trace bool
Timeout int64
}
// Configuration is the app configuration stored in a json file.
type Configuration struct {
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
Port int `json:"port" mapstructure:"port"`
DBType string `json:"dbtype" mapstructure:"dbtype"`
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
WebPath string `json:"webpath" mapstructure:"webpath"`
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
FilesPath string `json:"filespath" mapstructure:"filespath"`
MaxFileSize int64 `json:"maxfilesize" mapstructure:"maxfilesize"`
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"`
PrometheusAddress string `json:"prometheusaddress" mapstructure:"prometheusaddress"`
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
Secret string `json:"secret" mapstructure:"secret"`
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards" mapstructure:"enablePublicSharedBoards"`
FeatureFlags map[string]string `json:"featureFlags" mapstructure:"featureFlags"`
EnableDataRetention bool `json:"enable_data_retention" mapstructure:"enable_data_retention"`
DataRetentionDays int `json:"data_retention_days" mapstructure:"data_retention_days"`
TeammateNameDisplay string `json:"teammate_name_display" mapstructure:"teammateNameDisplay"`
ShowEmailAddress bool `json:"show_email_address" mapstructure:"showEmailAddress"`
ShowFullName bool `json:"show_full_name" mapstructure:"showFullName"`
AuthMode string `json:"authMode" mapstructure:"authMode"`
LoggingCfgFile string `json:"logging_cfg_file" mapstructure:"logging_cfg_file"`
LoggingCfgJSON string `json:"logging_cfg_json" mapstructure:"logging_cfg_json"`
AuditCfgFile string `json:"audit_cfg_file" mapstructure:"audit_cfg_file"`
AuditCfgJSON string `json:"audit_cfg_json" mapstructure:"audit_cfg_json"`
NotifyFreqCardSeconds int `json:"notify_freq_card_seconds" mapstructure:"notify_freq_card_seconds"`
NotifyFreqBoardSeconds int `json:"notify_freq_board_seconds" mapstructure:"notify_freq_board_seconds"`
}
// ReadConfigFile read the configuration from the filesystem.
func ReadConfigFile(configFilePath string) (*Configuration, error) {
if configFilePath == "" {
viper.SetConfigFile("./config.json")
} else {
viper.SetConfigFile(configFilePath)
}
viper.SetEnvPrefix("focalboard")
viper.AutomaticEnv() // read config values from env like FOCALBOARD_SERVERROOT=...
viper.SetDefault("ServerRoot", DefaultServerRoot)
viper.SetDefault("Port", DefaultPort)
viper.SetDefault("DBType", "postgres")
viper.SetDefault("DBConfigString", "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes")
viper.SetDefault("DBTablePrefix", "")
viper.SetDefault("SecureCookie", false)
viper.SetDefault("WebPath", "./pack")
viper.SetDefault("FilesPath", "./files")
viper.SetDefault("FilesDriver", "local")
viper.SetDefault("Telemetry", true)
viper.SetDefault("TelemetryID", "")
viper.SetDefault("WebhookUpdate", nil)
viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime
viper.SetDefault("SessionRefreshTime", 60*60*5) // 5 minutes session refresh
viper.SetDefault("LocalOnly", false)
viper.SetDefault("EnableLocalMode", false)
viper.SetDefault("LocalModeSocketLocation", "/var/tmp/focalboard_local.socket")
viper.SetDefault("EnablePublicSharedBoards", false)
viper.SetDefault("FeatureFlags", map[string]string{})
viper.SetDefault("AuthMode", "native")
viper.SetDefault("NotifyFreqCardSeconds", 120) // 2 minutes after last card edit
viper.SetDefault("NotifyFreqBoardSeconds", 86400) // 1 day after last card edit
viper.SetDefault("EnableDataRetention", false)
viper.SetDefault("DataRetentionDays", 365) // 1 year is default
viper.SetDefault("PrometheusAddress", "")
viper.SetDefault("TeammateNameDisplay", "username")
viper.SetDefault("ShowEmailAddress", false)
viper.SetDefault("ShowFullName", false)
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
return nil, err
}
configuration := Configuration{}
err = viper.Unmarshal(&configuration)
if err != nil {
return nil, err
}
log.Println("readConfigFile")
log.Printf("%+v", removeSecurityData(configuration))
return &configuration, nil
}
func removeSecurityData(config Configuration) Configuration {
clean := config
return clean
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package metrics
import (
"os"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
const (
MetricsNamespace = "focalboard"
MetricsSubsystemBlocks = "blocks"
MetricsSubsystemBoards = "boards"
MetricsSubsystemTeams = "teams"
MetricsSubsystemSystem = "system"
MetricsCloudInstallationLabel = "installationId"
)
type InstanceInfo struct {
Version string
BuildNum string
Edition string
InstallationID string
}
// Metrics used to instrumentate metrics in prometheus.
type Metrics struct {
registry *prometheus.Registry
instance *prometheus.GaugeVec
startTime prometheus.Gauge
loginCount prometheus.Counter
logoutCount prometheus.Counter
loginFailCount prometheus.Counter
blocksInsertedCount prometheus.Counter
blocksPatchedCount prometheus.Counter
blocksDeletedCount prometheus.Counter
blockCount *prometheus.GaugeVec
boardCount prometheus.Gauge
teamCount prometheus.Gauge
blockLastActivity prometheus.Gauge
}
// NewMetrics Factory method to create a new metrics collector.
func NewMetrics(info InstanceInfo) *Metrics {
m := &Metrics{}
m.registry = prometheus.NewRegistry()
options := collectors.ProcessCollectorOpts{
Namespace: MetricsNamespace,
}
m.registry.MustRegister(collectors.NewProcessCollector(options))
m.registry.MustRegister(collectors.NewGoCollector())
additionalLabels := map[string]string{}
if info.InstallationID != "" {
additionalLabels[MetricsCloudInstallationLabel] = os.Getenv("MM_CLOUD_INSTALLATION_ID")
}
m.loginCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "login_total",
Help: "Total number of logins.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.loginCount)
m.logoutCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "logout_total",
Help: "Total number of logouts.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.logoutCount)
m.loginFailCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "login_fail_total",
Help: "Total number of failed logins.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.loginFailCount)
m.instance = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "focalboard_instance_info",
Help: "Instance information for Focalboard.",
ConstLabels: additionalLabels,
}, []string{"Version", "BuildNum", "Edition"})
m.registry.MustRegister(m.instance)
m.instance.WithLabelValues(info.Version, info.BuildNum, info.Edition).Set(1)
m.startTime = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "server_start_time",
Help: "The time the server started.",
ConstLabels: additionalLabels,
})
m.startTime.SetToCurrentTime()
m.registry.MustRegister(m.startTime)
m.blocksInsertedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBlocks,
Name: "blocks_inserted_total",
Help: "Total number of blocks inserted.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.blocksInsertedCount)
m.blocksPatchedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBlocks,
Name: "blocks_patched_total",
Help: "Total number of blocks patched.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.blocksPatchedCount)
m.blocksDeletedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBlocks,
Name: "blocks_deleted_total",
Help: "Total number of blocks deleted.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.blocksDeletedCount)
m.blockCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBlocks,
Name: "blocks_total",
Help: "Total number of blocks.",
ConstLabels: additionalLabels,
}, []string{"BlockType"})
m.registry.MustRegister(m.blockCount)
m.boardCount = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBoards,
Name: "boards_total",
Help: "Total number of boards.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.boardCount)
m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemTeams,
Name: "teams_total",
Help: "Total number of teams.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.teamCount)
m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemBlocks,
Name: "blocks_last_activity",
Help: "Time of last block insert, update, delete.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.blockLastActivity)
return m
}
func (m *Metrics) IncrementLoginCount(num int) {
if m != nil {
m.loginCount.Add(float64(num))
}
}
func (m *Metrics) IncrementLogoutCount(num int) {
if m != nil {
m.logoutCount.Add(float64(num))
}
}
func (m *Metrics) IncrementLoginFailCount(num int) {
if m != nil {
m.loginFailCount.Add(float64(num))
}
}
func (m *Metrics) IncrementBlocksInserted(num int) {
if m != nil {
m.blocksInsertedCount.Add(float64(num))
m.blockLastActivity.SetToCurrentTime()
}
}
func (m *Metrics) IncrementBlocksPatched(num int) {
if m != nil {
m.blocksPatchedCount.Add(float64(num))
m.blockLastActivity.SetToCurrentTime()
}
}
func (m *Metrics) IncrementBlocksDeleted(num int) {
if m != nil {
m.blocksDeletedCount.Add(float64(num))
m.blockLastActivity.SetToCurrentTime()
}
}
func (m *Metrics) ObserveBlockCount(blockType string, count int64) {
if m != nil {
m.blockCount.WithLabelValues(blockType).Set(float64(count))
}
}
func (m *Metrics) ObserveBoardCount(count int64) {
if m != nil {
m.boardCount.Set(float64(count))
}
}
func (m *Metrics) ObserveTeamCount(count int64) {
if m != nil {
m.teamCount.Set(float64(count))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package metrics
import (
"net/http"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Service prometheus to run the server.
type Service struct {
*http.Server
}
// NewMetricsServer factory method to create a new prometheus server.
func NewMetricsServer(address string, metricsService *Metrics, logger mlog.LoggerIFace) *Service {
return &Service{
&http.Server{ //nolint:gosec
Addr: address,
Handler: promhttp.HandlerFor(metricsService.registry, promhttp.HandlerOpts{
ErrorLog: logger.StdLogger(mlog.LvlError),
}),
},
}
}
// Run will start the prometheus server.
func (h *Service) Run() error {
return errors.Wrap(h.Server.ListenAndServe(), "prometheus ListenAndServe")
}
// Shutdown will shutdown the prometheus server.
func (h *Service) Shutdown() error {
return errors.Wrap(h.Server.Close(), "prometheus Close")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifylogger
import (
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
backendName = "notifyLogger"
)
type Backend struct {
logger mlog.LoggerIFace
level mlog.Level
}
func New(logger mlog.LoggerIFace, level mlog.Level) *Backend {
return &Backend{
logger: logger,
level: level,
}
}
func (b *Backend) Start() error {
return nil
}
func (b *Backend) ShutDown() error {
_ = b.logger.Flush()
return nil
}
func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
var board string
var card string
if evt.Board != nil {
board = evt.Board.Title
}
if evt.Card != nil {
card = evt.Card.Title
}
b.logger.Log(b.level, "Block change event",
mlog.String("action", string(evt.Action)),
mlog.String("board", board),
mlog.String("card", card),
mlog.String("block_id", evt.BlockChanged.ID),
)
return nil
}
func (b *Backend) Name() string {
return backendName
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifymentions
import "strings"
const (
defPrefixLines = 2
defPrefixMaxChars = 100
defSuffixLines = 2
defSuffixMaxChars = 100
)
type limits struct {
prefixLines int
prefixMaxChars int
suffixLines int
suffixMaxChars int
}
func newLimits() limits {
return limits{
prefixLines: defPrefixLines,
prefixMaxChars: defPrefixMaxChars,
suffixLines: defSuffixLines,
suffixMaxChars: defSuffixMaxChars,
}
}
// extractText returns all or a subset of the input string, such that
// no more than `prefixLines` lines preceding the mention and `suffixLines`
// lines after the mention are returned, and no more than approx
// prefixMaxChars+suffixMaxChars are returned.
func extractText(s string, mention string, limits limits) string {
if !strings.HasPrefix(mention, "@") {
mention = "@" + mention
}
lines := strings.Split(s, "\n")
// find first line with mention
found := -1
for i, l := range lines {
if strings.Contains(l, mention) {
found = i
break
}
}
if found == -1 {
return ""
}
prefix := safeConcat(lines, found-limits.prefixLines, found)
suffix := safeConcat(lines, found+1, found+limits.suffixLines+1)
combined := strings.TrimSpace(strings.Join([]string{prefix, lines[found], suffix}, "\n"))
// find mention position within
pos := strings.Index(combined, mention)
pos = max(pos, 0)
return safeSubstr(combined, pos-limits.prefixMaxChars, pos+limits.suffixMaxChars)
}
func safeConcat(lines []string, start int, end int) string {
count := len(lines)
start = min(max(start, 0), count)
end = min(max(end, start), count)
var sb strings.Builder
for i := start; i < end; i++ {
if lines[i] != "" {
sb.WriteString(lines[i])
sb.WriteByte('\n')
}
}
return strings.TrimSpace(sb.String())
}
func safeSubstr(s string, start int, end int) string {
count := len(s)
start = min(max(start, 0), count)
end = min(max(end, start), count)
return s[start:end]
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifymentions
import (
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
// extractMentions extracts any mentions in the specified block and returns
// a slice of usernames.
func extractMentions(block *model.Block) map[string]struct{} {
mentions := make(map[string]struct{})
if block == nil || !strings.Contains(block.Title, "@") {
return mentions
}
str := block.Title
for _, match := range atMentionRegexp.FindAllString(str, -1) {
name := mm_model.NormalizeUsername(match[1:])
if mm_model.IsValidUsernameAllowRemote(name) {
mentions[name] = struct{}{}
}
}
return mentions
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifymentions
import (
"errors"
"fmt"
"sync"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
backendName = "notifyMentions"
)
var (
ErrMentionPermission = errors.New("mention not permitted")
)
type MentionListener interface {
OnMention(userID string, evt notify.BlockChangeEvent)
}
type BackendParams struct {
AppAPI AppAPI
Permissions permissions.PermissionsService
Delivery MentionDelivery
Logger mlog.LoggerIFace
}
// Backend provides the notification backend for @mentions.
type Backend struct {
appAPI AppAPI
permissions permissions.PermissionsService
delivery MentionDelivery
logger mlog.LoggerIFace
mux sync.RWMutex
listeners []MentionListener
}
func New(params BackendParams) *Backend {
return &Backend{
appAPI: params.AppAPI,
permissions: params.Permissions,
delivery: params.Delivery,
logger: params.Logger,
}
}
func (b *Backend) Start() error {
return nil
}
func (b *Backend) ShutDown() error {
_ = b.logger.Flush()
return nil
}
func (b *Backend) Name() string {
return backendName
}
func (b *Backend) AddListener(l MentionListener) {
b.mux.Lock()
defer b.mux.Unlock()
b.listeners = append(b.listeners, l)
b.logger.Debug("Mention listener added.", mlog.Int("listener_count", len(b.listeners)))
}
func (b *Backend) RemoveListener(l MentionListener) {
b.mux.Lock()
defer b.mux.Unlock()
list := make([]MentionListener, 0, len(b.listeners))
for _, listener := range b.listeners {
if listener != l {
list = append(list, listener)
}
}
b.listeners = list
b.logger.Debug("Mention listener removed.", mlog.Int("listener_count", len(b.listeners)))
}
func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
if evt.Board == nil || evt.Card == nil {
return nil
}
if evt.Action == notify.Delete {
return nil
}
switch evt.BlockChanged.Type {
case model.TypeText, model.TypeComment, model.TypeImage:
default:
return nil
}
mentions := extractMentions(evt.BlockChanged)
if len(mentions) == 0 {
return nil
}
oldMentions := extractMentions(evt.BlockOld)
merr := merror.New()
b.mux.RLock()
listeners := make([]MentionListener, len(b.listeners))
copy(listeners, b.listeners)
b.mux.RUnlock()
for username := range mentions {
if _, exists := oldMentions[username]; exists {
// the mention already existed; no need to notify again
continue
}
extract := extractText(evt.BlockChanged.Title, username, newLimits())
userID, err := b.deliverMentionNotification(username, extract, evt)
if err != nil {
if errors.Is(err, ErrMentionPermission) {
b.logger.Debug("Cannot deliver notification", mlog.String("user", username), mlog.Err(err))
} else {
merr.Append(fmt.Errorf("cannot deliver notification for @%s: %w", username, err))
}
}
if userID == "" {
// was a `@` followed by something other than a username.
continue
}
b.logger.Debug("Mention notification delivered",
mlog.String("user", username),
mlog.Int("listener_count", len(listeners)),
)
for _, listener := range listeners {
safeCallListener(listener, userID, evt, b.logger)
}
}
return merr.ErrorOrNil()
}
func safeCallListener(listener MentionListener, userID string, evt notify.BlockChangeEvent, logger mlog.LoggerIFace) {
// don't let panicky listeners stop notifications
defer func() {
if r := recover(); r != nil {
logger.Error("panic calling @mention notification listener", mlog.Any("err", r))
}
}()
listener.OnMention(userID, evt)
}
func (b *Backend) deliverMentionNotification(username string, extract string, evt notify.BlockChangeEvent) (string, error) {
mentionedUser, err := b.delivery.UserByUsername(username)
if err != nil {
if model.IsErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
return "", nil
}
return "", fmt.Errorf("cannot lookup mentioned user: %w", err)
}
if evt.ModifiedBy == nil {
return "", fmt.Errorf("invalid user cannot mention: %w", ErrMentionPermission)
}
if evt.Board.Type == model.BoardTypeOpen {
// public board rules:
// - admin, editor, commenter: can mention anyone on team (mentioned users are automatically added to board)
// - guest: can mention board members
switch {
case evt.ModifiedBy.SchemeAdmin, evt.ModifiedBy.SchemeEditor, evt.ModifiedBy.SchemeCommenter:
if !b.permissions.HasPermissionToTeam(mentionedUser.Id, evt.TeamID, model.PermissionViewTeam) {
return "", fmt.Errorf("%s cannot mention non-team member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
// add mentioned user to board (if not already a member)
member, err := b.appAPI.GetMemberForBoard(evt.Board.ID, mentionedUser.Id)
if member == nil || model.IsErrNotFound(err) {
// create memberships based on minimum board role
newBoardMember := &model.BoardMember{
UserID: mentionedUser.Id,
BoardID: evt.Board.ID,
SchemeViewer: evt.Board.MinimumRole == model.BoardRoleViewer ||
evt.Board.MinimumRole == model.BoardRoleCommenter ||
evt.Board.MinimumRole == model.BoardRoleEditor,
SchemeCommenter: evt.Board.MinimumRole == model.BoardRoleCommenter ||
evt.Board.MinimumRole == model.BoardRoleEditor,
SchemeEditor: evt.Board.MinimumRole == model.BoardRoleEditor,
}
if _, err = b.appAPI.AddMemberToBoard(newBoardMember); err != nil {
return "", fmt.Errorf("cannot add mentioned user %s to board %s: %w", mentionedUser.Id, evt.Board.ID, err)
}
b.logger.Debug("auto-added mentioned user to board",
mlog.String("user_id", mentionedUser.Id),
mlog.String("board_id", evt.Board.ID),
mlog.String("board_type", string(evt.Board.Type)),
)
} else {
b.logger.Debug("skipping auto-add mentioned user to board; already a member",
mlog.String("user_id", mentionedUser.Id),
mlog.String("board_id", evt.Board.ID),
mlog.String("board_type", string(evt.Board.Type)),
)
}
case evt.ModifiedBy.SchemeViewer:
// viewer should not have gotten this far since they cannot add text to a card
return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
default:
// this is a guest
if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) {
return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
}
} else {
// private board rules:
// - admin, editor, commenter, guest: can mention board members
switch {
case evt.ModifiedBy.SchemeViewer:
// viewer should not have gotten this far since they cannot add text to a card
return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
default:
// everyone else can mention board members
if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) {
return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission)
}
}
}
return b.delivery.MentionDeliver(mentionedUser, extract, evt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"fmt"
"sort"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Diff represents a difference between two versions of a block.
type Diff struct {
Board *model.Board
Card *model.Block
Authors StringMap
BlockType model.BlockType
OldBlock *model.Block
NewBlock *model.Block
UpdateAt int64 // the UpdateAt of the latest version of the block
schemaDiffs []SchemaDiff
PropDiffs []PropDiff
Diffs []*Diff // Diffs for child blocks
}
type PropDiff struct {
ID string // property id
Index int
Name string
OldValue string
NewValue string
}
type SchemaDiff struct {
Board *model.Board
OldPropDef *model.PropDef
NewPropDef *model.PropDef
}
type diffGenerator struct {
board *model.Board
card *model.Block
store AppAPI
hint *model.NotificationHint
lastNotifyAt int64
logger mlog.LoggerIFace
}
func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
// use block_history to fetch blocks in case they were deleted and no longer exist in blocks table.
opts := model.QueryBlockHistoryOptions{
Limit: 1,
Descending: true,
}
blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts)
if err != nil {
return nil, fmt.Errorf("could not get block for notification: %w", err)
}
if len(blocks) == 0 {
return nil, fmt.Errorf("block not found for notification: %w", err)
}
block := blocks[0]
if dg.board == nil || dg.card == nil {
return nil, fmt.Errorf("cannot generate diff for block %s; must have a valid board and card: %w", dg.hint.BlockID, err)
}
// parse board's property schema here so it only happens once.
schema, err := model.ParsePropertySchema(dg.board)
if err != nil {
return nil, fmt.Errorf("could not parse property schema for board %s: %w", dg.board.ID, err)
}
switch block.Type {
case model.TypeBoard:
dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID))
// TODO: Fix this
// return dg.generateDiffsForBoard(block, schema)
return nil, nil
case model.TypeCard:
diff, err := dg.generateDiffsForCard(block, schema)
if err != nil || diff == nil {
return nil, err
}
return []*Diff{diff}, nil
default:
diff, err := dg.generateDiffForBlock(block, schema)
if err != nil || diff == nil {
return nil, err
}
return []*Diff{diff}, nil
}
}
// TODO: fix this
/*
func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) {
opts := model.QuerySubtreeOptions{
AfterUpdateAt: dg.lastNotifyAt,
}
find all child blocks of the board that updated since last notify.
blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err)
}
var diffs []*Diff
generate diff for board title change or description
boardDiff, err := dg.generateDiffForBlock(board, schema)
if err != nil {
return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err)
}
if boardDiff != nil {
TODO: phase 2 feature (generate schema diffs and add to board diff) goes here.
diffs = append(diffs, boardDiff)
}
for _, b := range blocks {
block := b
if block.Type == model.TypeCard {
cardDiffs, err := dg.generateDiffsForCard(&block, schema)
if err != nil {
return nil, err
}
diffs = append(diffs, cardDiffs)
}
}
return diffs, nil
}
*/
func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) {
// generate diff for card title change and properties.
cardDiff, err := dg.generateDiffForBlock(card, schema)
if err != nil {
return nil, fmt.Errorf("could not generate diff for card %s: %w", card.ID, err)
}
// fetch all card content blocks that were updated after last notify
opts := model.QueryBlockHistoryChildOptions{
AfterUpdateAt: dg.lastNotifyAt,
}
blocks, _, err := dg.store.GetBlockHistoryNewestChildren(card.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err)
}
authors := make(StringMap)
// walk child blocks
var childDiffs []*Diff
for i := range blocks {
if blocks[i].ID == card.ID {
continue
}
blockDiff, err := dg.generateDiffForBlock(blocks[i], schema)
if err != nil {
return nil, fmt.Errorf("could not generate diff for block %s: %w", blocks[i].ID, err)
}
if blockDiff != nil {
childDiffs = append(childDiffs, blockDiff)
authors.Append(blockDiff.Authors)
}
}
dg.logger.Debug("generateDiffsForCard",
mlog.Bool("has_top_changes", cardDiff != nil),
mlog.Int("subtree", len(blocks)),
mlog.Array("author_names", authors.Values()),
mlog.Int("child_diffs", len(childDiffs)),
)
if len(childDiffs) != 0 {
if cardDiff == nil { // will be nil if the card has no other changes besides child diffs
cardDiff = &Diff{
Board: dg.board,
Card: card,
Authors: make(StringMap),
BlockType: card.Type,
OldBlock: card,
NewBlock: card,
UpdateAt: card.UpdateAt,
PropDiffs: nil,
schemaDiffs: nil,
}
}
cardDiff.Diffs = childDiffs
}
cardDiff.Authors.Append(authors)
return cardDiff, nil
}
func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema model.PropSchema) (*Diff, error) {
dg.logger.Debug("generateDiffForBlock - new block",
mlog.String("block_id", newBlock.ID),
mlog.String("block_type", string(newBlock.Type)),
mlog.String("modified_by", newBlock.ModifiedBy),
mlog.Int64("update_at", newBlock.UpdateAt),
)
// find the version of the block as it was at the time of last notify.
opts := model.QueryBlockHistoryOptions{
BeforeUpdateAt: dg.lastNotifyAt + 1,
Limit: 1,
Descending: true,
}
history, err := dg.store.GetBlockHistory(newBlock.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err)
}
var oldBlock *model.Block
if len(history) != 0 {
oldBlock = history[0]
dg.logger.Debug("generateDiffForBlock - old block",
mlog.String("block_id", oldBlock.ID),
mlog.String("block_type", string(oldBlock.Type)),
mlog.Int64("before_update_at", dg.lastNotifyAt),
mlog.String("modified_by", oldBlock.ModifiedBy),
mlog.Int64("update_at", oldBlock.UpdateAt),
)
}
// find all the versions of the blocks that changed so we can gather all the author usernames.
opts = model.QueryBlockHistoryOptions{
AfterUpdateAt: dg.lastNotifyAt,
Descending: true,
}
chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts)
if err != nil {
return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err)
}
authors := make(StringMap)
dg.logger.Debug("generateDiffForBlock - authors",
mlog.Int64("after_update_at", dg.lastNotifyAt),
mlog.Int("history_count", len(chgBlocks)),
)
// have to loop through history slice because GetBlockHistory does not return pointers.
for _, b := range chgBlocks {
user, err := dg.store.GetUserByID(b.ModifiedBy)
if err != nil || user == nil {
dg.logger.Error("could not fetch username for block",
mlog.String("modified_by", b.ModifiedBy),
mlog.Err(err),
)
authors.Add(b.ModifiedBy, "unknown_user") // todo: localize this when server has i18n
} else {
authors.Add(user.ID, user.Username)
}
}
propDiffs := dg.generatePropDiffs(oldBlock, newBlock, schema)
dg.logger.Debug("generateDiffForBlock - results",
mlog.String("block_id", newBlock.ID),
mlog.String("block_type", string(newBlock.Type)),
mlog.Array("author_names", authors.Values()),
mlog.Int("history_count", len(history)),
mlog.Int("prop_diff_count", len(propDiffs)),
)
diff := &Diff{
Board: dg.board,
Card: dg.card,
Authors: authors,
BlockType: newBlock.Type,
OldBlock: oldBlock,
NewBlock: newBlock,
UpdateAt: newBlock.UpdateAt,
PropDiffs: propDiffs,
schemaDiffs: nil,
}
return diff, nil
}
func (dg *diffGenerator) generatePropDiffs(oldBlock, newBlock *model.Block, schema model.PropSchema) []PropDiff {
var propDiffs []PropDiff
oldProps, err := model.ParseProperties(oldBlock, schema, dg.store)
if err != nil {
dg.logger.Error("Cannot parse properties for old block",
mlog.String("block_id", oldBlock.ID),
mlog.Err(err),
)
}
newProps, err := model.ParseProperties(newBlock, schema, dg.store)
if err != nil {
dg.logger.Error("Cannot parse properties for new block",
mlog.String("block_id", oldBlock.ID),
mlog.Err(err),
)
}
// look for new or changed properties.
for k, prop := range newProps {
oldP, ok := oldProps[k]
if ok {
// prop changed
if prop.Value != oldP.Value {
propDiffs = append(propDiffs, PropDiff{
ID: prop.ID,
Index: prop.Index,
Name: prop.Name,
NewValue: prop.Value,
OldValue: oldP.Value,
})
}
} else {
// prop added
propDiffs = append(propDiffs, PropDiff{
ID: prop.ID,
Index: prop.Index,
Name: prop.Name,
NewValue: prop.Value,
OldValue: "",
})
}
}
// look for deleted properties
for k, prop := range oldProps {
_, ok := newProps[k]
if !ok {
// prop deleted
propDiffs = append(propDiffs, PropDiff{
ID: prop.ID,
Index: prop.Index,
Name: prop.Name,
NewValue: "",
OldValue: prop.Value,
})
}
}
return sortPropDiffs(propDiffs)
}
func sortPropDiffs(propDiffs []PropDiff) []PropDiff {
if len(propDiffs) == 0 {
return propDiffs
}
sort.Slice(propDiffs, func(i, j int) bool {
return propDiffs[i].Index < propDiffs[j].Index
})
return propDiffs
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func generateMarkdownDiff(oldText string, newText string, logger mlog.LoggerIFace) string {
oldTxtNorm := normalizeText(oldText)
newTxtNorm := normalizeText(newText)
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(oldTxtNorm, newTxtNorm, false)
diffs = dmp.DiffCleanupSemantic(diffs)
diffs = dmp.DiffCleanupEfficiency(diffs)
// check there is at least one insert or delete
var editFound bool
for _, d := range diffs {
if (d.Type == diffmatchpatch.DiffInsert || d.Type == diffmatchpatch.DiffDelete) && strings.TrimSpace(d.Text) != "" {
editFound = true
break
}
}
if !editFound {
logger.Debug("skipping notification for superficial diff")
return ""
}
cfg := markDownCfg{
insertOpen: "`",
insertClose: "`",
deleteOpen: "~~`",
deleteClose: "`~~",
}
markdown := generateMarkdown(diffs, cfg)
markdown = strings.ReplaceAll(markdown, "¶", "\n")
return markdown
}
const (
truncLenEquals = 60
truncLenInserts = 120
truncLenDeletes = 80
)
type markDownCfg struct {
insertOpen string
insertClose string
deleteOpen string
deleteClose string
}
func generateMarkdown(diffs []diffmatchpatch.Diff, cfg markDownCfg) string {
sb := &strings.Builder{}
var first, last bool
for i, diff := range diffs {
first = i == 0
last = i == len(diffs)-1
switch diff.Type {
case diffmatchpatch.DiffInsert:
sb.WriteString(cfg.insertOpen)
sb.WriteString(truncate(diff.Text, truncLenInserts, first, last))
sb.WriteString(cfg.insertClose)
case diffmatchpatch.DiffDelete:
sb.WriteString(cfg.deleteOpen)
sb.WriteString(truncate(diff.Text, truncLenDeletes, first, last))
sb.WriteString(cfg.deleteClose)
case diffmatchpatch.DiffEqual:
sb.WriteString(truncate(diff.Text, truncLenEquals, first, last))
}
}
return sb.String()
}
func truncate(s string, maxLen int, first bool, last bool) string {
if len(s) < maxLen {
return s
}
var result string
switch {
case first:
// truncate left
result = " ... " + rightWords(s, maxLen)
case last:
// truncate right
result = leftWords(s, maxLen) + " ... "
default:
// truncate in the middle
half := len(s) / 2
left := leftWords(s[:half], maxLen/2)
right := rightWords(s[half:], maxLen/2)
result = left + " ... " + right
}
return strings.ReplaceAll(result, "¶", "↩")
}
func normalizeText(s string) string {
s = strings.ReplaceAll(s, "\t", " ")
s = strings.ReplaceAll(s, " ", " ")
s = strings.ReplaceAll(s, "\n\n", "\n")
s = strings.ReplaceAll(s, "\n", "¶")
return s
}
// leftWords returns approximately maxLen characters from the left part of the source string by truncating on the right,
// with best effort to include whole words.
func leftWords(s string, maxLen int) string {
if len(s) < maxLen {
return s
}
fields := strings.Fields(s)
fields = words(fields, maxLen)
return strings.Join(fields, " ")
}
// rightWords returns approximately maxLen from the right part of the source string by truncating from the left,
// with best effort to include whole words.
func rightWords(s string, maxLen int) string {
if len(s) < maxLen {
return s
}
fields := strings.Fields(s)
// reverse the fields so that the right-most words end up at the beginning.
reverse(fields)
fields = words(fields, maxLen)
// reverse the fields again so that the original order is restored.
reverse(fields)
return strings.Join(fields, " ")
}
func reverse(ss []string) {
ssLen := len(ss)
for i := 0; i < ssLen/2; i++ {
ss[i], ss[ssLen-i-1] = ss[ssLen-i-1], ss[i]
}
}
// words returns a subslice containing approximately maxChars of characters. The last item may be truncated.
func words(words []string, maxChars int) []string {
var count int
result := make([]string, 0, len(words))
for i, w := range words {
wordLen := len(w)
if wordLen+count > maxChars {
switch {
case i == 0:
result = append(result, w[:maxChars])
case wordLen < 8:
result = append(result, w)
}
return result
}
count += wordLen
result = append(result, w)
}
return result
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
"text/template"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
// card change notifications.
defAddCardNotify = "{{.Authors | printAuthors \"unknown_user\" }} has added the card {{. | makeLink}}\n"
defModifyCardNotify = "###### {{.Authors | printAuthors \"unknown_user\" }} has modified the card {{. | makeLink}} on the board {{. | makeBoardLink}}\n"
defDeleteCardNotify = "{{.Authors | printAuthors \"unknown_user\" }} has deleted the card {{. | makeLink}}\n"
)
var (
// templateCache is a map of text templateCache keyed by languange code.
templateCache = make(map[string]*template.Template)
templateCacheMux sync.Mutex
)
// DiffConvOpts provides options when converting diffs to slack attachments.
type DiffConvOpts struct {
Language string
MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string
MakeBoardLink func(board *model.Board) string
Logger mlog.LoggerIFace
}
// getTemplate returns a new or cached named template based on the language specified.
func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template, error) {
templateCacheMux.Lock()
defer templateCacheMux.Unlock()
key := name + "&" + opts.Language
t, ok := templateCache[key]
if !ok {
t = template.New(key)
if opts.MakeCardLink == nil {
opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string {
return fmt.Sprintf("`%s`", block.Title)
}
}
if opts.MakeBoardLink == nil {
opts.MakeBoardLink = func(board *model.Board) string {
return fmt.Sprintf("`%s`", board.Title)
}
}
myFuncs := template.FuncMap{
"getBoardDescription": getBoardDescription,
"makeLink": func(diff *Diff) string {
return opts.MakeCardLink(diff.NewBlock, diff.Board, diff.Card)
},
"makeBoardLink": func(diff *Diff) string {
return opts.MakeBoardLink(diff.Board)
},
"stripNewlines": func(s string) string {
return strings.TrimSpace(strings.ReplaceAll(s, "\n", "¶ "))
},
"printAuthors": func(empty string, authors StringMap) string {
return makeAuthorsList(authors, empty)
},
}
t.Funcs(myFuncs)
s := def // TODO: lookup i18n string when supported on server
t2, err := t.Parse(s)
if err != nil {
return nil, fmt.Errorf("cannot parse markdown template '%s' for notifications: %w", key, err)
}
templateCache[key] = t2
}
return t, nil
}
func makeAuthorsList(authors StringMap, empty string) string {
if len(authors) == 0 {
return empty
}
prefix := ""
sb := &strings.Builder{}
for _, name := range authors.Values() {
sb.WriteString(prefix)
sb.WriteString("@")
sb.WriteString(strings.TrimSpace(name))
prefix = ", "
}
return sb.String()
}
// execTemplate executes the named template corresponding to the template name and language specified.
func execTemplate(w io.Writer, name string, opts DiffConvOpts, def string, data interface{}) error {
t, err := getTemplate(name, opts, def)
if err != nil {
return err
}
return t.Execute(w, data)
}
// Diffs2SlackAttachments converts a slice of `Diff` to slack attachments to be used in a post.
func Diffs2SlackAttachments(diffs []*Diff, opts DiffConvOpts) ([]*mm_model.SlackAttachment, error) {
var attachments []*mm_model.SlackAttachment
merr := merror.New()
for _, d := range diffs {
// only handle cards for now.
if d.BlockType == model.TypeCard {
a, err := cardDiff2SlackAttachment(d, opts)
if err != nil {
merr.Append(err)
continue
}
if a == nil {
continue
}
attachments = append(attachments, a)
}
}
return attachments, merr.ErrorOrNil()
}
func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.SlackAttachment, error) {
// sanity check
if cardDiff.NewBlock == nil && cardDiff.OldBlock == nil {
return nil, nil
}
attachment := &mm_model.SlackAttachment{}
buf := &bytes.Buffer{}
// card added
if cardDiff.NewBlock != nil && cardDiff.OldBlock == nil {
if err := execTemplate(buf, "AddCardNotify", opts, defAddCardNotify, cardDiff); err != nil {
return nil, err
}
attachment.Pretext = buf.String()
attachment.Fallback = attachment.Pretext
return attachment, nil
}
// card deleted
if (cardDiff.NewBlock == nil || cardDiff.NewBlock.DeleteAt != 0) && cardDiff.OldBlock != nil {
buf.Reset()
if err := execTemplate(buf, "DeleteCardNotify", opts, defDeleteCardNotify, cardDiff); err != nil {
return nil, err
}
attachment.Pretext = buf.String()
attachment.Fallback = attachment.Pretext
return attachment, nil
}
// at this point new and old block are non-nil
opts.Logger.Debug("cardDiff2SlackAttachment",
mlog.String("board_id", cardDiff.Board.ID),
mlog.String("card_id", cardDiff.Card.ID),
mlog.String("new_block_id", cardDiff.NewBlock.ID),
mlog.String("old_block_id", cardDiff.OldBlock.ID),
mlog.Int("childDiffs", len(cardDiff.Diffs)),
)
buf.Reset()
if err := execTemplate(buf, "ModifyCardNotify", opts, defModifyCardNotify, cardDiff); err != nil {
return nil, fmt.Errorf("cannot write notification for card %s: %w", cardDiff.NewBlock.ID, err)
}
attachment.Pretext = buf.String()
attachment.Fallback = attachment.Pretext
// title changes
attachment.Fields = appendTitleChanges(attachment.Fields, cardDiff)
// property changes
attachment.Fields = appendPropertyChanges(attachment.Fields, cardDiff)
// comment add/delete
attachment.Fields = appendCommentChanges(attachment.Fields, cardDiff)
// File Attachment add/delete
attachment.Fields = appendAttachmentChanges(attachment.Fields, cardDiff)
// content/description changes
attachment.Fields = appendContentChanges(attachment.Fields, cardDiff, opts.Logger)
if len(attachment.Fields) == 0 {
return nil, nil
}
return attachment, nil
}
func appendTitleChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
if cardDiff.NewBlock.Title != cardDiff.OldBlock.Title {
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Title",
Value: fmt.Sprintf("%s ~~`%s`~~", stripNewlines(cardDiff.NewBlock.Title), stripNewlines(cardDiff.OldBlock.Title)),
})
}
return fields
}
func appendPropertyChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
if len(cardDiff.PropDiffs) == 0 {
return fields
}
for _, propDiff := range cardDiff.PropDiffs {
if propDiff.NewValue == propDiff.OldValue {
continue
}
var val string
if propDiff.OldValue != "" {
val = fmt.Sprintf("%s ~~`%s`~~", stripNewlines(propDiff.NewValue), stripNewlines(propDiff.OldValue))
} else {
val = propDiff.NewValue
}
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: propDiff.Name,
Value: val,
})
}
return fields
}
func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
for _, child := range cardDiff.Diffs {
if child.BlockType == model.TypeComment {
var format string
var msg string
if child.NewBlock != nil && child.OldBlock == nil {
// added comment
format = "%s"
msg = child.NewBlock.Title
}
if (child.NewBlock == nil || child.NewBlock.DeleteAt != 0) && child.OldBlock != nil {
// deleted comment
format = "~~`%s`~~"
msg = stripNewlines(child.OldBlock.Title)
}
if format != "" {
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Comment by " + makeAuthorsList(child.Authors, "unknown_user"), // todo: localize this when server has i18n
Value: fmt.Sprintf(format, msg),
})
}
}
}
return fields
}
func appendAttachmentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
for _, child := range cardDiff.Diffs {
if child.BlockType == model.TypeAttachment {
var format string
var msg string
if child.NewBlock != nil && child.OldBlock == nil {
format = "Added an attachment: **`%s`**"
msg = child.NewBlock.Title
} else {
format = "Removed ~~`%s`~~ attachment"
msg = stripNewlines(child.OldBlock.Title)
}
if format != "" {
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Changed by " + makeAuthorsList(child.Authors, "unknown_user"), // TODO: localize this when server has i18n
Value: fmt.Sprintf(format, msg),
})
}
}
}
return fields
}
func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField {
for _, child := range cardDiff.Diffs {
var opAdd, opDelete bool
var opString string
switch {
case child.OldBlock == nil && child.NewBlock != nil:
opAdd = true
opString = "added" // TODO: localize when i18n added to server
case child.NewBlock == nil || child.NewBlock.DeleteAt != 0:
opDelete = true
opString = "deleted"
default:
opString = "modified"
}
var newTitle, oldTitle string
if child.OldBlock != nil {
oldTitle = child.OldBlock.Title
}
if child.NewBlock != nil {
newTitle = child.NewBlock.Title
}
switch child.BlockType {
case model.TypeDivider, model.TypeComment:
// do nothing
continue
case model.TypeImage:
if newTitle == "" {
newTitle = "An image was " + opString + "." // TODO: localize when i18n added to server
}
oldTitle = ""
case model.TypeAttachment:
if newTitle == "" {
newTitle = "A file attachment was " + opString + "." // TODO: localize when i18n added to server
}
oldTitle = ""
default:
if !opAdd {
if opDelete {
newTitle = ""
}
// only strip newlines when modifying or deleting
oldTitle = stripNewlines(oldTitle)
newTitle = stripNewlines(newTitle)
}
if newTitle == oldTitle {
continue
}
}
logger.Trace("appendContentChanges",
mlog.String("type", string(child.BlockType)),
mlog.String("opString", opString),
mlog.String("oldTitle", oldTitle),
mlog.String("newTitle", newTitle),
)
markdown := generateMarkdownDiff(oldTitle, newTitle, logger)
if markdown == "" {
continue
}
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Description",
Value: markdown,
})
}
return fields
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"errors"
"fmt"
"sync"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defBlockNotificationFreq = time.Minute * 2
enqueueNotifyHintTimeout = time.Second * 10
hintQueueSize = 20
)
var (
errEnqueueNotifyHintTimeout = errors.New("enqueue notify hint timed out")
)
// notifier provides block change notifications for subscribers. Block change events are batched
// via notifications hints written to the database so that fewer notifications are sent for active
// blocks.
type notifier struct {
serverRoot string
store AppAPI
permissions permissions.PermissionsService
delivery SubscriptionDelivery
logger mlog.LoggerIFace
hints chan *model.NotificationHint
mux sync.Mutex
done chan struct{}
}
func newNotifier(params BackendParams) *notifier {
return ¬ifier{
serverRoot: params.ServerRoot,
store: params.AppAPI,
permissions: params.Permissions,
delivery: params.Delivery,
logger: params.Logger,
done: nil,
hints: make(chan *model.NotificationHint, hintQueueSize),
}
}
func (n *notifier) start() {
n.mux.Lock()
defer n.mux.Unlock()
if n.done == nil {
n.done = make(chan struct{})
go n.loop()
}
}
func (n *notifier) stop() {
n.mux.Lock()
defer n.mux.Unlock()
if n.done != nil {
close(n.done)
n.done = nil
}
}
func (n *notifier) loop() {
done := n.done
var nextNotify time.Time
for {
hint, err := n.store.GetNextNotificationHint(false)
switch {
case model.IsErrNotFound(err):
// no hints in table; wait up to an hour or when `onNotifyHint` is called again
nextNotify = time.Now().Add(time.Hour * 1)
n.logger.Debug("notify loop - no hints in queue", mlog.Time("next_check", nextNotify))
case err != nil:
// try again in a minute
nextNotify = time.Now().Add(time.Minute * 1)
n.logger.Error("notify loop - error fetching next notification", mlog.Err(err))
case hint.NotifyAt > utils.GetMillis():
// next hint is not ready yet; sleep until hint.NotifyAt
nextNotify = utils.GetTimeForMillis(hint.NotifyAt)
default:
// it's time to notify
n.notify()
continue
}
n.logger.Debug("subscription notifier loop",
mlog.Time("next_notify", nextNotify),
)
select {
case <-n.hints:
// A new hint was added. Wake up and check if next hint is ready to go.
case <-time.After(time.Until(nextNotify)):
// Next scheduled hint should be ready now.
case <-done:
return
}
}
}
func (n *notifier) onNotifyHint(hint *model.NotificationHint) error {
n.logger.Debug("onNotifyHint - enqueing hint", mlog.Any("hint", hint))
select {
case n.hints <- hint:
case <-time.After(enqueueNotifyHintTimeout):
return errEnqueueNotifyHintTimeout
}
return nil
}
func (n *notifier) notify() {
var hint *model.NotificationHint
var err error
hint, err = n.store.GetNextNotificationHint(true)
if err != nil {
if model.IsErrNotFound(err) {
// Expected when multiple nodes in a cluster try to process the same hint at the same time.
// This simply means the other node won. Returning here will simply try fetching another hint.
return
}
n.logger.Error("notify - error fetching next notification", mlog.Err(err))
return
}
if err = n.notifySubscribers(hint); err != nil {
n.logger.Error("Error notifying subscribers", mlog.Err(err))
}
}
func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
// get the subscriber list
subs, err := n.store.GetSubscribersForBlock(hint.BlockID)
if err != nil {
return err
}
if len(subs) == 0 {
n.logger.Debug("notifySubscribers - no subscribers", mlog.Any("hint", hint))
return nil
}
// subs slice is sorted by `NotifiedAt`, therefore subs[0] contains the oldest NotifiedAt needed
oldestNotifiedAt := subs[0].NotifiedAt
// need the block's board and card.
board, card, err := n.store.GetBoardAndCardByID(hint.BlockID)
if err != nil || board == nil || card == nil {
return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err)
}
n.logger.Debug("notifySubscribers - subscribers",
mlog.Any("hint", hint),
mlog.String("board_id", board.ID),
mlog.String("card_id", card.ID),
mlog.Int("sub_count", len(subs)),
)
dg := &diffGenerator{
board: board,
card: card,
store: n.store,
hint: hint,
lastNotifyAt: oldestNotifiedAt,
logger: n.logger,
}
diffs, err := dg.generateDiffs()
if err != nil {
return err
}
n.logger.Debug("notifySubscribers - diffs",
mlog.Any("hint", hint),
mlog.Int("diff_count", len(diffs)),
)
if len(diffs) == 0 {
return nil
}
diffAuthors := make(StringMap)
for _, d := range diffs {
diffAuthors.Append(d.Authors)
}
opts := DiffConvOpts{
Language: "en", // TODO: use correct language when i18n is available on server.
MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string {
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID))
},
MakeBoardLink: func(board *model.Board) string {
return fmt.Sprintf("[%s](%s)", board.Title, utils.MakeBoardLink(n.serverRoot, board.TeamID, board.ID))
},
Logger: n.logger,
}
attachments, err := Diffs2SlackAttachments(diffs, opts)
if err != nil {
return err
}
merr := merror.New()
if len(attachments) > 0 {
for _, sub := range subs {
// don't notify the author of their own changes.
authorName, isAuthor := diffAuthors[sub.SubscriberID]
if isAuthor && len(diffAuthors) == 1 {
n.logger.Debug("notifySubscribers - skipping author",
mlog.Any("hint", hint),
mlog.String("author_id", sub.SubscriberID),
mlog.String("author_username", authorName),
)
continue
}
// make sure the subscriber still has permissions for the board.
if !n.permissions.HasPermissionToBoard(sub.SubscriberID, board.ID, model.PermissionViewBoard) {
n.logger.Debug("notifySubscribers - skipping non-board member",
mlog.Any("hint", hint),
mlog.String("subscriber_id", sub.SubscriberID),
mlog.String("board_id", board.ID),
)
continue
}
n.logger.Debug("notifySubscribers - deliver",
mlog.Any("hint", hint),
mlog.String("modified_by_id", hint.ModifiedByID),
mlog.String("subscriber_id", sub.SubscriberID),
mlog.String("subscriber_type", string(sub.SubscriberType)),
)
if err = n.delivery.SubscriptionDeliverSlackAttachments(board.TeamID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil {
merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w",
sub.SubscriberID, sub.SubscriberType, err))
}
}
} else {
n.logger.Debug("notifySubscribers - skip delivery; no chg",
mlog.Any("hint", hint),
mlog.String("modified_by_id", hint.ModifiedByID),
)
}
// find the new NotifiedAt based on the newest diff.
var notifiedAt int64
for _, d := range diffs {
if d.UpdateAt > notifiedAt {
notifiedAt = d.UpdateAt
}
for _, c := range d.Diffs {
if c.UpdateAt > notifiedAt {
notifiedAt = c.UpdateAt
}
}
}
// update the last notified_at for all subscribers since we at least attempted to notify all of them.
err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt)
if err != nil {
merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err))
}
return merr.ErrorOrNil()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"fmt"
"os"
"strconv"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
backendName = "notifySubscriptions"
)
type BackendParams struct {
ServerRoot string
AppAPI AppAPI
Permissions permissions.PermissionsService
Delivery SubscriptionDelivery
Logger mlog.LoggerIFace
NotifyFreqCardSeconds int
NotifyFreqBoardSeconds int
}
// Backend provides the notification backend for subscriptions.
type Backend struct {
appAPI AppAPI
permissions permissions.PermissionsService
delivery SubscriptionDelivery
notifier *notifier
logger mlog.LoggerIFace
notifyFreqCardSeconds int
notifyFreqBoardSeconds int
}
func New(params BackendParams) *Backend {
return &Backend{
appAPI: params.AppAPI,
delivery: params.Delivery,
permissions: params.Permissions,
notifier: newNotifier(params),
logger: params.Logger,
notifyFreqCardSeconds: params.NotifyFreqCardSeconds,
notifyFreqBoardSeconds: params.NotifyFreqBoardSeconds,
}
}
func (b *Backend) Start() error {
b.logger.Debug("Starting subscriptions backend",
mlog.Int("freq_card", b.notifyFreqCardSeconds),
mlog.Int("freq_board", b.notifyFreqBoardSeconds),
)
b.notifier.start()
return nil
}
func (b *Backend) ShutDown() error {
b.logger.Debug("Stopping subscriptions backend")
b.notifier.stop()
_ = b.logger.Flush()
return nil
}
func (b *Backend) Name() string {
return backendName
}
func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration {
// check for env variable override
sFreq := os.Getenv("MM_BOARDS_NOTIFY_FREQ_SECONDS")
if sFreq != "" && sFreq != "0" {
if freq, err := strconv.ParseInt(sFreq, 10, 64); err != nil {
b.logger.Error("Environment variable MM_BOARDS_NOTIFY_FREQ_SECONDS invalid (ignoring)", mlog.Err(err))
} else {
return time.Second * time.Duration(freq)
}
}
switch blockType {
case model.TypeCard:
return time.Second * time.Duration(b.notifyFreqCardSeconds)
default:
return defBlockNotificationFreq
}
}
func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
if evt.Board == nil {
b.logger.Warn("No board found for block, skipping notify",
mlog.String("block_id", evt.BlockChanged.ID),
)
return nil
}
merr := merror.New()
var err error
// if new card added, automatically subscribe the author.
if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard {
sub := &model.Subscription{
BlockType: model.TypeCard,
BlockID: evt.BlockChanged.ID,
SubscriberType: model.SubTypeUser,
SubscriberID: evt.ModifiedBy.UserID,
}
if _, err = b.appAPI.CreateSubscription(sub); err != nil {
b.logger.Warn("Cannot subscribe card author to card",
mlog.String("card_id", evt.BlockChanged.ID),
mlog.Err(err),
)
}
}
// notify board subscribers
subs, err := b.appAPI.GetSubscribersForBlock(evt.Board.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err))
}
if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err))
}
if evt.Card == nil {
return merr.ErrorOrNil()
}
// notify card subscribers
subs, err = b.appAPI.GetSubscribersForBlock(evt.Card.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err))
}
if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err))
}
// notify block subscribers (if/when other types can be subscribed to)
if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID {
subs, err := b.appAPI.GetSubscribersForBlock(evt.BlockChanged.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err))
}
if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedBy.UserID); err != nil {
merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err))
}
}
return merr.ErrorOrNil()
}
// notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database.
func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error {
if len(subs) == 0 {
return nil
}
hint := &model.NotificationHint{
BlockType: idType,
BlockID: blockID,
ModifiedByID: modifiedByID,
}
hint, err := b.appAPI.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType))
if err != nil {
return fmt.Errorf("cannot upsert notification hint: %w", err)
}
if err := b.notifier.onNotifyHint(hint); err != nil {
return err
}
return nil
}
// OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification
// is sent. Here we create a subscription for the mentioned user to the card.
func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
if evt.Card == nil {
b.logger.Debug("Cannot subscribe mentioned user to nil card",
mlog.String("user_id", userID),
mlog.String("block_id", evt.BlockChanged.ID),
)
return
}
// user mentioned must be a board member to subscribe to card.
if !b.permissions.HasPermissionToBoard(userID, evt.Board.ID, model.PermissionViewBoard) {
b.logger.Debug("Not subscribing mentioned non-board member to card",
mlog.String("user_id", userID),
mlog.String("block_id", evt.BlockChanged.ID),
)
return
}
sub := &model.Subscription{
BlockType: model.TypeCard,
BlockID: evt.Card.ID,
SubscriberType: model.SubTypeUser,
SubscriberID: userID,
}
var err error
if _, err = b.appAPI.CreateSubscription(sub); err != nil {
b.logger.Warn("Cannot subscribe mentioned user to card",
mlog.String("user_id", userID),
mlog.String("card_id", evt.Card.ID),
mlog.Err(err),
)
return
}
b.logger.Debug("Subscribed mentioned user to card",
mlog.String("user_id", userID),
mlog.String("card_id", evt.Card.ID),
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notifysubscriptions
import (
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func getBoardDescription(board *model.Block) string {
if board == nil {
return ""
}
descr, ok := board.Fields["description"]
if !ok {
return ""
}
description, ok := descr.(string)
if !ok {
return ""
}
return description
}
func stripNewlines(s string) string {
return strings.TrimSpace(strings.ReplaceAll(s, "\n", "¶ "))
}
type StringMap map[string]string
func (sm StringMap) Add(k string, v string) {
sm[k] = v
}
func (sm StringMap) Append(m StringMap) {
for k, v := range m {
sm[k] = v
}
}
func (sm StringMap) Keys() []string {
keys := make([]string, 0, len(sm))
for k := range sm {
keys = append(keys, k)
}
return keys
}
func (sm StringMap) Values() []string {
values := make([]string, 0, len(sm))
for _, v := range sm {
values = append(values, v)
}
return values
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugindelivery
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/services/notify"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// MentionDeliver notifies a user they have been mentioned in a blockv ia the plugin API.
func (pd *PluginDelivery) MentionDeliver(mentionedUser *mm_model.User, extract string, evt notify.BlockChangeEvent) (string, error) {
author, err := pd.api.GetUserByID(evt.ModifiedBy.UserID)
if err != nil {
return "", fmt.Errorf("cannot find user: %w", err)
}
channel, err := pd.getDirectChannel(evt.TeamID, mentionedUser.Id, pd.botID)
if err != nil {
return "", fmt.Errorf("cannot get direct channel: %w", err)
}
link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID)
boardLink := utils.MakeBoardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID)
post := &mm_model.Post{
UserId: pd.botID,
ChannelId: channel.Id,
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged, boardLink, evt.Board.Title),
}
if _, err := pd.api.CreatePost(post); err != nil {
return "", err
}
return mentionedUser.Id, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugindelivery
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
const (
// TODO: localize these when i18n is available.
defCommentTemplate = "@%s mentioned you in a comment on the card [%s](%s) in board [%s](%s)\n> %s"
defDescriptionTemplate = "@%s mentioned you in the card [%s](%s) in board [%s](%s)\n> %s"
)
func formatMessage(author string, extract string, card string, link string, block *model.Block, boardLink string, board string) string {
template := defDescriptionTemplate
if block.Type == model.TypeComment {
template = defCommentTemplate
}
return fmt.Sprintf(template, author, card, link, board, boardLink, extract)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugindelivery
import (
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
type servicesAPI interface {
// GetDirectChannelOrCreate gets a direct message channel,
// or creates one if it does not already exist
GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error)
// CreatePost creates a post.
CreatePost(post *mm_model.Post) (*mm_model.Post, error)
// GetUserByID gets a user by their ID.
GetUserByID(userID string) (*mm_model.User, error)
// GetUserByUsername gets a user by their username.
GetUserByUsername(name string) (*mm_model.User, error)
// GetTeamMember gets a team member by their user id.
GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error)
// GetChannelByID gets a Channel by its ID.
GetChannelByID(channelID string) (*mm_model.Channel, error)
// GetChannelMember gets a channel member by userID.
GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error)
// CreateMember adds a user to the specified team. Safe to call if the user is
// already a member of the team.
CreateMember(teamID string, userID string) (*mm_model.TeamMember, error)
}
// PluginDelivery provides ability to send notifications to direct message channels via Mattermost plugin API.
type PluginDelivery struct {
botID string
serverRoot string
api servicesAPI
}
// New creates a PluginDelivery instance.
func New(botID string, serverRoot string, api servicesAPI) *PluginDelivery {
return &PluginDelivery{
botID: botID,
serverRoot: serverRoot,
api: api,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugindelivery
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
var (
ErrUnsupportedSubscriberType = errors.New("invalid subscriber type")
)
// SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to.
func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(teamID string, subscriberID string, subscriptionType model.SubscriberType,
attachments []*mm_model.SlackAttachment) error {
// check subscriber is member of channel
_, err := pd.api.GetUserByID(subscriberID)
if err != nil {
if model.IsErrNotFound(err) {
// subscriber is not a member of the channel; fail silently.
return nil
}
return fmt.Errorf("cannot fetch channel member for user %s: %w", subscriberID, err)
}
channelID, err := pd.getDirectChannelID(teamID, subscriberID, subscriptionType, pd.botID)
if err != nil {
return err
}
post := &mm_model.Post{
UserId: pd.botID,
ChannelId: channelID,
}
mm_model.ParseSlackAttachment(post, attachments)
_, err = pd.api.CreatePost(post)
return err
}
func (pd *PluginDelivery) getDirectChannelID(teamID string, subscriberID string, subscriberType model.SubscriberType, botID string) (string, error) {
switch subscriberType {
case model.SubTypeUser:
user, err := pd.api.GetUserByID(subscriberID)
if err != nil {
return "", fmt.Errorf("cannot find user: %w", err)
}
channel, err := pd.getDirectChannel(teamID, user.Id, botID)
if err != nil || channel == nil {
return "", fmt.Errorf("cannot get direct channel: %w", err)
}
return channel.Id, nil
case model.SubTypeChannel:
return subscriberID, nil
default:
return "", ErrUnsupportedSubscriberType
}
}
func (pd *PluginDelivery) getDirectChannel(teamID string, userID string, botID string) (*mm_model.Channel, error) {
// first ensure the bot is a member of the team.
_, err := pd.api.CreateMember(teamID, botID)
if err != nil {
return nil, fmt.Errorf("cannot add bot to team %s: %w", teamID, err)
}
return pd.api.GetDirectChannelOrCreate(userID, botID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugindelivery
import (
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
const (
usernameSpecialChars = ".-_ "
)
func (pd *PluginDelivery) UserByUsername(username string) (*mm_model.User, error) {
// check for usernames that might have trailing punctuation
var user *mm_model.User
var err error
ok := true
trimmed := username
for ok {
user, err = pd.api.GetUserByUsername(trimmed)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
if err == nil {
break
}
trimmed, ok = trimUsernameSpecialChar(trimmed)
}
if user == nil {
return nil, err
}
return user, nil
}
// trimUsernameSpecialChar tries to remove the last character from word if it
// is a special character for usernames (dot, dash or underscore). If not, it
// returns the same string.
func trimUsernameSpecialChar(word string) (string, bool) {
len := len(word)
if len > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (len-1) {
return word[:len-1], true
}
return word, false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify
import (
"sync"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Action string
const (
Add Action = "add"
Update Action = "update"
Delete Action = "delete"
)
type BlockChangeEvent struct {
Action Action
TeamID string
Board *model.Board
Card *model.Block
BlockChanged *model.Block
BlockOld *model.Block
ModifiedBy *model.BoardMember
}
// Backend provides an interface for sending notifications.
type Backend interface {
Start() error
ShutDown() error
BlockChanged(evt BlockChangeEvent) error
Name() string
}
// Service is a service that sends notifications based on block activity using one or more backends.
type Service struct {
mux sync.RWMutex
backends []Backend
logger mlog.LoggerIFace
}
// New creates a notification service with one or more Backends capable of sending notifications.
func New(logger mlog.LoggerIFace, backends ...Backend) (*Service, error) {
notify := &Service{
backends: make([]Backend, 0, len(backends)),
logger: logger,
}
merr := merror.New()
for _, backend := range backends {
if err := notify.AddBackend(backend); err != nil {
merr.Append(err)
} else {
logger.Info("Initialized notification backend", mlog.String("name", backend.Name()))
}
}
return notify, merr.ErrorOrNil()
}
// AddBackend adds a backend to the list that will be informed of any block changes.
func (s *Service) AddBackend(backend Backend) error {
if err := backend.Start(); err != nil {
return err
}
s.mux.Lock()
defer s.mux.Unlock()
s.backends = append(s.backends, backend)
return nil
}
// Shutdown calls shutdown for all backends.
func (s *Service) Shutdown() error {
s.mux.Lock()
defer s.mux.Unlock()
merr := merror.New()
for _, backend := range s.backends {
if err := backend.ShutDown(); err != nil {
merr.Append(err)
}
}
s.backends = nil
return merr.ErrorOrNil()
}
// BlockChanged should be called whenever a block is added/updated/deleted.
// All backends are informed of the event.
func (s *Service) BlockChanged(evt BlockChangeEvent) {
s.mux.RLock()
defer s.mux.RUnlock()
for _, backend := range s.backends {
if err := backend.BlockChanged(evt); err != nil {
s.logger.Error("Error delivering notification",
mlog.String("backend", backend.Name()),
mlog.String("action", string(evt.Action)),
mlog.String("block_id", evt.BlockChanged.ID),
mlog.Err(err),
)
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mmpermissions
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type APIInterface interface {
HasPermissionTo(userID string, permission *mm_model.Permission) bool
HasPermissionToTeam(userID string, teamID string, permission *mm_model.Permission) bool
HasPermissionToChannel(userID string, channelID string, permission *mm_model.Permission) bool
}
type Service struct {
store permissions.Store
api APIInterface
logger mlog.LoggerIFace
}
func New(store permissions.Store, api APIInterface, logger mlog.LoggerIFace) *Service {
return &Service{
store: store,
api: api,
logger: logger,
}
}
func (s *Service) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
if userID == "" || permission == nil {
return false
}
return s.api.HasPermissionTo(userID, permission)
}
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
if userID == "" || teamID == "" || permission == nil {
return false
}
return s.api.HasPermissionToTeam(userID, teamID, permission)
}
func (s *Service) HasPermissionToChannel(userID, channelID string, permission *mm_model.Permission) bool {
if userID == "" || channelID == "" || permission == nil {
return false
}
return s.api.HasPermissionToChannel(userID, channelID, permission)
}
func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool {
if userID == "" || boardID == "" || permission == nil {
return false
}
board, err := s.store.GetBoard(boardID)
if model.IsErrNotFound(err) {
var boards []*model.Board
boards, err = s.store.GetBoardHistory(boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return false
}
if len(boards) == 0 {
return false
}
board = boards[0]
} else if err != nil {
s.logger.Error("error getting board",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
mlog.Err(err),
)
return false
}
// we need to check that the user has permission to see the team
// regardless of its local permissions to the board
if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
return false
}
member, err := s.store.GetMemberForBoard(boardID, userID)
if model.IsErrNotFound(err) {
return false
}
if err != nil {
s.logger.Error("error getting member for board",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
mlog.Err(err),
)
return false
}
switch member.MinimumRole {
case "admin":
member.SchemeAdmin = true
case "editor":
member.SchemeEditor = true
case "commenter":
member.SchemeCommenter = true
case "viewer":
member.SchemeViewer = true
}
// Admins become member of boards, but get minimal role
// if they are a System/Team Admin (model.PermissionManageTeam)
// elevate their permissions
if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
return true
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
return member.SchemeAdmin
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
return member.SchemeAdmin || member.SchemeEditor
case model.PermissionCommentBoardCards:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
case model.PermissionViewBoard:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
default:
return false
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package scheduler
import (
"fmt"
"time"
)
type TaskFunc func()
type ScheduledTask struct {
Name string `json:"name"`
Interval time.Duration `json:"interval"`
Recurring bool `json:"recurring"`
function func()
cancel chan struct{}
cancelled chan struct{}
}
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask {
return createTask(name, function, timeToExecution, false)
}
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
return createTask(name, function, interval, true)
}
func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: interval,
Recurring: recurring,
function: function,
cancel: make(chan struct{}),
cancelled: make(chan struct{}),
}
go func() {
defer close(task.cancelled)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
function()
case <-task.cancel:
return
}
if !task.Recurring {
break
}
}
}()
return task
}
func (task *ScheduledTask) Cancel() {
close(task.cancel)
<-task.cancelled
}
func (task *ScheduledTask) String() string {
return fmt.Sprintf(
"%s\nInterval: %s\nRecurring: %t\n",
task.Name,
task.Interval.String(),
task.Recurring,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mattermostauthlayer
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
mm_model "github.com/mattermost/mattermost-server/v6/model"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var boardsBotID string
// servicesAPI is the interface required my the MattermostAuthLayer to interact with
// the mattermost-server. You can use plugin-api or product-api adapter implementations.
type servicesAPI interface {
GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error)
GetChannelByID(channelID string) (*mm_model.Channel, error)
GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error)
GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error)
GetUserByID(userID string) (*mm_model.User, error)
UpdateUser(user *mm_model.User) (*mm_model.User, error)
GetUserByEmail(email string) (*mm_model.User, error)
GetUserByUsername(username string) (*mm_model.User, error)
GetLicense() *mm_model.License
GetFileInfo(fileID string) (*mm_model.FileInfo, error)
GetCloudLimits() (*mm_model.ProductLimits, error)
EnsureBot(bot *mm_model.Bot) (string, error)
CreatePost(post *mm_model.Post) (*mm_model.Post, error)
GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error)
GetPreferencesForUser(userID string) (mm_model.Preferences, error)
DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error
UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error
}
// Store represents the abstraction of the data storage.
type MattermostAuthLayer struct {
store.Store
dbType string
mmDB *sql.DB
logger mlog.LoggerIFace
servicesAPI servicesAPI
tablePrefix string
}
// New creates a new SQL implementation of the store.
func New(dbType string, db *sql.DB, store store.Store, logger mlog.LoggerIFace, api servicesAPI, tablePrefix string) (*MattermostAuthLayer, error) {
layer := &MattermostAuthLayer{
Store: store,
dbType: dbType,
mmDB: db,
logger: logger,
servicesAPI: api,
tablePrefix: tablePrefix,
}
return layer, nil
}
// Shutdown close the connection with the store.
func (s *MattermostAuthLayer) Shutdown() error {
return s.Store.Shutdown()
}
func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("Users").
Where(sq.Eq{"deleteAt": 0})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *MattermostAuthLayer) GetUserByID(userID string) (*model.User, error) {
mmuser, err := s.servicesAPI.GetUserByID(userID)
if err != nil {
return nil, err
}
user := mmUserToFbUser(mmuser)
return &user, nil
}
func (s *MattermostAuthLayer) GetUserByEmail(email string) (*model.User, error) {
mmuser, err := s.servicesAPI.GetUserByEmail(email)
if err != nil {
return nil, err
}
user := mmUserToFbUser(mmuser)
return &user, nil
}
func (s *MattermostAuthLayer) GetUserByUsername(username string) (*model.User, error) {
mmuser, err := s.servicesAPI.GetUserByUsername(username)
if err != nil {
return nil, err
}
user := mmUserToFbUser(mmuser)
return &user, nil
}
func (s *MattermostAuthLayer) CreateUser(user *model.User) (*model.User, error) {
return nil, store.NewNotSupportedError("no user creation allowed from focalboard, create it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUser(user *model.User) (*model.User, error) {
return nil, store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUserPassword(username, password string) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) {
preferences, err := s.GetUserPreferences(userID)
if err != nil {
return nil, err
}
if len(patch.UpdatedFields) > 0 {
updatedPreferences := mm_model.Preferences{}
for key, value := range patch.UpdatedFields {
preference := mm_model.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
Value: value,
}
updatedPreferences = append(updatedPreferences, preference)
}
if err := s.servicesAPI.UpdatePreferencesForUser(userID, updatedPreferences); err != nil {
s.logger.Error("failed to update user preferences", mlog.String("user_id", userID), mlog.Err(err))
return nil, err
}
// we update the preferences list replacing or adding those
// that were updated
newPreferences := mm_model.Preferences{}
for _, existingPreference := range preferences {
hasBeenUpdated := false
for _, updatedPreference := range updatedPreferences {
if updatedPreference.Name == existingPreference.Name {
hasBeenUpdated = true
break
}
}
if !hasBeenUpdated {
newPreferences = append(newPreferences, existingPreference)
}
}
newPreferences = append(newPreferences, updatedPreferences...)
preferences = newPreferences
}
if len(patch.DeletedFields) > 0 {
deletedPreferences := mm_model.Preferences{}
for _, key := range patch.DeletedFields {
preference := mm_model.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
}
deletedPreferences = append(deletedPreferences, preference)
}
if err := s.servicesAPI.DeletePreferencesForUser(userID, deletedPreferences); err != nil {
s.logger.Error("failed to delete user preferences", mlog.String("user_id", userID), mlog.Err(err))
return nil, err
}
// we update the preferences removing those that have been
// deleted
newPreferences := mm_model.Preferences{}
for _, existingPreference := range preferences {
hasBeenDeleted := false
for _, deletedPreference := range deletedPreferences {
if deletedPreference.Name == existingPreference.Name {
hasBeenDeleted = true
break
}
}
if !hasBeenDeleted {
newPreferences = append(newPreferences, existingPreference)
}
}
preferences = newPreferences
}
return preferences, nil
}
func (s *MattermostAuthLayer) GetUserPreferences(userID string) (mm_model.Preferences, error) {
return s.servicesAPI.GetPreferencesForUser(userID)
}
// GetActiveUserCount returns the number of users with active sessions within N seconds ago.
func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
query := s.getQueryBuilder().
Select("count(distinct userId)").
From("Sessions").
Where(sq.Gt{"LastActivityAt": utils.GetMillis() - utils.SecondsToMillis(updatedSecondsAgo)})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *MattermostAuthLayer) GetSession(token string, expireTime int64) (*model.Session, error) {
return nil, store.NewNotSupportedError("sessions not used when using mattermost")
}
func (s *MattermostAuthLayer) CreateSession(session *model.Session) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) RefreshSession(session *model.Session) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) UpdateSession(session *model.Session) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) DeleteSession(sessionID string) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) CleanUpSessions(expireTime int64) error {
return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost")
}
func (s *MattermostAuthLayer) GetTeam(id string) (*model.Team, error) {
if id == "0" {
team := model.Team{
ID: id,
Title: "",
}
return &team, nil
}
query := s.getQueryBuilder().
Select("DisplayName").
From("Teams").
Where(sq.Eq{"ID": id})
row := query.QueryRow()
var displayName string
err := row.Scan(&displayName)
if err != nil && !model.IsErrNotFound(err) {
s.logger.Error("GetTeam scan error",
mlog.String("team_id", id),
mlog.Err(err),
)
return nil, err
}
return &model.Team{ID: id, Title: displayName}, nil
}
// GetTeamsForUser retrieves all the teams that the user is a member of.
func (s *MattermostAuthLayer) GetTeamsForUser(userID string) ([]*model.Team, error) {
query := s.getQueryBuilder().
Select("t.Id", "t.DisplayName").
From("Teams as t").
Join("TeamMembers as tm on t.Id=tm.TeamId").
Where(sq.Eq{"tm.UserId": userID}).
Where(sq.Eq{"tm.DeleteAt": 0})
rows, err := query.Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
teams := []*model.Team{}
for rows.Next() {
var team model.Team
err := rows.Scan(
&team.ID,
&team.Title,
)
if err != nil {
return nil, err
}
teams = append(teams, &team)
}
return teams, nil
}
func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
builder := sq.StatementBuilder
if s.dbType == model.PostgresDBType {
builder = builder.PlaceholderFormat(sq.Dollar)
}
return builder.RunWith(s.mmDB)
}
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string, asGuestID string, showEmail, showName bool) ([]*model.User, error) {
query := s.baseUserQuery(showEmail, showName).
Where(sq.Eq{"u.deleteAt": 0})
if asGuestID == "" {
query = query.
Join("TeamMembers as tm ON tm.UserID = u.id").
Where(sq.Eq{"tm.TeamId": teamID})
} else {
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
if err != nil {
return nil, err
}
boardsIDs := []string{}
for _, board := range boards {
boardsIDs = append(boardsIDs, board.ID)
}
query = query.
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
Where(sq.Eq{"bm.BoardId": boardsIDs})
}
rows, err := query.Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
users, err := s.usersFromRows(rows)
if err != nil {
return nil, err
}
return users, nil
}
func (s *MattermostAuthLayer) GetUsersList(userIDs []string, showEmail, showName bool) ([]*model.User, error) {
query := s.baseUserQuery(showEmail, showName).
Where(sq.Eq{"u.id": userIDs})
rows, err := query.Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
users, err := s.usersFromRows(rows)
if err != nil {
return nil, err
}
if len(users) != len(userIDs) {
return users, model.NewErrNotAllFound("user", userIDs)
}
return users, nil
}
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots, showEmail, showName bool) ([]*model.User, error) {
query := s.baseUserQuery(showEmail, showName).
Where(sq.Eq{"u.deleteAt": 0}).
Where(sq.Or{
sq.Like{"u.username": "%" + searchQuery + "%"},
sq.Like{"u.nickname": "%" + searchQuery + "%"},
sq.Like{"u.firstname": "%" + searchQuery + "%"},
sq.Like{"u.lastname": "%" + searchQuery + "%"},
}).
OrderBy("u.username").
Limit(10)
if excludeBots {
query = query.
Where(sq.Eq{"b.UserId IS NOT NULL": false})
}
if asGuestID == "" {
query = query.
Join("TeamMembers as tm ON tm.UserID = u.id").
Where(sq.Eq{"tm.TeamId": teamID})
} else {
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
if err != nil {
return nil, err
}
boardsIDs := []string{}
for _, board := range boards {
boardsIDs = append(boardsIDs, board.ID)
}
query = query.
Join(s.tablePrefix + "board_members as bm ON bm.user_id = u.ID").
Where(sq.Eq{"bm.board_id": boardsIDs})
}
rows, err := query.Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
users, err := s.usersFromRows(rows)
if err != nil {
return nil, err
}
return users, nil
}
func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
users := []*model.User{}
for rows.Next() {
var user model.User
err := rows.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.Nickname,
&user.FirstName,
&user.LastName,
&user.CreateAt,
&user.UpdateAt,
&user.DeleteAt,
&user.IsBot,
&user.IsGuest,
)
if err != nil {
return nil, err
}
users = append(users, &user)
}
return users, nil
}
func (s *MattermostAuthLayer) CloseRows(rows *sql.Rows) {
if err := rows.Close(); err != nil {
s.logger.Error("error closing MattermostAuthLayer row set", mlog.Err(err))
}
}
func (s *MattermostAuthLayer) CreatePrivateWorkspace(userID string) (string, error) {
// we emulate a private workspace by creating
// a DM channel from the user to themselves.
channel, err := s.servicesAPI.GetDirectChannel(userID, userID)
if err != nil {
s.logger.Error("error fetching private workspace", mlog.String("userID", userID), mlog.Err(err))
return "", err
}
return channel.Id, nil
}
func mmUserToFbUser(mmUser *mm_model.User) model.User {
authData := ""
if mmUser.AuthData != nil {
authData = *mmUser.AuthData
}
return model.User{
ID: mmUser.Id,
Username: mmUser.Username,
Email: mmUser.Email,
Password: mmUser.Password,
Nickname: mmUser.Nickname,
FirstName: mmUser.FirstName,
LastName: mmUser.LastName,
MfaSecret: mmUser.MfaSecret,
AuthService: mmUser.AuthService,
AuthData: authData,
CreateAt: mmUser.CreateAt,
UpdateAt: mmUser.UpdateAt,
DeleteAt: mmUser.DeleteAt,
IsBot: mmUser.IsBot,
IsGuest: mmUser.IsGuest(),
Roles: mmUser.Roles,
}
}
func (s *MattermostAuthLayer) GetFileInfo(id string) (*mm_model.FileInfo, error) {
fileInfo, err := s.servicesAPI.GetFileInfo(id)
if err != nil {
// Not finding fileinfo is fine because we don't have data for
// any existing files already uploaded in Boards before this code
// was deployed.
var appErr *mm_model.AppError
if errors.As(err, &appErr) {
if appErr.StatusCode == http.StatusNotFound {
return nil, model.NewErrNotFound("file info ID=" + id)
}
}
s.logger.Error("error fetching fileinfo",
mlog.String("id", id),
mlog.Err(err),
)
return nil, err
}
return fileInfo, nil
}
func (s *MattermostAuthLayer) SaveFileInfo(fileInfo *mm_model.FileInfo) error {
query := s.getQueryBuilder().
Insert("FileInfo").
Columns(
"Id",
"CreatorId",
"PostId",
"CreateAt",
"UpdateAt",
"DeleteAt",
"Path",
"ThumbnailPath",
"PreviewPath",
"Name",
"Extension",
"Size",
"MimeType",
"Width",
"Height",
"HasPreviewImage",
"MiniPreview",
"Content",
"RemoteId",
"Archived",
).
Values(
fileInfo.Id,
fileInfo.CreatorId,
fileInfo.PostId,
fileInfo.CreateAt,
fileInfo.UpdateAt,
fileInfo.DeleteAt,
fileInfo.Path,
fileInfo.ThumbnailPath,
fileInfo.PreviewPath,
fileInfo.Name,
fileInfo.Extension,
fileInfo.Size,
fileInfo.MimeType,
fileInfo.Width,
fileInfo.Height,
fileInfo.HasPreviewImage,
fileInfo.MiniPreview,
fileInfo.Content,
fileInfo.RemoteId,
false,
)
if _, err := query.Exec(); err != nil {
s.logger.Error(
"failed to save fileinfo",
mlog.String("file_name", fileInfo.Name),
mlog.Int64("size", fileInfo.Size),
mlog.Err(err),
)
return err
}
return nil
}
func (s *MattermostAuthLayer) GetLicense() *mm_model.License {
return s.servicesAPI.GetLicense()
}
func boardFields(prefix string) []string { //nolint:unparam
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"modified_by",
"type",
"minimum_role",
"title",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"create_at",
"update_at",
"delete_at",
}
if prefix == "" {
return fields
}
prefixedFields := make([]string, len(fields))
for i, field := range fields {
if strings.HasPrefix(field, "COALESCE(") {
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
} else {
prefixedFields[i] = prefix + field
}
}
return prefixedFields
}
func (s *MattermostAuthLayer) baseUserQuery(showEmail, showName bool) sq.SelectBuilder {
emailField := "''"
if showEmail {
emailField = "u.email"
}
firstNameField := "''"
lastNameField := "''"
if showName {
firstNameField = "u.firstname"
lastNameField = "u.lastname"
}
return s.getQueryBuilder().
Select(
"u.id",
"u.username",
emailField,
"u.nickname",
firstNameField,
lastNameField,
"u.CreateAt as create_at",
"u.UpdateAt as update_at",
"u.DeleteAt as delete_at",
"b.UserId IS NOT NULL AS is_bot",
"u.roles = 'system_guest' as is_guest",
).
From("Users as u").
LeftJoin("Bots b ON ( b.UserID = u.id )")
}
// SearchBoardsForUser returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *MattermostAuthLayer) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) {
// as we're joining three queries, we need to avoid numbered
// placeholders until the join is done, so we use the default
// question mark placeholder here
builder := s.getQueryBuilder().PlaceholderFormat(sq.Question)
boardMembersQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Join(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{
"b.is_template": false,
"bm.user_id": userID,
})
teamMembersQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Join("TeamMembers as tm on tm.teamid=b.team_id").
Where(sq.Eq{
"b.is_template": false,
"tm.userID": userID,
"tm.deleteAt": 0,
"b.type": model.BoardTypeOpen,
})
channelMembersQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Join("ChannelMembers as cm on cm.channelId=b.channel_id").
Where(sq.Eq{
"b.is_template": false,
"cm.userId": userID,
})
if term != "" {
if searchField == model.BoardSearchFieldPropertyName {
var where, whereTerm string
switch s.dbType {
case model.PostgresDBType:
where = "b.properties->? is not null"
whereTerm = term
case model.MysqlDBType:
where = "JSON_EXTRACT(b.properties, ?) IS NOT NULL"
whereTerm = "$." + term
default:
where = "b.properties LIKE ?"
whereTerm = "%\"" + term + "\"%"
}
boardMembersQ = boardMembersQ.Where(where, whereTerm)
teamMembersQ = teamMembersQ.Where(where, whereTerm)
channelMembersQ = channelMembersQ.Where(where, whereTerm)
} else { // model.BoardSearchFieldTitle
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
boardMembersQ = boardMembersQ.Where(conditions)
teamMembersQ = teamMembersQ.Where(conditions)
channelMembersQ = channelMembersQ.Where(conditions)
}
}
teamMembersSQL, teamMembersArgs, err := teamMembersQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUser error getting teamMembersSQL: %w", err)
}
channelMembersSQL, channelMembersArgs, err := channelMembersQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUser error getting channelMembersSQL: %w", err)
}
unionQ := boardMembersQ
user, err := s.GetUserByID(userID)
if err != nil {
return nil, err
}
// NOTE: theoretically, could do e.g. `isGuest := !includePublicBoards`
// but that introduces some tight coupling + fragility
if !user.IsGuest {
unionQ = unionQ.
Prefix("(").
Suffix(") UNION ("+channelMembersSQL+")", channelMembersArgs...)
if includePublicBoards {
unionQ = unionQ.Suffix(" UNION ("+teamMembersSQL+")", teamMembersArgs...)
}
} else if includePublicBoards {
unionQ = unionQ.
Prefix("(").
Suffix(") UNION ("+teamMembersSQL+")", teamMembersArgs...)
}
unionSQL, unionArgs, err := unionQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUser error getting unionSQL: %w", err)
}
// if we're using postgres, we need to replace the question mark
// placeholder with the numbered dollar one, now that the full
// query is built
if s.dbType == model.PostgresDBType {
var rErr error
unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL)
if rErr != nil {
return nil, fmt.Errorf("SearchBoardsForUser unable to replace unionSQL placeholders: %w", rErr)
}
}
rows, err := s.mmDB.Query(unionSQL, unionArgs...)
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows, false)
}
// searchBoardsForUserInTeam returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *MattermostAuthLayer) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {
// as we're joining three queries, we need to avoid numbered
// placeholders until the join is done, so we use the default
// question mark placeholder here
builder := s.getQueryBuilder().PlaceholderFormat(sq.Question)
openBoardsQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Where(sq.Eq{
"b.is_template": false,
"b.team_id": teamID,
"b.type": model.BoardTypeOpen,
})
memberBoardsQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards AS b").
Join(s.tablePrefix + "board_members AS bm on b.id = bm.board_id").
Where(sq.Eq{
"b.is_template": false,
"b.team_id": teamID,
"bm.user_id": userID,
})
channelMemberBoardsQ := builder.
Select(boardFields("b.")...).
From(s.tablePrefix + "boards AS b").
Join("ChannelMembers AS cm on cm.channelId = b.channel_id").
Where(sq.Eq{
"b.is_template": false,
"b.team_id": teamID,
"cm.userId": userID,
})
if term != "" {
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
openBoardsQ = openBoardsQ.Where(conditions)
memberBoardsQ = memberBoardsQ.Where(conditions)
channelMemberBoardsQ = channelMemberBoardsQ.Where(conditions)
}
memberBoardsSQL, memberBoardsArgs, err := memberBoardsQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting memberBoardsSQL: %w", err)
}
channelMemberBoardsSQL, channelMemberBoardsArgs, err := channelMemberBoardsQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting channelMemberBoardsSQL: %w", err)
}
unionQ := openBoardsQ.
Prefix("(").
Suffix(") UNION ("+memberBoardsSQL, memberBoardsArgs...).
Suffix(") UNION ("+channelMemberBoardsSQL+")", channelMemberBoardsArgs...)
unionSQL, unionArgs, err := unionQ.ToSql()
if err != nil {
return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting unionSQL: %w", err)
}
// if we're using postgres, we need to replace the question mark
// placeholder with the numbered dollar one, now that the full
// query is built
if s.dbType == model.PostgresDBType {
var rErr error
unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL)
if rErr != nil {
return nil, fmt.Errorf("SearchBoardsForUserInTeam unable to replace unionSQL placeholders: %w", rErr)
}
}
rows, err := s.mmDB.Query(unionSQL, unionArgs...)
if err != nil {
s.logger.Error(`searchBoardsForUserInTeam ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows, false)
}
func (s *MattermostAuthLayer) boardsFromRows(rows *sql.Rows, removeDuplicates bool) ([]*model.Board, error) {
boards := []*model.Board{}
idMap := make(map[string]struct{})
for rows.Next() {
var board model.Board
var propertiesBytes []byte
var cardPropertiesBytes []byte
err := rows.Scan(
&board.ID,
&board.TeamID,
&board.ChannelID,
&board.CreatedBy,
&board.ModifiedBy,
&board.Type,
&board.MinimumRole,
&board.Title,
&board.Description,
&board.Icon,
&board.ShowDescription,
&board.IsTemplate,
&board.TemplateVersion,
&propertiesBytes,
&cardPropertiesBytes,
&board.CreateAt,
&board.UpdateAt,
&board.DeleteAt,
)
if err != nil {
s.logger.Error("boardsFromRows scan error", mlog.Err(err))
return nil, err
}
if removeDuplicates {
if _, ok := idMap[board.ID]; ok {
continue
} else {
idMap[board.ID] = struct{}{}
}
}
err = json.Unmarshal(propertiesBytes, &board.Properties)
if err != nil {
s.logger.Error("board properties unmarshal error", mlog.Err(err))
return nil, err
}
err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties)
if err != nil {
s.logger.Error("board card properties unmarshal error", mlog.Err(err))
return nil, err
}
boards = append(boards, &board)
}
return boards, nil
}
func (s *MattermostAuthLayer) GetCloudLimits() (*mm_model.ProductLimits, error) {
return s.servicesAPI.GetCloudLimits()
}
func (s *MattermostAuthLayer) implicitBoardMembershipsFromRows(rows *sql.Rows) ([]*model.BoardMember, error) {
boardMembers := []*model.BoardMember{}
for rows.Next() {
var boardMember model.BoardMember
err := rows.Scan(
&boardMember.UserID,
&boardMember.BoardID,
)
if err != nil {
return nil, err
}
boardMember.Roles = "editor"
boardMember.SchemeEditor = true
boardMember.Synthetic = true
boardMembers = append(boardMembers, &boardMember)
}
return boardMembers, nil
}
func (s *MattermostAuthLayer) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) {
bm, originalErr := s.Store.GetMemberForBoard(boardID, userID)
// Explicit membership not found
if model.IsErrNotFound(originalErr) {
if userID == model.SystemUserID {
return nil, model.NewErrNotFound(userID)
}
var user *model.User
// No synthetic memberships for guests
user, err := s.GetUserByID(userID)
if err != nil {
return nil, err
}
if user.IsGuest {
return nil, model.NewErrNotFound("user is a guest")
}
b, boardErr := s.Store.GetBoard(boardID)
if boardErr != nil {
return nil, boardErr
}
if b.ChannelID != "" {
_, memberErr := s.servicesAPI.GetChannelMember(b.ChannelID, userID)
if memberErr != nil {
var appErr *mm_model.AppError
if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound {
// Plugin API returns error if channel member doesn't exist.
// We're fine if it doesn't exist, so its not an error for us.
message := fmt.Sprintf("member BoardID=%s UserID=%s", boardID, userID)
return nil, model.NewErrNotFound(message)
}
return nil, memberErr
}
return &model.BoardMember{
BoardID: boardID,
UserID: userID,
Roles: "editor",
SchemeAdmin: false,
SchemeEditor: true,
SchemeCommenter: false,
SchemeViewer: false,
Synthetic: true,
}, nil
}
if b.Type == model.BoardTypeOpen && b.IsTemplate {
_, memberErr := s.servicesAPI.GetTeamMember(b.TeamID, userID)
if memberErr != nil {
var appErr *mm_model.AppError
if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound {
return nil, model.NewErrNotFound(userID)
}
return nil, memberErr
}
return &model.BoardMember{
BoardID: boardID,
UserID: userID,
Roles: "viewer",
SchemeAdmin: false,
SchemeEditor: false,
SchemeCommenter: false,
SchemeViewer: true,
Synthetic: true,
}, nil
}
}
if originalErr != nil {
return nil, originalErr
}
return bm, nil
}
func (s *MattermostAuthLayer) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
explicitMembers, err := s.Store.GetMembersForUser(userID)
if err != nil {
s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err))
return nil, err
}
query := s.getQueryBuilder().
Select("CM.userID, B.Id").
From(s.tablePrefix + "boards AS B").
Join("ChannelMembers AS CM ON B.channel_id=CM.channelId").
Where(sq.Eq{"CM.userID": userID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
members := []*model.BoardMember{}
existingMembers := map[string]bool{}
for _, m := range explicitMembers {
members = append(members, m)
existingMembers[m.BoardID] = true
}
// No synthetic memberships for guests
user, err := s.GetUserByID(userID)
if err != nil {
return nil, err
}
if user.IsGuest {
return members, nil
}
implicitMembers, err := s.implicitBoardMembershipsFromRows(rows)
if err != nil {
return nil, err
}
for _, m := range implicitMembers {
if !existingMembers[m.BoardID] {
members = append(members, m)
}
}
return members, nil
}
func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
explicitMembers, err := s.Store.GetMembersForBoard(boardID)
if err != nil {
s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err))
return nil, err
}
query := s.getQueryBuilder().
Select("CM.userID, B.Id").
From(s.tablePrefix + "boards AS B").
Join("ChannelMembers AS CM ON B.channel_id=CM.channelId").
Join("Users as U on CM.userID = U.id").
LeftJoin("Bots as bo on U.id = bo.UserID").
Where(sq.Eq{"B.id": boardID}).
Where(sq.NotEq{"B.channel_id": ""}).
// Filter out guests as they don't have synthetic membership
Where(sq.NotEq{"U.roles": "system_guest"}).
Where(sq.Eq{"bo.UserId IS NOT NULL": false})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
implicitMembers, err := s.implicitBoardMembershipsFromRows(rows)
if err != nil {
return nil, err
}
members := []*model.BoardMember{}
existingMembers := map[string]bool{}
for _, m := range explicitMembers {
members = append(members, m)
existingMembers[m.UserID] = true
}
for _, m := range implicitMembers {
if !existingMembers[m.UserID] {
members = append(members, m)
}
}
return members, nil
}
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
if includePublicBoards {
boards, err := s.SearchBoardsForUserInTeam(teamID, "", userID)
if err != nil {
return nil, err
}
return boards, nil
}
// retrieve only direct memberships for user
// this is usually done for guests.
members, err := s.GetMembersForUser(userID)
if err != nil {
return nil, err
}
boardIDs := []string{}
for _, m := range members {
boardIDs = append(boardIDs, m.BoardID)
}
boards, err := s.Store.GetBoardsInTeamByIds(boardIDs, teamID)
if model.IsErrNotFound(err) {
if boards == nil {
boards = []*model.Board{}
}
return boards, nil
}
if err != nil {
return nil, err
}
return boards, nil
}
func (s *MattermostAuthLayer) SearchUserChannels(teamID, userID, query string) ([]*mm_model.Channel, error) {
channels, err := s.servicesAPI.GetChannelsForTeamForUser(teamID, userID, false)
if err != nil {
return nil, err
}
lowerQuery := strings.ToLower(query)
result := []*mm_model.Channel{}
count := 0
for _, channel := range channels {
if channel.Type != mm_model.ChannelTypeDirect &&
channel.Type != mm_model.ChannelTypeGroup &&
(strings.Contains(strings.ToLower(channel.Name), lowerQuery) || strings.Contains(strings.ToLower(channel.DisplayName), lowerQuery)) {
result = append(result, channel)
count++
if count >= 10 {
break
}
}
}
return result, nil
}
func (s *MattermostAuthLayer) GetChannel(teamID, channelID string) (*mm_model.Channel, error) {
channel, err := s.servicesAPI.GetChannelByID(channelID)
if err != nil {
return nil, err
}
return channel, nil
}
func (s *MattermostAuthLayer) getBoardsBotID() (string, error) {
if boardsBotID == "" {
var err error
boardsBotID, err = s.servicesAPI.EnsureBot(model.FocalboardBot)
if err != nil {
s.logger.Error("failed to ensure boards bot", mlog.Err(err))
return "", err
}
}
return boardsBotID, nil
}
func (s *MattermostAuthLayer) SendMessage(message, postType string, receipts []string) error {
botID, err := s.getBoardsBotID()
if err != nil {
return err
}
for _, receipt := range receipts {
channel, err := s.servicesAPI.GetDirectChannel(botID, receipt)
if err != nil {
s.logger.Error(
"failed to get DM channel between system bot and user for receipt",
mlog.String("receipt", receipt),
mlog.String("user_id", receipt),
mlog.Err(err),
)
continue
}
if err := s.PostMessage(message, postType, channel.Id); err != nil {
s.logger.Error(
"failed to send message to receipt from SendMessage",
mlog.String("receipt", receipt),
mlog.Err(err),
)
continue
}
}
return nil
}
func (s *MattermostAuthLayer) PostMessage(message, postType, channelID string) error {
botID, err := s.getBoardsBotID()
if err != nil {
return err
}
post := &mm_model.Post{
Message: message,
UserId: botID,
ChannelId: channelID,
Type: postType,
}
if _, err := s.servicesAPI.CreatePost(post); err != nil {
s.logger.Error(
"failed to send message to receipt from PostMessage",
mlog.Err(err),
)
}
return nil
}
func (s *MattermostAuthLayer) GetUserTimezone(userID string) (string, error) {
user, err := s.servicesAPI.GetUserByID(userID)
if err != nil {
return "", err
}
timezone := user.Timezone
return mm_model.GetPreferredTimezone(timezone), nil
}
func (s *MattermostAuthLayer) CanSeeUser(seerID string, seenID string) (bool, error) {
mmuser, appErr := s.servicesAPI.GetUserByID(seerID)
if appErr != nil {
return false, appErr
}
if !mmuser.IsGuest() {
return true, nil
}
query := s.getQueryBuilder().
Select("1").
From(s.tablePrefix + "board_members AS BM1").
Join(s.tablePrefix + "board_members AS BM2 ON BM1.BoardID=BM2.BoardID").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Or{
sq.And{
sq.Eq{"BM1.UserID": seerID},
sq.Eq{"BM2.UserID": seenID},
},
sq.And{
sq.Eq{"BM1.UserID": seenID},
sq.Eq{"BM2.UserID": seerID},
},
}).Limit(1)
rows, err := query.Query()
if err != nil {
return false, err
}
defer s.CloseRows(rows)
for rows.Next() {
return true, err
}
query = s.getQueryBuilder().
Select("1").
From("ChannelMembers AS CM1").
Join("ChannelMembers AS CM2 ON CM1.BoardID=CM2.BoardID").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Or{
sq.And{
sq.Eq{"CM1.UserID": seerID},
sq.Eq{"CM2.UserID": seenID},
},
sq.And{
sq.Eq{"CM1.UserID": seenID},
sq.Eq{"CM2.UserID": seerID},
},
}).Limit(1)
rows, err = query.Query()
if err != nil {
return false, err
}
defer s.CloseRows(rows)
for rows.Next() {
return true, err
}
return false, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
_ "github.com/lib/pq" // postgres driver
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
maxSearchDepth = 50
descClause = " DESC "
)
type ErrEmptyBoardID struct{}
func (re ErrEmptyBoardID) Error() string {
return "boardID is empty"
}
type ErrLimitExceeded struct{ max int }
func (le ErrLimitExceeded) Error() string {
return fmt.Sprintf("limit exceeded (max=%d)", le.max)
}
func (s *SQLStore) timestampToCharField(name string, as string) string {
switch s.dbType {
case model.MysqlDBType:
return fmt.Sprintf("date_format(%s, '%%Y-%%m-%%d %%H:%%i:%%S') AS %s", name, as)
case model.PostgresDBType:
return fmt.Sprintf("to_char(%s, 'YYYY-MM-DD HH:MI:SS.MS') AS %s", name, as)
default:
return fmt.Sprintf("%s AS %s", name, as)
}
}
func (s *SQLStore) blockFields(tableAlias string) []string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
return []string{
tableAlias + "id",
tableAlias + "parent_id",
tableAlias + "created_by",
tableAlias + "modified_by",
tableAlias + s.escapeField("schema"),
tableAlias + "type",
tableAlias + "title",
"COALESCE(" + tableAlias + "fields, '{}')",
s.timestampToCharField(tableAlias+"insert_at", "insertAt"),
tableAlias + "create_at",
tableAlias + "update_at",
tableAlias + "delete_at",
"COALESCE(" + tableAlias + "board_id, '0')",
}
}
func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks")
if opts.BoardID != "" {
query = query.Where(sq.Eq{"board_id": opts.BoardID})
}
if opts.ParentID != "" {
query = query.Where(sq.Eq{"parent_id": opts.ParentID})
}
if opts.BlockType != "" && opts.BlockType != model.TypeUnknown {
query = query.Where(sq.Eq{"type": opts.BlockType})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
query = query.Limit(uint64(opts.PerPage))
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBlocks ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
BlockType: model.BlockType(blockType),
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": ids})
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlocksByIDs ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
blocks, err := s.blocksFromRows(rows)
if err != nil {
return nil, err
}
if len(blocks) != len(ids) {
return blocks, model.NewErrNotAllFound("block", ids)
}
return blocks, nil
}
func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
BlockType: model.BlockType(blockType),
}
return s.getBlocks(db, opts)
}
// getSubTree2 returns blocks within 2 levels of the given blockID.
func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Or{sq.Eq{"id": blockID}, sq.Eq{"parent_id": blockID}}).
Where(sq.Eq{"board_id": boardID}).
OrderBy("insert_at, update_at")
if opts.BeforeUpdateAt != 0 {
query = query.Where(sq.LtOrEq{"update_at": opts.BeforeUpdateAt})
}
if opts.AfterUpdateAt != 0 {
query = query.Where(sq.GtOrEq{"update_at": opts.AfterUpdateAt})
}
if opts.Limit != 0 {
query = query.Limit(opts.Limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getSubTree ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
func (s *SQLStore) getBlocksForBoard(db sq.BaseRunner, boardID string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
results := []*model.Block{}
for rows.Next() {
var block model.Block
var fieldsJSON string
var modifiedBy sql.NullString
var insertAt sql.NullString
err := rows.Scan(
&block.ID,
&block.ParentID,
&block.CreatedBy,
&modifiedBy,
&block.Schema,
&block.Type,
&block.Title,
&fieldsJSON,
&insertAt,
&block.CreateAt,
&block.UpdateAt,
&block.DeleteAt,
&block.BoardID)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err))
return nil, err
}
if modifiedBy.Valid {
block.ModifiedBy = modifiedBy.String
}
err = json.Unmarshal([]byte(fieldsJSON), &block.Fields)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err))
return nil, err
}
results = append(results, &block)
}
return results, nil
}
func (s *SQLStore) insertBlock(db sq.BaseRunner, block *model.Block, userID string) error {
if block.BoardID == "" {
return ErrEmptyBoardID{}
}
fieldsJSON, err := json.Marshal(block.Fields)
if err != nil {
return err
}
existingBlock, err := s.getBlock(db, block.ID)
if err != nil && !model.IsErrNotFound(err) {
return err
}
block.UpdateAt = utils.GetMillis()
block.ModifiedBy = userID
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(
"channel_id",
"id",
"parent_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"fields",
"create_at",
"update_at",
"delete_at",
"board_id",
)
insertQueryValues := map[string]interface{}{
"channel_id": "",
"id": block.ID,
"parent_id": block.ParentID,
s.escapeField("schema"): block.Schema,
"type": block.Type,
"title": block.Title,
"fields": fieldsJSON,
"delete_at": block.DeleteAt,
"created_by": userID,
"modified_by": block.ModifiedBy,
"create_at": utils.GetMillis(),
"update_at": block.UpdateAt,
"board_id": block.BoardID,
}
if existingBlock != nil {
// block with ID exists, so this is an update operation
query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks").
Where(sq.Eq{"id": block.ID}).
Where(sq.Eq{"board_id": block.BoardID}).
Set("parent_id", block.ParentID).
Set("modified_by", block.ModifiedBy).
Set(s.escapeField("schema"), block.Schema).
Set("type", block.Type).
Set("title", block.Title).
Set("fields", fieldsJSON).
Set("update_at", block.UpdateAt).
Set("delete_at", block.DeleteAt)
if _, err := query.Exec(); err != nil {
s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err))
return err
}
} else {
block.CreatedBy = userID
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks")
if _, err := query.Exec(); err != nil {
return err
}
}
// writing block history
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history")
if _, err := query.Exec(); err != nil {
return err
}
return nil
}
func (s *SQLStore) patchBlock(db sq.BaseRunner, blockID string, blockPatch *model.BlockPatch, userID string) error {
existingBlock, err := s.getBlock(db, blockID)
if err != nil {
return err
}
block := blockPatch.Patch(existingBlock)
return s.insertBlock(db, block, userID)
}
func (s *SQLStore) patchBlocks(db sq.BaseRunner, blockPatches *model.BlockPatchBatch, userID string) error {
for i, blockID := range blockPatches.BlockIDs {
err := s.patchBlock(db, blockID, &blockPatches.BlockPatches[i], userID)
if err != nil {
return err
}
}
return nil
}
func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []*model.Block, userID string) error {
for _, block := range blocks {
if block.BoardID == "" {
return ErrEmptyBoardID{}
}
}
for i := range blocks {
err := s.insertBlock(db, blocks[i], userID)
if err != nil {
return err
}
}
return nil
}
func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error {
return s.deleteBlockAndChildren(db, blockID, modifiedBy, false)
}
func (s *SQLStore) deleteBlockAndChildren(db sq.BaseRunner, blockID string, modifiedBy string, keepChildren bool) error {
block, err := s.getBlock(db, blockID)
if model.IsErrNotFound(err) {
s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID))
return nil // deleting non-exiting block is not considered an error (for now)
}
if err != nil {
return err
}
fieldsJSON, err := json.Marshal(block.Fields)
if err != nil {
return err
}
now := utils.GetMillis()
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix+"blocks_history").
Columns(
"board_id",
"id",
"parent_id",
s.escapeField("schema"),
"type",
"title",
"fields",
"modified_by",
"create_at",
"update_at",
"delete_at",
"created_by",
).
Values(
block.BoardID,
block.ID,
block.ParentID,
block.Schema,
block.Type,
block.Title,
fieldsJSON,
modifiedBy,
block.CreateAt,
now,
now,
block.CreatedBy,
)
if _, err := insertQuery.Exec(); err != nil {
return err
}
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "blocks").
Where(sq.Eq{"id": blockID})
if _, err := deleteQuery.Exec(); err != nil {
return err
}
if keepChildren {
return nil
}
return s.deleteBlockChildren(db, block.BoardID, block.ID, modifiedBy)
}
func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error {
blocks, err := s.getBlockHistory(db, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return err
}
if len(blocks) == 0 {
s.logger.Warn("undeleteBlock block not found", mlog.String("block_id", blockID))
return nil // undeleting non-exiting block is not considered an error (for now)
}
block := blocks[0]
if block.DeleteAt == 0 {
s.logger.Warn("undeleteBlock block not deleted", mlog.String("block_id", block.ID))
return nil // undeleting not deleted block is not considered an error (for now)
}
fieldsJSON, err := json.Marshal(block.Fields)
if err != nil {
return err
}
now := utils.GetMillis()
columns := []string{
"board_id",
"channel_id",
"id",
"parent_id",
s.escapeField("schema"),
"type",
"title",
"fields",
"modified_by",
"create_at",
"update_at",
"delete_at",
"created_by",
}
values := []interface{}{
block.BoardID,
"",
block.ID,
block.ParentID,
block.Schema,
block.Type,
block.Title,
fieldsJSON,
modifiedBy,
block.CreateAt,
now,
0,
block.CreatedBy,
}
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks_history").
Columns(columns...).
Values(values...)
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks").
Columns(columns...).
Values(values...)
if _, err := insertHistoryQuery.Exec(); err != nil {
return err
}
if _, err := insertQuery.Exec(); err != nil {
return err
}
return s.undeleteBlockChildren(db, block.BoardID, block.ID, modifiedBy)
}
func (s *SQLStore) getBlockCountsByType(db sq.BaseRunner) (map[string]int64, error) {
query := s.getQueryBuilder(db).
Select(
"type",
"COUNT(*) AS count",
).
From(s.tablePrefix + "blocks").
GroupBy("type")
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlockCountsByType ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
m := make(map[string]int64)
for rows.Next() {
var blockType string
var count int64
err := rows.Scan(&blockType, &count)
if err != nil {
s.logger.Error("Failed to fetch block count", mlog.Err(err))
return nil, err
}
m[blockType] = count
}
return m, nil
}
func (s *SQLStore) getBoardCount(db sq.BaseRunner) (int64, error) {
query := s.getQueryBuilder(db).
Select("COUNT(*) AS count").
From(s.tablePrefix + "boards").
Where(sq.Eq{"delete_at": 0}).
Where(sq.Eq{"is_template": false})
row := query.QueryRow()
var count int64
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": blockID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlock ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
blocks, err := s.blocksFromRows(rows)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
return nil, model.NewErrNotFound("block ID=" + blockID)
}
return blocks[0], nil
}
func (s *SQLStore) getBlockHistory(db sq.BaseRunner, blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
var order string
if opts.Descending {
order = descClause
}
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks_history").
Where(sq.Eq{"id": blockID}).
OrderBy("insert_at " + order + ", update_at" + order)
if opts.BeforeUpdateAt != 0 {
query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt})
}
if opts.AfterUpdateAt != 0 {
query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt})
}
if opts.Limit != 0 {
query = query.Limit(opts.Limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlockHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
var order string
if opts.Descending {
order = descClause
}
query := s.getQueryBuilder(db).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks_history").
Where(sq.Eq{"board_id": boardID}).
OrderBy("insert_at " + order + ", update_at" + order)
if opts.BeforeUpdateAt != 0 {
query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt})
}
if opts.AfterUpdateAt != 0 {
query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt})
}
if opts.Limit != 0 {
query = query.Limit(opts.Limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlockHistoryDescendants ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
// getBlockHistoryNewestChildren returns the newest (latest) version child blocks for the
// specified parent from the blocks_history table. This includes any deleted children.
func (s *SQLStore) getBlockHistoryNewestChildren(db sq.BaseRunner, parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
// as we're joining 2 queries, we need to avoid numbered
// placeholders until the join is done, so we use the default
// question mark placeholder here
builder := s.getQueryBuilder(db).PlaceholderFormat(sq.Question)
sub := builder.
Select("bh2.id", "MAX(bh2.insert_at) AS max_insert_at").
From(s.tablePrefix + "blocks_history AS bh2").
Where(sq.Eq{"bh2.parent_id": parentID}).
GroupBy("bh2.id")
if opts.AfterUpdateAt != 0 {
sub = sub.Where(sq.Gt{"bh2.update_at": opts.AfterUpdateAt})
}
if opts.BeforeUpdateAt != 0 {
sub = sub.Where(sq.Lt{"bh2.update_at": opts.BeforeUpdateAt})
}
subQuery, subArgs, err := sub.ToSql()
if err != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate subquery: %w", err)
}
query := s.getQueryBuilder(db).
Select(s.blockFields("bh")...).
From(s.tablePrefix+"blocks_history AS bh").
InnerJoin("("+subQuery+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", subArgs...)
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// limit+1 to detect if more records available
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate sql: %w", err)
}
// if we're using postgres, we need to replace the question mark
// placeholder with the numbered dollar one, now that the full
// query is built
if s.dbType == model.PostgresDBType {
var rErr error
sql, rErr = sq.Dollar.ReplacePlaceholders(sql)
if rErr != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to replace sql placeholders: %w", rErr)
}
}
rows, err := db.Query(sql, args...)
if err != nil {
s.logger.Error(`getBlockHistoryNewestChildren ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
blocks, err := s.blocksFromRows(rows)
if err != nil {
return nil, false, err
}
hasMore := false
if opts.PerPage > 0 && len(blocks) > opts.PerPage {
blocks = blocks[:opts.PerPage]
hasMore = true
}
return blocks, hasMore, nil
}
// getBoardAndCardByID returns the first parent of type `card` and first parent of type `board` for the block specified by ID.
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board *model.Board, card *model.Block, err error) {
// use block_history to fetch block in case it was deleted and no longer exists in blocks table.
opts := model.QueryBlockHistoryOptions{
Limit: 1,
Descending: true,
}
blocks, err := s.getBlockHistory(db, blockID, opts)
if err != nil {
return nil, nil, err
}
if len(blocks) == 0 {
return nil, nil, model.NewErrNotFound("block history BlockID=" + blockID)
}
return s.getBoardAndCard(db, blocks[0])
}
// getBoardAndCard returns the first parent of type `card` and and the `board` for the specified block.
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
func (s *SQLStore) getBoardAndCard(db sq.BaseRunner, block *model.Block) (board *model.Board, card *model.Block, err error) {
var count int // don't let invalid blocks hierarchy cause infinite loop.
iter := block
// use block_history to fetch blocks in case they were deleted and no longer exist in blocks table.
opts := model.QueryBlockHistoryOptions{
Limit: 1,
Descending: true,
}
for {
count++
if card == nil && iter.Type == model.TypeCard {
card = iter
}
if iter.ParentID == "" || card != nil || count > maxSearchDepth {
break
}
blocks, err2 := s.getBlockHistory(db, iter.ParentID, opts)
if err2 != nil {
return nil, nil, err2
}
if len(blocks) == 0 {
return board, card, nil
}
iter = blocks[0]
}
board, err = s.getBoard(db, block.BoardID)
if err != nil {
return nil, nil, err
}
return board, card, nil
}
func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceID string) error {
runUpdateForBlocksAndHistory := func(query sq.UpdateBuilder) error {
if _, err := query.Table(s.tablePrefix + "blocks").Exec(); err != nil {
return err
}
if _, err := query.Table(s.tablePrefix + "blocks_history").Exec(); err != nil {
return err
}
return nil
}
baseQuery := s.getQueryBuilder(db).
Where(sq.Eq{"workspace_id": workspaceID})
// update ID
updateIDQ := baseQuery.Update("").
Set("id", newID).
Where(sq.Eq{"id": currentID})
if errID := runUpdateForBlocksAndHistory(updateIDQ); errID != nil {
s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errID))
return errID
}
// update BoardID
updateBoardIDQ := baseQuery.Update("").
Set("board_id", newID).
Where(sq.Eq{"board_id": currentID})
if errBoardID := runUpdateForBlocksAndHistory(updateBoardIDQ); errBoardID != nil {
s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errBoardID))
return errBoardID
}
// update ParentID
updateParentIDQ := baseQuery.Update("").
Set("parent_id", newID).
Where(sq.Eq{"parent_id": currentID})
if errParentID := runUpdateForBlocksAndHistory(updateParentIDQ); errParentID != nil {
s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errParentID))
return errParentID
}
// update parent contentOrder
updateContentOrder := baseQuery.Update("")
if s.dbType == model.PostgresDBType {
updateContentOrder = updateContentOrder.
Set("fields", sq.Expr("REPLACE(fields::text, ?, ?)::json", currentID, newID)).
Where(sq.Like{"fields->>'contentOrder'": "%" + currentID + "%"}).
Where(sq.Eq{"type": model.TypeCard})
} else {
updateContentOrder = updateContentOrder.
Set("fields", sq.Expr("REPLACE(fields, ?, ?)", currentID, newID)).
Where(sq.Like{"fields": "%" + currentID + "%"}).
Where(sq.Eq{"type": model.TypeCard})
}
if errParentID := runUpdateForBlocksAndHistory(updateContentOrder); errParentID != nil {
s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errParentID))
return errParentID
}
return nil
}
func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) {
blocks, err := s.getSubTree2(db, boardID, blockID, model.QuerySubtreeOptions{})
if err != nil {
return nil, err
}
if len(blocks) == 0 {
message := fmt.Sprintf("block subtree BoardID=%s BlockID=%s", boardID, blockID)
return nil, model.NewErrNotFound(message)
}
var rootBlock *model.Block
allBlocks := []*model.Block{}
for _, block := range blocks {
if block.Type == model.TypeComment {
continue
}
if block.ID == blockID {
if block.Fields == nil {
block.Fields = make(map[string]interface{})
}
block.Fields["isTemplate"] = asTemplate
rootBlock = block
} else {
allBlocks = append(allBlocks, block)
}
}
allBlocks = append([]*model.Block{rootBlock}, allBlocks...)
allBlocks = model.GenerateBlockIDs(allBlocks, nil)
if err := s.insertBlocks(db, allBlocks, userID); err != nil {
return nil, err
}
return allBlocks, nil
}
func (s *SQLStore) deleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error {
now := utils.GetMillis()
selectQuery := s.getQueryBuilder(db).
Select(
"board_id",
"id",
"parent_id",
s.escapeField("schema"),
"type",
"title",
"fields",
"'"+modifiedBy+"'",
"create_at",
s.castInt(now, "update_at"),
s.castInt(now, "delete_at"),
"created_by",
).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"board_id": boardID})
if parentID != "" {
selectQuery = selectQuery.Where(sq.Eq{"parent_id": parentID})
}
insertQuery := s.getQueryBuilder(db).
Insert(s.tablePrefix+"blocks_history").
Columns(
"board_id",
"id",
"parent_id",
s.escapeField("schema"),
"type",
"title",
"fields",
"modified_by",
"create_at",
"update_at",
"delete_at",
"created_by",
).Select(selectQuery)
if _, err := insertQuery.Exec(); err != nil {
return err
}
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "blocks").
Where(sq.Eq{"board_id": boardID})
if parentID != "" {
deleteQuery = deleteQuery.Where(sq.Eq{"parent_id": parentID})
}
if _, err := deleteQuery.Exec(); err != nil {
return err
}
return nil
}
func (s *SQLStore) undeleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error {
if boardID == "" {
return ErrEmptyBoardID{}
}
where := fmt.Sprintf("board_id='%s'", boardID)
if parentID != "" {
where += fmt.Sprintf(" AND parent_id='%s'", parentID)
}
selectQuery := s.getQueryBuilder(db).
Select(
"bh.board_id",
"'' AS channel_id",
"bh.id",
"bh.parent_id",
"bh.schema",
"bh.type",
"bh.title",
"bh.fields",
"'"+modifiedBy+"' AS modified_by",
"bh.create_at",
s.castInt(utils.GetMillis(), "update_at"),
s.castInt(0, "delete_at"),
"bh.created_by",
).
From(fmt.Sprintf(`
%sblocks_history AS bh,
(SELECT id, max(insert_at) AS max_insert_at FROM %sblocks_history WHERE %s GROUP BY id) AS sub`,
s.tablePrefix, s.tablePrefix, where)).
Where("bh.id=sub.id").
Where("bh.insert_at=sub.max_insert_at").
Where(sq.NotEq{"bh.delete_at": 0})
columns := []string{
"board_id",
"channel_id",
"id",
"parent_id",
s.escapeField("schema"),
"type",
"title",
"fields",
"modified_by",
"create_at",
"update_at",
"delete_at",
"created_by",
}
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks").
Columns(columns...).
Select(selectQuery)
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks_history").
Columns(columns...).
Select(selectQuery)
sql, args, err := insertQuery.ToSql()
s.logger.Trace("undeleteBlockChildren - insertQuery",
mlog.String("sql", sql),
mlog.Array("args", args),
mlog.Err(err),
)
sql, args, err = insertHistoryQuery.ToSql()
s.logger.Trace("undeleteBlockChildren - insertHistoryQuery",
mlog.String("sql", sql),
mlog.Array("args", args),
mlog.Err(err),
)
// insert into blocks table must happen before history table, otherwise the history
// table will be changed and the second query will fail to find the same records.
result, err := insertQuery.Exec()
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
s.logger.Debug("undeleteBlockChildren - insertQuery", mlog.Int64("rows_affected", rowsAffected))
result, err = insertHistoryQuery.Exec()
if err != nil {
return err
}
rowsAffected, _ = result.RowsAffected()
s.logger.Debug("undeleteBlockChildren - insertHistoryQuery", mlog.Int64("rows_affected", rowsAffected))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
//nolint:gosec
"crypto/md5"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func boardFields(tableAlias string) []string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
return []string{
tableAlias + "id",
tableAlias + "team_id",
"COALESCE(" + tableAlias + "channel_id, '')",
"COALESCE(" + tableAlias + "created_by, '')",
tableAlias + "modified_by",
tableAlias + "type",
tableAlias + "minimum_role",
tableAlias + "title",
tableAlias + "description",
tableAlias + "icon",
tableAlias + "show_description",
tableAlias + "is_template",
tableAlias + "template_version",
"COALESCE(" + tableAlias + "properties, '{}')",
"COALESCE(" + tableAlias + "card_properties, '[]')",
tableAlias + "create_at",
tableAlias + "update_at",
tableAlias + "delete_at",
}
}
func boardHistoryFields() []string {
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"COALESCE(modified_by, '')",
"type",
"minimum_role",
"COALESCE(title, '')",
"COALESCE(description, '')",
"COALESCE(icon, '')",
"COALESCE(show_description, false)",
"COALESCE(is_template, false)",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"COALESCE(create_at, 0)",
"COALESCE(update_at, 0)",
"COALESCE(delete_at, 0)",
}
return fields
}
var boardMemberFields = []string{
"COALESCE(B.minimum_role, '')",
"BM.board_id",
"BM.user_id",
"BM.roles",
"BM.scheme_admin",
"BM.scheme_editor",
"BM.scheme_commenter",
"BM.scheme_viewer",
}
func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) {
boards := []*model.Board{}
for rows.Next() {
var board model.Board
var propertiesBytes []byte
var cardPropertiesBytes []byte
err := rows.Scan(
&board.ID,
&board.TeamID,
&board.ChannelID,
&board.CreatedBy,
&board.ModifiedBy,
&board.Type,
&board.MinimumRole,
&board.Title,
&board.Description,
&board.Icon,
&board.ShowDescription,
&board.IsTemplate,
&board.TemplateVersion,
&propertiesBytes,
&cardPropertiesBytes,
&board.CreateAt,
&board.UpdateAt,
&board.DeleteAt,
)
if err != nil {
s.logger.Error("boardsFromRows scan error", mlog.Err(err))
return nil, err
}
err = json.Unmarshal(propertiesBytes, &board.Properties)
if err != nil {
s.logger.Error("board properties unmarshal error", mlog.Err(err))
return nil, err
}
err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties)
if err != nil {
s.logger.Error("board card properties unmarshal error", mlog.Err(err))
return nil, err
}
boards = append(boards, &board)
}
return boards, nil
}
func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, error) {
boardMembers := []*model.BoardMember{}
for rows.Next() {
var boardMember model.BoardMember
err := rows.Scan(
&boardMember.MinimumRole,
&boardMember.BoardID,
&boardMember.UserID,
&boardMember.Roles,
&boardMember.SchemeAdmin,
&boardMember.SchemeEditor,
&boardMember.SchemeCommenter,
&boardMember.SchemeViewer,
)
if err != nil {
return nil, err
}
boardMembers = append(boardMembers, &boardMember)
}
return boardMembers, nil
}
func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) {
boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{}
for rows.Next() {
var boardMemberHistoryEntry model.BoardMemberHistoryEntry
var insertAt sql.NullString
err := rows.Scan(
&boardMemberHistoryEntry.BoardID,
&boardMemberHistoryEntry.UserID,
&boardMemberHistoryEntry.Action,
&insertAt,
)
if err != nil {
return nil, err
}
// parse the insert_at timestamp which is different based on database type.
dateTemplate := "2006-01-02T15:04:05Z0700"
if s.dbType == model.MysqlDBType {
dateTemplate = "2006-01-02 15:04:05.000000"
}
ts, err := time.Parse(dateTemplate, insertAt.String)
if err != nil {
return nil, fmt.Errorf("cannot parse datetime '%s' for board_members_history scan: %w", insertAt.String, err)
}
boardMemberHistoryEntry.InsertAt = ts
boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry)
}
return boardMemberHistoryEntries, nil
}
func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) {
boards, err := s.getBoardsByCondition(db, conditions...)
if err != nil {
return nil, err
}
return boards[0], nil
}
func (s *SQLStore) getBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) {
return s.getBoardsFieldsByCondition(db, boardFields(""), conditions...)
}
func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string, conditions ...interface{}) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(fields...).
From(s.tablePrefix + "boards")
for _, c := range conditions {
query = query.Where(c)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsFieldsByCondition ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
if len(boards) == 0 {
return nil, model.NewErrNotFound("boards")
}
return boards, nil
}
func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, error) {
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
}
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.Eq{"bm.user_id": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
})
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsForUserAndTeam ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, teamID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Eq{"b.id": boardIDs})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsInTeamByIds ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
if len(boards) != len(boardIDs) {
s.logger.Warn("getBoardsInTeamByIds mismatched number of boards found",
mlog.Int("len(boards)", len(boards)),
mlog.Int("len(boardIDs)", len(boardIDs)),
)
return boards, model.NewErrNotAllFound("board", boardIDs)
}
return boards, nil
}
func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) {
// Generate tracking IDs for in-built templates
if board.IsTemplate && board.TeamID == model.GlobalTeamID {
//nolint:gosec
// we don't need cryptographically secure hash, so MD5 is fine
board.Properties["trackingTemplateId"] = fmt.Sprintf("%x", md5.Sum([]byte(board.Title)))
}
propertiesBytes, err := s.MarshalJSONB(board.Properties)
if err != nil {
s.logger.Error(
"failed to marshal board.Properties",
mlog.String("board_id", board.ID),
mlog.String("board.Properties", fmt.Sprintf("%v", board.Properties)),
mlog.Err(err),
)
return nil, err
}
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
s.logger.Error(
"failed to marshal board.CardProperties",
mlog.String("board_id", board.ID),
mlog.String("board.CardProperties", fmt.Sprintf("%v", board.CardProperties)),
mlog.Err(err),
)
return nil, err
}
existingBoard, err := s.getBoard(db, board.ID)
if err != nil && !model.IsErrNotFound(err) {
return nil, fmt.Errorf("insertBoard error occurred while fetching existing board %s: %w", board.ID, err)
}
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(boardFields("")...)
now := utils.GetMillis()
board.ModifiedBy = userID
board.UpdateAt = now
insertQueryValues := map[string]interface{}{
"id": board.ID,
"team_id": board.TeamID,
"channel_id": board.ChannelID,
"created_by": board.CreatedBy,
"modified_by": board.ModifiedBy,
"type": board.Type,
"title": board.Title,
"minimum_role": board.MinimumRole,
"description": board.Description,
"icon": board.Icon,
"show_description": board.ShowDescription,
"is_template": board.IsTemplate,
"template_version": board.TemplateVersion,
"properties": propertiesBytes,
"card_properties": cardPropertiesBytes,
"create_at": board.CreateAt,
"update_at": board.UpdateAt,
"delete_at": board.DeleteAt,
}
if existingBoard != nil {
query := s.getQueryBuilder(db).Update(s.tablePrefix+"boards").
Where(sq.Eq{"id": board.ID}).
Set("modified_by", board.ModifiedBy).
Set("type", board.Type).
Set("channel_id", board.ChannelID).
Set("minimum_role", board.MinimumRole).
Set("title", board.Title).
Set("description", board.Description).
Set("icon", board.Icon).
Set("show_description", board.ShowDescription).
Set("is_template", board.IsTemplate).
Set("template_version", board.TemplateVersion).
Set("properties", propertiesBytes).
Set("card_properties", cardPropertiesBytes).
Set("update_at", board.UpdateAt).
Set("delete_at", board.DeleteAt)
if _, err := query.Exec(); err != nil {
s.logger.Error(`InsertBoard error occurred while updating existing board`, mlog.String("boardID", board.ID), mlog.Err(err))
return nil, fmt.Errorf("insertBoard error occurred while updating existing board %s: %w", board.ID, err)
}
} else {
board.CreatedBy = userID
board.CreateAt = now
insertQueryValues["created_by"] = board.CreatedBy
insertQueryValues["create_at"] = board.CreateAt
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards")
if _, err := query.Exec(); err != nil {
return nil, fmt.Errorf("insertBoard error occurred while inserting board %s: %w", board.ID, err)
}
}
// writing board history
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to insert board history", mlog.String("board_id", board.ID), mlog.Err(err))
return nil, fmt.Errorf("failed to insert board %s history: %w", board.ID, err)
}
return board, nil
}
func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) {
existingBoard, err := s.getBoard(db, boardID)
if err != nil {
return nil, err
}
board := boardPatch.Patch(existingBoard)
return s.insertBoard(db, board, userID)
}
func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
return s.deleteBoardAndChildren(db, boardID, userID, false)
}
func (s *SQLStore) deleteBoardAndChildren(db sq.BaseRunner, boardID, userID string, keepChildren bool) error {
now := utils.GetMillis()
board, err := s.getBoard(db, boardID)
if err != nil {
return err
}
propertiesBytes, err := s.MarshalJSONB(board.Properties)
if err != nil {
return err
}
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
return err
}
insertQueryValues := map[string]interface{}{
"id": board.ID,
"team_id": board.TeamID,
"channel_id": board.ChannelID,
"created_by": board.CreatedBy,
"modified_by": userID,
"type": board.Type,
"minimum_role": board.MinimumRole,
"title": board.Title,
"description": board.Description,
"icon": board.Icon,
"show_description": board.ShowDescription,
"is_template": board.IsTemplate,
"template_version": board.TemplateVersion,
"properties": propertiesBytes,
"card_properties": cardPropertiesBytes,
"create_at": board.CreateAt,
"update_at": now,
"delete_at": now,
}
// writing board history
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(boardHistoryFields()...)
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
if _, err := query.Exec(); err != nil {
return err
}
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "boards").
Where(sq.Eq{"id": boardID}).
Where(sq.Eq{"COALESCE(team_id, '0')": board.TeamID})
if _, err := deleteQuery.Exec(); err != nil {
return err
}
if keepChildren {
return nil
}
return s.deleteBlockChildren(db, boardID, "", userID)
}
func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) {
newBoard, err := s.insertBoard(db, board, userID)
if err != nil {
return nil, nil, err
}
bm := &model.BoardMember{
BoardID: newBoard.ID,
UserID: newBoard.CreatedBy,
SchemeAdmin: true,
SchemeEditor: true,
}
nbm, err := s.saveMember(db, bm)
if err != nil {
return nil, nil, fmt.Errorf("cannot save member %s while inserting board %s: %w", bm.UserID, bm.BoardID, err)
}
return newBoard, nbm, nil
}
func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.BoardMember, error) {
queryValues := map[string]interface{}{
"board_id": bm.BoardID,
"user_id": bm.UserID,
"roles": "",
"scheme_admin": bm.SchemeAdmin,
"scheme_editor": bm.SchemeEditor,
"scheme_commenter": bm.SchemeCommenter,
"scheme_viewer": bm.SchemeViewer,
}
oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix + "board_members").
SetMap(queryValues)
if s.dbType == model.MysqlDBType {
query = query.Suffix(
"ON DUPLICATE KEY UPDATE scheme_admin = ?, scheme_editor = ?, scheme_commenter = ?, scheme_viewer = ?",
bm.SchemeAdmin, bm.SchemeEditor, bm.SchemeCommenter, bm.SchemeViewer)
} else {
query = query.Suffix(
`ON CONFLICT (board_id, user_id)
DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor,
scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`,
)
}
if _, err := query.Exec(); err != nil {
return nil, err
}
if oldMember == nil {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action").
Values(bm.BoardID, bm.UserID, "created")
if _, err := addToMembersHistory.Exec(); err != nil {
return nil, err
}
}
return bm, nil
}
func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error {
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "board_members").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID})
result, err := deleteQuery.Exec()
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected > 0 {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action").
Values(boardID, userID, "deleted")
if _, err := addToMembersHistory.Exec(); err != nil {
return err
}
}
return nil
}
func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID}).
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMemberForBoard ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
members, err := s.boardMembersFromRows(rows)
if err != nil {
return nil, err
}
if len(members) == 0 {
message := fmt.Sprintf("board member BoardID=%s UserID=%s", boardID, userID)
return nil, model.NewErrNotFound(message)
}
return members[0], nil
}
func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
members, err := s.boardMembersFromRows(rows)
if err != nil {
return nil, err
}
return members, nil
}
func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardMembersFromRows(rows)
}
// searchBoardsForUser returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.Eq{"bm.user_id": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
})
}
if term != "" {
if searchField == model.BoardSearchFieldPropertyName {
switch s.dbType {
case model.PostgresDBType:
where := "b.properties->? is not null"
query = query.Where(where, term)
case model.MysqlDBType:
where := "JSON_EXTRACT(b.properties, ?) IS NOT NULL"
query = query.Where(where, "$."+term)
default:
where := "b.properties LIKE ?"
query = query.Where(where, "%\""+term+"\"%")
}
} else { // model.BoardSearchFieldTitle
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
query = query.Where(conditions)
}
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
// searchBoardsForUserInTeam returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *SQLStore) searchBoardsForUserInTeam(db sq.BaseRunner, teamID, term, userID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.is_template": false}).
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.And{
sq.Eq{"b.type": model.BoardTypePrivate},
sq.Eq{"bm.user_id": userID},
},
})
if term != "" {
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
query = query.Where(conditions)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) getBoardHistory(db sq.BaseRunner, boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) {
var order string
if opts.Descending {
order = " DESC "
}
query := s.getQueryBuilder(db).
Select(boardHistoryFields()...).
From(s.tablePrefix + "boards_history").
Where(sq.Eq{"id": boardID}).
OrderBy("insert_at " + order + ", update_at" + order)
if opts.BeforeUpdateAt != 0 {
query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt})
}
if opts.AfterUpdateAt != 0 {
query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt})
}
if opts.Limit != 0 {
query = query.Limit(opts.Limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy string) error {
boards, err := s.getBoardHistory(db, boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return err
}
if len(boards) == 0 {
s.logger.Warn("undeleteBlock board not found", mlog.String("board_id", boardID))
return nil // undeleting non-existing board is not considered an error (for now)
}
board := boards[0]
if board.DeleteAt == 0 {
s.logger.Warn("undeleteBlock board not deleted", mlog.String("board_id", board.ID))
return nil // undeleting not deleted board is not considered an error (for now)
}
propertiesJSON, err := s.MarshalJSONB(board.Properties)
if err != nil {
return err
}
cardPropertiesJSON, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
return err
}
now := utils.GetMillis()
columns := []string{
"id",
"team_id",
"channel_id",
"created_by",
"modified_by",
"type",
"title",
"minimum_role",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"properties",
"card_properties",
"create_at",
"update_at",
"delete_at",
}
values := []interface{}{
board.ID,
board.TeamID,
"",
board.CreatedBy,
modifiedBy,
board.Type,
board.Title,
board.MinimumRole,
board.Description,
board.Icon,
board.ShowDescription,
board.IsTemplate,
board.TemplateVersion,
propertiesJSON,
cardPropertiesJSON,
board.CreateAt,
now,
0,
}
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards_history").
Columns(columns...).
Values(values...)
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards").
Columns(columns...).
Values(values...)
if _, err := insertHistoryQuery.Exec(); err != nil {
return err
}
if _, err := insertQuery.Exec(); err != nil {
return err
}
return s.undeleteBlockChildren(db, board.ID, "", modifiedBy)
}
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
query := s.getQueryBuilder(db).
Select("board_id", "user_id", "action", "insert_at").
From(s.tablePrefix + "board_members_history").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID}).
OrderBy("insert_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows)
if err != nil {
return nil, err
}
return memberHistory, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
sq "github.com/Masterminds/squirrel"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getTeamBoardsInsights(db sq.BaseRunner, teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
boardsHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by").
From(s.tablePrefix + "boards_history as boards_history").
Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id").
Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"boards_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, boards_history.id, boards_history.modified_by")
blocksHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by").
Prefix("UNION ALL").
From(s.tablePrefix + "blocks_history as blocks_history").
Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id").
Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"blocks_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by")
boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery)
insightsQuery := s.getQueryBuilder(db).Select(
fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")),
).
FromSelect(boardsActivity, "boards_and_blocks_history").
GroupBy("id, title, icon, created_by").
OrderBy("activity_count desc").
Offset(uint64(offset)).
Limit(uint64(limit))
rows, err := insightsQuery.Query()
if err != nil {
s.logger.Error(`Team insights query ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boardsInsights, err := boardsInsightsFromRows(rows)
if err != nil {
return nil, err
}
boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit)
return boardInsightsPaginated, nil
}
func (s *SQLStore) getUserBoardsInsights(db sq.BaseRunner, teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
boardsHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by").
From(s.tablePrefix + "boards_history as boards_history").
Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id").
Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"boards_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, boards_history.id, boards_history.modified_by")
blocksHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by").
Prefix("UNION ALL").
From(s.tablePrefix + "blocks_history as blocks_history").
Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id").
Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"blocks_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by")
boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery)
insightsQuery := s.getQueryBuilder(db).Select(
fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")),
).
FromSelect(boardsActivity, "boards_and_blocks_history").
GroupBy("id, title, icon, created_by").
OrderBy("activity_count desc")
userQuery := s.getQueryBuilder(db).Select("*").
FromSelect(insightsQuery, "boards_and_blocks_history_for_user").
Where(sq.Or{
sq.Eq{
"created_by": userID,
},
sq.Expr(s.elementInColumn("active_users"), userID),
}).
Offset(uint64(offset)).
Limit(uint64(limit))
rows, err := userQuery.Query()
if err != nil {
s.logger.Error(`Team insights query ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boardsInsights, err := boardsInsightsFromRows(rows)
if err != nil {
return nil, err
}
boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit)
return boardInsightsPaginated, nil
}
func boardsInsightsFromRows(rows *sql.Rows) ([]*model.BoardInsight, error) {
boardsInsights := []*model.BoardInsight{}
for rows.Next() {
var boardInsight model.BoardInsight
var activeUsersString string
err := rows.Scan(
&boardInsight.BoardID,
&boardInsight.Title,
&boardInsight.Icon,
&boardInsight.ActivityCount,
&activeUsersString,
&boardInsight.CreatedBy,
)
// split activeUsersString into slice
boardInsight.ActiveUsers = strings.Split(activeUsersString, ",")
if err != nil {
return nil, err
}
boardsInsights = append(boardsInsights, &boardInsight)
}
return boardsInsights, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
type BlockDoesntBelongToBoardsErr struct {
blockID string
}
func (e BlockDoesntBelongToBoardsErr) Error() string {
return fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", e.blockID)
}
func (s *SQLStore) createBoardsAndBlocksWithAdmin(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
newBab, err := s.createBoardsAndBlocks(db, bab, userID)
if err != nil {
return nil, nil, err
}
members := []*model.BoardMember{}
for _, board := range newBab.Boards {
bm := &model.BoardMember{
BoardID: board.ID,
UserID: board.CreatedBy,
SchemeAdmin: true,
SchemeEditor: true,
}
nbm, err := s.saveMember(db, bm)
if err != nil {
return nil, nil, err
}
members = append(members, nbm)
}
return newBab, members, nil
}
func (s *SQLStore) createBoardsAndBlocks(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
boards := []*model.Board{}
blocks := []*model.Block{}
for _, board := range bab.Boards {
newBoard, err := s.insertBoard(db, board, userID)
if err != nil {
return nil, err
}
boards = append(boards, newBoard)
}
for _, block := range bab.Blocks {
b := block
err := s.insertBlock(db, b, userID)
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
newBab := &model.BoardsAndBlocks{
Boards: boards,
Blocks: blocks,
}
return newBab, nil
}
func (s *SQLStore) patchBoardsAndBlocks(db sq.BaseRunner, pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
bab := &model.BoardsAndBlocks{}
for i, boardID := range pbab.BoardIDs {
board, err := s.patchBoard(db, boardID, pbab.BoardPatches[i], userID)
if err != nil {
return nil, err
}
bab.Boards = append(bab.Boards, board)
}
for i, blockID := range pbab.BlockIDs {
if err := s.patchBlock(db, blockID, pbab.BlockPatches[i], userID); err != nil {
return nil, err
}
block, err := s.getBlock(db, blockID)
if err != nil {
return nil, err
}
bab.Blocks = append(bab.Blocks, block)
}
return bab, nil
}
// deleteBoardsAndBlocks deletes all the boards and blocks entities of
// the DeleteBoardsAndBlocks struct, making sure that all the blocks
// belong to the boards in the struct.
func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoardsAndBlocks, userID string) error {
boardIDMap := map[string]bool{}
for _, boardID := range dbab.Boards {
boardIDMap[boardID] = true
}
// delete the blocks first, since deleting the board will clean up any children and we'll get
// not found errors when deleting the blocks after.
for _, blockID := range dbab.Blocks {
block, err := s.getBlock(db, blockID)
if err != nil {
return err
}
if _, ok := boardIDMap[block.BoardID]; !ok {
return BlockDoesntBelongToBoardsErr{blockID}
}
if err := s.deleteBlock(db, blockID, userID); err != nil {
return err
}
}
for _, boardID := range dbab.Boards {
if err := s.deleteBoard(db, boardID, userID); err != nil {
return err
}
}
return nil
}
func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
bab := &model.BoardsAndBlocks{
Boards: []*model.Board{},
Blocks: []*model.Block{},
}
board, err := s.getBoard(db, boardID)
if err != nil {
return nil, nil, err
}
// todo: server localization
if asTemplate == board.IsTemplate {
// board -> board or template -> template
board.Title += " copy"
} else if asTemplate {
// template from board
board.Title = "New board template"
}
// make new board private
board.Type = "P"
board.IsTemplate = asTemplate
board.CreatedBy = userID
board.ChannelID = ""
if toTeam != "" {
board.TeamID = toTeam
}
bab.Boards = []*model.Board{board}
blocks, err := s.getBlocksForBoard(db, boardID)
if err != nil {
return nil, nil, err
}
newBlocks := []*model.Block{}
for _, b := range blocks {
if b.Type != model.TypeComment {
newBlocks = append(newBlocks, b)
}
}
bab.Blocks = newBlocks
bab, err = model.GenerateBoardsAndBlocksIDs(bab, nil)
if err != nil {
return nil, nil, err
}
return s.createBoardsAndBlocksWithAdmin(db, bab, userID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"context"
"database/sql"
"fmt"
"path/filepath"
"text/template"
"github.com/mattermost/morph"
"github.com/mattermost/morph/drivers"
"github.com/mattermost/morph/drivers/mysql"
"github.com/mattermost/morph/drivers/postgres"
embedded "github.com/mattermost/morph/sources/embedded"
"github.com/mgdelacroix/foundation"
"github.com/mattermost/mattermost-server/v6/server/channels/db"
mmSqlStore "github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
var tablePrefix = "focalboard_"
type BoardsMigrator struct {
connString string
driverName string
db *sql.DB
store *SQLStore
morphEngine *morph.Morph
morphDriver drivers.Driver
}
func NewBoardsMigrator(store *SQLStore) *BoardsMigrator {
return &BoardsMigrator{
connString: store.connectionString,
driverName: store.dbType,
store: store,
}
}
func (bm *BoardsMigrator) runMattermostMigrations() error {
assets := db.Assets()
assetsList, err := assets.ReadDir(filepath.Join("migrations", bm.driverName))
if err != nil {
return err
}
assetNames := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNames[i] = entry.Name()
}
src, err := embedded.WithInstance(&embedded.AssetSource{
Names: assetNames,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(filepath.Join("migrations", bm.driverName, name))
},
})
if err != nil {
return err
}
driver, err := bm.getDriver()
if err != nil {
return err
}
options := []morph.EngineOption{
morph.SetStatementTimeoutInSeconds(1000000),
}
engine, err := morph.New(context.Background(), driver, src, options...)
if err != nil {
return err
}
defer engine.Close()
return engine.ApplyAll()
}
func (bm *BoardsMigrator) getDriver() (drivers.Driver, error) {
var driver drivers.Driver
var err error
switch bm.driverName {
case model.PostgresDBType:
driver, err = postgres.WithInstance(bm.db)
if err != nil {
return nil, err
}
case model.MysqlDBType:
driver, err = mysql.WithInstance(bm.db)
if err != nil {
return nil, err
}
}
return driver, nil
}
func (bm *BoardsMigrator) getMorphConnection() (*morph.Morph, drivers.Driver, error) {
driver, err := bm.getDriver()
if err != nil {
return nil, nil, err
}
assetsList, err := Assets.ReadDir("migrations")
if err != nil {
return nil, nil, err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, dirEntry := range assetsList {
assetNamesForDriver[i] = dirEntry.Name()
}
params := map[string]interface{}{
"prefix": tablePrefix,
"postgres": bm.driverName == model.PostgresDBType,
"mysql": bm.driverName == model.MysqlDBType,
"plugin": true, // TODO: to be removed
"singleUser": false,
}
migrationAssets := &embedded.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
asset, mErr := Assets.ReadFile("migrations/" + name)
if mErr != nil {
return nil, mErr
}
tmpl, pErr := template.New("sql").Funcs(bm.store.GetTemplateHelperFuncs()).Parse(string(asset))
if pErr != nil {
return nil, pErr
}
buffer := bytes.NewBufferString("")
err = tmpl.Execute(buffer, params)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
},
}
src, err := embedded.WithInstance(migrationAssets)
if err != nil {
return nil, nil, err
}
engine, err := morph.New(context.Background(), driver, src, morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", tablePrefix)))
if err != nil {
return nil, nil, err
}
return engine, driver, nil
}
func (bm *BoardsMigrator) Setup() error {
var err error
if bm.driverName == model.MysqlDBType {
bm.connString, err = mmSqlStore.ResetReadTimeout(bm.connString)
if err != nil {
return err
}
bm.connString, err = mmSqlStore.AppendMultipleStatementsFlag(bm.connString)
if err != nil {
return err
}
}
var dbErr error
bm.db, dbErr = sql.Open(bm.driverName, bm.connString)
if dbErr != nil {
return dbErr
}
if err2 := bm.db.Ping(); err2 != nil {
return err2
}
if err3 := bm.runMattermostMigrations(); err3 != nil {
return err3
}
storeParams := Params{
DBType: bm.driverName,
ConnectionString: bm.connString,
TablePrefix: tablePrefix,
Logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug),
DB: bm.db,
IsPlugin: true, // TODO: to be removed
SkipMigrations: true,
}
bm.store, err = New(storeParams)
if err != nil {
return err
}
morphEngine, morphDriver, err := bm.getMorphConnection()
if err != nil {
return err
}
bm.morphEngine = morphEngine
bm.morphDriver = morphDriver
return nil
}
func (bm *BoardsMigrator) MigrateToStep(step int) error {
applied, err := bm.morphDriver.AppliedMigrations()
if err != nil {
return err
}
currentVersion := len(applied)
if _, err := bm.morphEngine.Apply(step - currentVersion); err != nil {
return err
}
return nil
}
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
return map[int]foundation.Interceptor{
18: bm.store.RunDeletedMembershipBoardsMigration,
35: func() error {
return bm.store.RunDeDuplicateCategoryBoardsMigration(35)
},
}
}
func (bm *BoardsMigrator) TearDown() error {
if err := bm.morphEngine.Close(); err != nil {
return err
}
if err := bm.db.Close(); err != nil {
return err
}
return nil
}
func (bm *BoardsMigrator) DriverName() string {
return bm.driverName
}
func (bm *BoardsMigrator) DB() *sql.DB {
return bm.db
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const categorySortOrderGap = 10
func (s *SQLStore) categoryFields() []string {
return []string{
"id",
"name",
"user_id",
"team_id",
"create_at",
"update_at",
"delete_at",
"collapsed",
"COALESCE(sort_order, 0)",
"type",
}
}
func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, error) {
query := s.getQueryBuilder(db).
Select(s.categoryFields()...).
From(s.tablePrefix + "categories").
Where(sq.Eq{"id": id})
rows, err := query.Query()
if err != nil {
s.logger.Error("getCategory error", mlog.Err(err))
return nil, err
}
categories, err := s.categoriesFromRows(rows)
if err != nil {
s.logger.Error("getCategory row scan error", mlog.Err(err))
return nil, err
}
if len(categories) == 0 {
return nil, model.NewErrNotFound("category ID=" + id)
}
return &categories[0], nil
}
func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) error {
// A new category should always end up at the top.
// So we first insert the provided category, then bump up
// existing user-team categories' order
// creating provided category
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"categories").
Columns(
"id",
"name",
"user_id",
"team_id",
"create_at",
"update_at",
"delete_at",
"collapsed",
"sort_order",
"type",
).
Values(
category.ID,
category.Name,
category.UserID,
category.TeamID,
category.CreateAt,
category.UpdateAt,
category.DeleteAt,
category.Collapsed,
category.SortOrder,
category.Type,
)
_, err := query.Exec()
if err != nil {
s.logger.Error("Error creating category", mlog.String("category name", category.Name), mlog.Err(err))
return err
}
// bumping up order of existing categories
updateQuery := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("sort_order", sq.Expr(fmt.Sprintf("sort_order + %d", categorySortOrderGap))).
Where(
sq.Eq{
"user_id": category.UserID,
"team_id": category.TeamID,
"delete_at": 0,
},
)
if _, err := updateQuery.Exec(); err != nil {
s.logger.Error(
"createCategory failed to update sort order of existing user-team categories",
mlog.String("user_id", category.UserID),
mlog.String("team_id", category.TeamID),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) updateCategory(db sq.BaseRunner, category model.Category) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("name", category.Name).
Set("update_at", category.UpdateAt).
Set("collapsed", category.Collapsed).
Where(sq.Eq{
"id": category.ID,
"delete_at": 0,
})
_, err := query.Exec()
if err != nil {
s.logger.Error("Error updating category", mlog.String("category_id", category.ID), mlog.String("category_name", category.Name), mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID string) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("delete_at", utils.GetMillis()).
Where(sq.Eq{
"id": categoryID,
"user_id": userID,
"team_id": teamID,
"delete_at": 0,
})
_, err := query.Exec()
if err != nil {
s.logger.Error(
"Error updating category",
mlog.String("category_id", categoryID),
mlog.String("user_id", userID),
mlog.String("team_id", teamID),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) getUserCategories(db sq.BaseRunner, userID, teamID string) ([]model.Category, error) {
query := s.getQueryBuilder(db).
Select(s.categoryFields()...).
From(s.tablePrefix+"categories").
Where(sq.Eq{
"user_id": userID,
"team_id": teamID,
"delete_at": 0,
}).
OrderBy("sort_order", "name")
rows, err := query.Query()
if err != nil {
s.logger.Error("getUserCategories error", mlog.Err(err))
return nil, err
}
return s.categoriesFromRows(rows)
}
func (s *SQLStore) categoriesFromRows(rows *sql.Rows) ([]model.Category, error) {
var categories []model.Category
for rows.Next() {
category := model.Category{}
err := rows.Scan(
&category.ID,
&category.Name,
&category.UserID,
&category.TeamID,
&category.CreateAt,
&category.UpdateAt,
&category.DeleteAt,
&category.Collapsed,
&category.SortOrder,
&category.Type,
)
if err != nil {
s.logger.Error("categoriesFromRows row parsing error", mlog.Err(err))
return nil, err
}
categories = append(categories, category)
}
return categories, nil
}
func (s *SQLStore) reorderCategories(db sq.BaseRunner, userID, teamID string, newCategoryOrder []string) ([]string, error) {
if len(newCategoryOrder) == 0 {
return nil, nil
}
updateCase := sq.Case("id")
for i, categoryID := range newCategoryOrder {
updateCase = updateCase.When("'"+categoryID+"'", sq.Expr(fmt.Sprintf("%d", i*categorySortOrderGap)))
}
updateCase = updateCase.Else("sort_order")
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("sort_order", updateCase).
Where(sq.Eq{
"user_id": userID,
"team_id": teamID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"reorderCategories failed to update category order",
mlog.String("user_id", userID),
mlog.String("team_id", teamID),
mlog.Err(err),
)
return nil, err
}
return newCategoryOrder, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getUserCategoryBoards(db sq.BaseRunner, userID, teamID string) ([]model.CategoryBoards, error) {
categories, err := s.getUserCategories(db, userID, teamID)
if err != nil {
return nil, err
}
userCategoryBoards := []model.CategoryBoards{}
for _, category := range categories {
boardMetadata, err := s.getCategoryBoardAttributes(db, category.ID)
if err != nil {
return nil, err
}
userCategoryBoard := model.CategoryBoards{
Category: category,
BoardMetadata: boardMetadata,
}
userCategoryBoards = append(userCategoryBoards, userCategoryBoard)
}
return userCategoryBoards, nil
}
func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID string) ([]model.CategoryBoardMetadata, error) {
query := s.getQueryBuilder(db).
Select("board_id, COALESCE(hidden, false)").
From(s.tablePrefix + "category_boards").
Where(sq.Eq{
"category_id": categoryID,
}).
OrderBy("sort_order")
rows, err := query.Query()
if err != nil {
s.logger.Error("getCategoryBoards error fetching categoryblocks", mlog.String("categoryID", categoryID), mlog.Err(err))
return nil, err
}
return s.categoryBoardsFromRows(rows)
}
func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID, categoryID string, boardIDsParam []string) error {
// we need to de-duplicate this array as Postgres failes to
// handle upsert if there are multiple incoming rows
// that conflict the same existing row.
// For example, having the entry "1" in DB and trying to upsert "1" and "1" will fail
// as there are multiple duplicates of the same "1".
//
// Source: https://stackoverflow.com/questions/42994373/postgresql-on-conflict-cannot-affect-row-a-second-time
boardIDs := utils.DedupeStringArr(boardIDsParam)
if len(boardIDs) == 0 {
return nil
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"category_boards").
Columns(
"id",
"user_id",
"category_id",
"board_id",
"create_at",
"update_at",
"sort_order",
"hidden",
)
now := utils.GetMillis()
for _, boardID := range boardIDs {
query = query.Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
now,
0,
false,
)
}
if s.dbType == model.MysqlDBType {
query = query.Suffix(
"ON DUPLICATE KEY UPDATE category_id = ?",
categoryID,
)
} else {
query = query.Suffix(
`ON CONFLICT (user_id, board_id)
DO UPDATE SET category_id = EXCLUDED.category_id, update_at = EXCLUDED.update_at`,
)
}
if _, err := query.Exec(); err != nil {
return fmt.Errorf(
"store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w",
userID, categoryID, len(boardIDs), err,
)
}
return nil
}
func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]model.CategoryBoardMetadata, error) {
metadata := []model.CategoryBoardMetadata{}
for rows.Next() {
datum := model.CategoryBoardMetadata{}
err := rows.Scan(&datum.BoardID, &datum.Hidden)
if err != nil {
s.logger.Error("categoryBoardsFromRows row scan error", mlog.Err(err))
return nil, err
}
metadata = append(metadata, datum)
}
return metadata, nil
}
func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, newBoardsOrder []string) ([]string, error) {
if len(newBoardsOrder) == 0 {
return nil, nil
}
updateCase := sq.Case("board_id")
for i, boardID := range newBoardsOrder {
updateCase = updateCase.When("'"+boardID+"'", sq.Expr(fmt.Sprintf("%d", i+model.CategoryBoardsSortOrderGap)))
}
updateCase.Else("sort_order")
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("sort_order", updateCase).
Where(sq.Eq{
"category_id": categoryID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"reorderCategoryBoards failed to update category board order",
mlog.String("category_id", categoryID),
mlog.Err(err),
)
return nil, err
}
return newBoardsOrder, nil
}
func (s *SQLStore) setBoardVisibility(db sq.BaseRunner, userID, categoryID, boardID string, visible bool) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("hidden", !visible).
Where(sq.Eq{
"user_id": userID,
"category_id": categoryID,
"board_id": boardID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"SQLStore setBoardVisibility: failed to update board visibility",
mlog.String("user_id", userID),
mlog.String("board_id", boardID),
mlog.Bool("visible", visible),
mlog.Err(err),
)
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"errors"
"strconv"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
)
var ErrInvalidCardLimitValue = errors.New("card limit value is invalid")
// activeCardsQuery applies the necessary filters to the query for it
// to fetch an active cards window if the cardLimit is set, or all the
// active cards if it's 0.
func (s *SQLStore) activeCardsQuery(builder sq.StatementBuilderType, selectStr string, cardLimit int) sq.SelectBuilder {
query := builder.
Select(selectStr).
From(s.tablePrefix + "blocks b").
Join(s.tablePrefix + "boards bd on b.board_id=bd.id").
Where(sq.Eq{
"b.delete_at": 0,
"b.type": model.TypeCard,
"bd.is_template": false,
})
if cardLimit != 0 {
query = query.
Limit(1).
Offset(uint64(cardLimit - 1))
}
return query
}
// getUsedCardsCount returns the amount of active cards in the server.
func (s *SQLStore) getUsedCardsCount(db sq.BaseRunner) (int, error) {
row := s.activeCardsQuery(s.getQueryBuilder(db), "count(b.id)", 0).
QueryRow()
var usedCards int
err := row.Scan(&usedCards)
if err != nil {
return 0, err
}
return usedCards, nil
}
// getCardLimitTimestamp returns the timestamp value from the
// system_settings table or zero if it doesn't exist.
func (s *SQLStore) getCardLimitTimestamp(db sq.BaseRunner) (int64, error) {
scanner := s.getQueryBuilder(db).
Select("value").
From(s.tablePrefix + "system_settings").
Where(sq.Eq{"id": store.CardLimitTimestampSystemKey}).
QueryRow()
var result string
err := scanner.Scan(&result)
if errors.Is(sql.ErrNoRows, err) {
return 0, nil
}
if err != nil {
return 0, err
}
cardLimitTimestamp, err := strconv.Atoi(result)
if err != nil {
return 0, ErrInvalidCardLimitValue
}
return int64(cardLimitTimestamp), nil
}
// updateCardLimitTimestamp updates the card limit value in the
// system_settings table with the timestamp of the nth last updated
// card, being nth the value of the cardLimit parameter. If cardLimit
// is zero, the timestamp will be set to zero.
func (s *SQLStore) updateCardLimitTimestamp(db sq.BaseRunner, cardLimit int) (int64, error) {
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"system_settings").
Columns("id", "value")
var value interface{} = 0
if cardLimit != 0 {
value = s.activeCardsQuery(sq.StatementBuilder, "b.update_at", cardLimit).
OrderBy("b.update_at DESC").
Prefix("COALESCE((").Suffix("), 0)")
}
query = query.Values(store.CardLimitTimestampSystemKey, value)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value)
} else {
query = query.Suffix(
`ON CONFLICT (id)
DO UPDATE SET value = EXCLUDED.value`,
)
}
result, err := query.Exec()
if err != nil {
return 0, err
}
if _, err := result.RowsAffected(); err != nil {
return 0, err
}
return s.getCardLimitTimestamp(db)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getBoardsForCompliance(db sq.BaseRunner, opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"b.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsForCompliance ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(boards) > opts.PerPage {
boards = boards[0:opts.PerPage]
hasMore = true
}
return boards, hasMore, nil
}
func (s *SQLStore) getBoardsComplianceHistory(db sq.BaseRunner, opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
queryDescendentLastUpdate := s.getQueryBuilder(db).
Select("MAX(blk1.update_at)").
From(s.tablePrefix + "blocks_history as blk1").
Where("blk1.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentLastUpdate.Where(sq.Eq{"blk1.delete_at": 0})
}
sqlDescendentLastUpdate, _, _ := queryDescendentLastUpdate.ToSql()
queryDescendentFirstUpdate := s.getQueryBuilder(db).
Select("MIN(blk2.update_at)").
From(s.tablePrefix + "blocks_history as blk2").
Where("blk2.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentFirstUpdate.Where(sq.Eq{"blk2.delete_at": 0})
}
sqlDescendentFirstUpdate, _, _ := queryDescendentFirstUpdate.ToSql()
query := s.getQueryBuilder(db).
Select(
"bh.id",
"bh.team_id",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"COALESCE(("+sqlDescendentLastUpdate+"),0) as decendentLastUpdateAt",
"COALESCE(("+sqlDescendentFirstUpdate+"),0) as decendentFirstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "boards_history as bh")
if !opts.IncludeDeleted {
// filtering out deleted boards; join with boards table to ensure no history
// for deleted boards are returned. Deleted boards won't exist in boards table.
query = query.Join(s.tablePrefix + "boards as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "bh.team_id", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("decendentLastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"bh.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.boardsHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) getBlocksComplianceHistory(db sq.BaseRunner, opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
query := s.getQueryBuilder(db).
Select(
"bh.id",
"brd.team_id",
"bh.board_id",
"bh.type",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"max(bh.update_at) as lastUpdateAt",
"min(bh.update_at) as firstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "blocks_history as bh").
Join(s.tablePrefix + "boards_history as brd on brd.id=bh.board_id")
if !opts.IncludeDeleted {
// filtering out deleted blocks; join with blocks table to ensure no history
// for deleted blocks are returned. Deleted blocks won't exist in blocks table.
query = query.Join(s.tablePrefix + "blocks as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "brd.team_id", "bh.board_id", "bh.type", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("lastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"brd.team_id": opts.TeamID})
}
if opts.BoardID != "" {
query = query.Where(sq.Eq{"bh.board_id": opts.BoardID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlocksComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.blocksHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) boardsHistoryFromRows(rows *sql.Rows) ([]*model.BoardHistory, error) {
history := []*model.BoardHistory{}
for rows.Next() {
boardHistory := &model.BoardHistory{}
err := rows.Scan(
&boardHistory.ID,
&boardHistory.TeamID,
&boardHistory.IsDeleted,
&boardHistory.DescendantLastUpdateAt,
&boardHistory.DescendantFirstUpdateAt,
&boardHistory.CreatedBy,
&boardHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("boardsHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, boardHistory)
}
return history, nil
}
func (s *SQLStore) blocksHistoryFromRows(rows *sql.Rows) ([]*model.BlockHistory, error) {
history := []*model.BlockHistory{}
for rows.Next() {
blockHistory := &model.BlockHistory{}
err := rows.Scan(
&blockHistory.ID,
&blockHistory.TeamID,
&blockHistory.BoardID,
&blockHistory.Type,
&blockHistory.IsDeleted,
&blockHistory.LastUpdateAt,
&blockHistory.FirstUpdateAt,
&blockHistory.CreatedBy,
&blockHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("blocksHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, blockHistory)
}
return history, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"fmt"
"os"
"strconv"
sq "github.com/Masterminds/squirrel"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
// we group the inserts on batches of 1000 because PostgreSQL
// supports a limit of around 64K values (not rows) on an insert
// query, so we want to stay safely below.
CategoryInsertBatch = 1000
TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete"
UniqueIDsMigrationKey = "UniqueIDsMigrationComplete"
CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete"
TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete"
DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete"
DeDuplicateCategoryBoardTableMigrationKey = "DeDuplicateCategoryBoardTableComplete"
)
func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]*model.Block, error) {
subquery, _, _ := s.getQueryBuilder(db).
Select("id").
From(s.tablePrefix + "blocks").
Having("count(id) > 1").
GroupBy("id").
ToSql()
blocksFields := []string{
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"COALESCE(fields, '{}')",
s.timestampToCharField("insert_at", "insertAt"),
"create_at",
"update_at",
"delete_at",
"COALESCE(workspace_id, '0')",
}
rows, err := s.getQueryBuilder(db).
Select(blocksFields...).
From(s.tablePrefix + "blocks").
Where(fmt.Sprintf("id IN (%s)", subquery)).
Query()
if err != nil {
s.logger.Error(`getBlocksWithSameID ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
func (s *SQLStore) RunUniqueIDsMigration() error {
setting, err := s.GetSystemSetting(UniqueIDsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
s.logger.Debug("Running Unique IDs migration")
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
blocks, err := s.getBlocksWithSameID(tx)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "getBlocksWithSameID"))
}
return fmt.Errorf("cannot get blocks with same ID: %w", err)
}
blocksByID := map[string][]*model.Block{}
for _, block := range blocks {
blocksByID[block.ID] = append(blocksByID[block.ID], block)
}
for _, blocks := range blocksByID {
for i, block := range blocks {
if i == 0 {
// do nothing for the first ID, only updating the others
continue
}
newID := utils.NewID(model.BlockType2IDType(block.Type))
if err := s.replaceBlockID(tx, block.ID, newID, block.WorkspaceID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "replaceBlockID"))
}
return fmt.Errorf("cannot replace blockID %s: %w", block.ID, err)
}
}
}
if err := s.setSystemSetting(tx, UniqueIDsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("cannot commit unique IDs transaction: %w", err)
}
s.logger.Debug("Unique IDs migration finished successfully")
return nil
}
// RunCategoryUUIDIDMigration takes care of deriving the categories
// from the boards and its memberships. The name references UUID
// because of the preexisting purpose of this migration, and has been
// preserved for compatibility with already migrated instances.
func (s *SQLStore) RunCategoryUUIDIDMigration() error {
setting, err := s.GetSystemSetting(CategoryUUIDIDMigrationKey)
if err != nil {
return fmt.Errorf("cannot get migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
s.logger.Debug("Running category UUID ID migration")
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
if s.isPlugin {
if err := s.createCategories(tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs insert categories transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return err
}
if err := s.createCategoryBoards(tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs insert category boards transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return err
}
}
if err := s.setSystemSetting(tx, CategoryUUIDIDMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("cannot commit category UUIDs transaction: %w", err)
}
s.logger.Debug("category UUIDs migration finished successfully")
return nil
}
func (s *SQLStore) createCategories(db sq.BaseRunner) error {
rows, err := s.getQueryBuilder(db).
Select("c.DisplayName, cm.UserId, c.TeamId, cm.ChannelId").
From(s.tablePrefix + "boards boards").
Join("ChannelMembers cm on boards.channel_id = cm.ChannelId").
Join("Channels c on cm.ChannelId = c.id and (c.Type = 'O' or c.Type = 'P')").
GroupBy("cm.UserId, c.TeamId, cm.ChannelId, c.DisplayName").
Query()
if err != nil {
s.logger.Error("get boards data error", mlog.Err(err))
return err
}
defer s.CloseRows(rows)
initQuery := func() sq.InsertBuilder {
return s.getQueryBuilder(db).
Insert(s.tablePrefix+"categories").
Columns(
"id",
"name",
"user_id",
"team_id",
"channel_id",
"create_at",
"update_at",
"delete_at",
)
}
// query will accumulate the insert values until the limit is
// reached, and then it will be stored and reset
query := initQuery()
// queryList stores those queries that already reached the limit
// to be run when all the data is processed
queryList := []sq.InsertBuilder{}
counter := 0
now := model.GetMillis()
for rows.Next() {
var displayName string
var userID string
var teamID string
var channelID string
err := rows.Scan(
&displayName,
&userID,
&teamID,
&channelID,
)
if err != nil {
return fmt.Errorf("cannot scan result while trying to create categories: %w", err)
}
query = query.Values(
utils.NewID(utils.IDTypeNone),
displayName,
userID,
teamID,
channelID,
now,
0,
0,
)
counter++
if counter%CategoryInsertBatch == 0 {
queryList = append(queryList, query)
query = initQuery()
}
}
if counter%CategoryInsertBatch != 0 {
queryList = append(queryList, query)
}
for _, q := range queryList {
if _, err := q.Exec(); err != nil {
return fmt.Errorf("cannot create category values: %w", err)
}
}
return nil
}
func (s *SQLStore) createCategoryBoards(db sq.BaseRunner) error {
rows, err := s.getQueryBuilder(db).
Select("categories.user_id, categories.id, boards.id").
From(s.tablePrefix + "categories categories").
Join(s.tablePrefix + "boards boards on categories.channel_id = boards.channel_id AND boards.is_template = false").
Query()
if err != nil {
s.logger.Error("get categories data error", mlog.Err(err))
return err
}
defer s.CloseRows(rows)
initQuery := func() sq.InsertBuilder {
return s.getQueryBuilder(db).
Insert(s.tablePrefix+"category_boards").
Columns(
"id",
"user_id",
"category_id",
"board_id",
"create_at",
"update_at",
"delete_at",
)
}
// query will accumulate the insert values until the limit is
// reached, and then it will be stored and reset
query := initQuery()
// queryList stores those queries that already reached the limit
// to be run when all the data is processed
queryList := []sq.InsertBuilder{}
counter := 0
now := model.GetMillis()
for rows.Next() {
var userID string
var categoryID string
var boardID string
err := rows.Scan(
&userID,
&categoryID,
&boardID,
)
if err != nil {
return fmt.Errorf("cannot scan result while trying to create category boards: %w", err)
}
query = query.Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
0,
0,
)
counter++
if counter%CategoryInsertBatch == 0 {
queryList = append(queryList, query)
query = initQuery()
}
}
if counter%CategoryInsertBatch != 0 {
queryList = append(queryList, query)
}
for _, q := range queryList {
if _, err := q.Exec(); err != nil {
return fmt.Errorf("cannot create category boards values: %w", err)
}
}
return nil
}
// We no longer support boards existing in DMs and private
// group messages. This function migrates all boards
// belonging to a DM to the best possible team.
func (s *SQLStore) RunTeamLessBoardsMigration() error {
if !s.isPlugin {
return nil
}
setting, err := s.GetSystemSetting(TeamLessBoardsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get teamless boards migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
boards, err := s.getDMBoards(s.db)
if err != nil {
return err
}
s.logger.Debug("Migrating teamless boards to a team", mlog.Int("count", len(boards)))
// cache for best suitable team for a DM. Since a DM can
// contain multiple boards, caching this avoids
// duplicate queries for the same DM.
channelToTeamCache := map[string]string{}
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
s.logger.Error("error starting transaction in runTeamLessBoardsMigration", mlog.Err(err))
return err
}
for i := range boards {
// check the cache first
teamID, ok := channelToTeamCache[boards[i].ChannelID]
// query DB if entry not found in cache
if !ok {
teamID, err = s.getBestTeamForBoard(s.db, boards[i])
if err != nil {
// don't let one board's error spoil
// the mood for others
s.logger.Error("could not find the best team for board during team less boards migration. Continuing", mlog.String("boardID", boards[i].ID))
continue
}
}
channelToTeamCache[boards[i].ChannelID] = teamID
boards[i].TeamID = teamID
query := s.getQueryBuilder(tx).
Update(s.tablePrefix+"boards").
Set("team_id", teamID).
Set("type", model.BoardTypePrivate).
Where(sq.Eq{"id": boards[i].ID})
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err))
return err
}
}
if err := s.setSystemSetting(tx, TeamLessBoardsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runTeamLessBoardsMigration"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
s.logger.Error("failed to commit runTeamLessBoardsMigration transaction", mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) getDMBoards(tx sq.BaseRunner) ([]*model.Board, error) {
conditions := sq.And{
sq.Eq{"team_id": ""},
sq.Or{
sq.Eq{"type": "D"},
sq.Eq{"type": "G"},
},
}
boards, err := s.getLegacyBoardsByCondition(tx, conditions)
if err != nil && model.IsErrNotFound(err) {
return []*model.Board{}, nil
}
return boards, err
}
// The destination is selected as the first team where all members
// of the DM are a part of. If no such team exists,
// we use the first team to which DM creator belongs to.
func (s *SQLStore) getBestTeamForBoard(tx sq.BaseRunner, board *model.Board) (string, error) {
userTeams, err := s.getBoardUserTeams(tx, board)
if err != nil {
return "", err
}
teams := [][]interface{}{}
for _, userTeam := range userTeams {
userTeamInterfaces := make([]interface{}, len(userTeam))
for i := range userTeam {
userTeamInterfaces[i] = userTeam[i]
}
teams = append(teams, userTeamInterfaces)
}
commonTeams := utils.Intersection(teams...)
var teamID string
if len(commonTeams) > 0 {
teamID = commonTeams[0].(string)
} else {
// no common teams found. Let's try finding the best suitable team
if board.Type == "D" {
// get DM's creator and pick one of their team
channel, err := (s.servicesAPI).GetChannelByID(board.ChannelID)
if err != nil {
s.logger.Error("failed to fetch DM channel for board",
mlog.String("board_id", board.ID),
mlog.String("channel_id", board.ChannelID),
mlog.Err(err),
)
return "", err
}
if _, ok := userTeams[channel.CreatorId]; !ok {
s.logger.Error("channel creator not found in user teams",
mlog.String("board_id", board.ID),
mlog.String("channel_id", board.ChannelID),
mlog.String("creator_id", channel.CreatorId),
)
err := fmt.Errorf("%w board_id: %s, channel_id: %s, creator_id: %s", errChannelCreatorNotInTeam, board.ID, board.ChannelID, channel.CreatorId)
return "", err
}
teamID = userTeams[channel.CreatorId][0]
} else if board.Type == "G" {
// pick the team that has the most users as members
teamFrequency := map[string]int{}
highestFrequencyTeam := ""
highestFrequencyTeamFrequency := -1
for _, teams := range userTeams {
for _, teamID := range teams {
teamFrequency[teamID]++
if teamFrequency[teamID] > highestFrequencyTeamFrequency {
highestFrequencyTeamFrequency = teamFrequency[teamID]
highestFrequencyTeam = teamID
}
}
}
teamID = highestFrequencyTeam
}
}
return teamID, nil
}
func (s *SQLStore) getBoardUserTeams(tx sq.BaseRunner, board *model.Board) (map[string][]string, error) {
query := s.getQueryBuilder(tx).
Select("tm.UserId", "tm.TeamId").
From("ChannelMembers cm").
Join("TeamMembers tm ON cm.UserId = tm.UserId").
Join("Teams t ON tm.TeamId = t.Id").
Where(sq.Eq{
"cm.ChannelId": board.ChannelID,
"t.DeleteAt": 0,
"tm.DeleteAt": 0,
})
rows, err := query.Query()
if err != nil {
s.logger.Error("failed to fetch user teams for board", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
return nil, err
}
defer rows.Close()
userTeams := map[string][]string{}
for rows.Next() {
var userID, teamID string
err := rows.Scan(&userID, &teamID)
if err != nil {
s.logger.Error("getBoardUserTeams failed to scan SQL query result", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
return nil, err
}
userTeams[userID] = append(userTeams[userID], teamID)
}
return userTeams, nil
}
func (s *SQLStore) RunDeletedMembershipBoardsMigration() error {
if !s.isPlugin {
return nil
}
setting, err := s.GetSystemSetting(DeletedMembershipBoardsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get deleted membership boards migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
boards, err := s.getDeletedMembershipBoards(s.db)
if err != nil {
return err
}
if len(boards) == 0 {
s.logger.Debug("No boards with owner not anymore on their team found, marking runDeletedMembershipBoardsMigration as done")
if sErr := s.SetSystemSetting(DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); sErr != nil {
return fmt.Errorf("cannot mark migration as completed: %w", sErr)
}
return nil
}
s.logger.Debug("Migrating boards with owner not anymore on their team", mlog.Int("count", len(boards)))
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
s.logger.Error("error starting transaction in runDeletedMembershipBoardsMigration", mlog.Err(err))
return err
}
for i := range boards {
teamID, err := s.getBestTeamForBoard(s.db, boards[i])
if err != nil {
// don't let one board's error spoil
// the mood for others
s.logger.Error("could not find the best team for board during deleted membership boards migration. Continuing", mlog.String("boardID", boards[i].ID))
continue
}
boards[i].TeamID = teamID
query := s.getQueryBuilder(tx).
Update(s.tablePrefix+"boards").
Set("team_id", teamID).
Where(sq.Eq{"id": boards[i].ID})
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err))
return err
}
}
if err := s.setSystemSetting(tx, DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runDeletedMembershipBoardsMigration"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
s.logger.Error("failed to commit runDeletedMembershipBoardsMigration transaction", mlog.Err(err))
return err
}
return nil
}
// getDeletedMembershipBoards retrieves those boards whose creator is
// associated to the board's team with a deleted team membership.
func (s *SQLStore) getDeletedMembershipBoards(tx sq.BaseRunner) ([]*model.Board, error) {
rows, err := s.getQueryBuilder(tx).
Select(legacyBoardFields("b.")...).
From(s.tablePrefix + "boards b").
Join("TeamMembers tm ON b.created_by = tm.UserId").
Where("b.team_id = tm.TeamId").
Where(sq.NotEq{"tm.DeleteAt": 0}).
Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
return boards, err
}
func (s *SQLStore) RunFixCollationsAndCharsetsMigration() error {
// This is for MySQL only
if s.dbType != model.MysqlDBType {
return nil
}
// get collation and charSet setting that Channels is using.
// when personal server or unit testing, no channels tables exist so just set to a default.
var collation string
var charSet string
var err error
if !s.isPlugin || os.Getenv("FOCALBOARD_UNIT_TESTING") == "1" {
collation = "utf8mb4_general_ci"
charSet = "utf8mb4"
} else {
collation, charSet, err = s.getCollationAndCharset("Channels")
if err != nil {
return err
}
}
// get all FocalBoard tables
tableNames, err := s.getFocalBoardTableNames()
if err != nil {
return err
}
merr := merror.New()
// alter each table if there is a collation or charset mismatch
for _, name := range tableNames {
tableCollation, tableCharSet, err := s.getCollationAndCharset(name)
if err != nil {
return err
}
if collation == tableCollation && charSet == tableCharSet {
// nothing to do
continue
}
s.logger.Warn(
"found collation/charset mismatch, fixing table",
mlog.String("tableName", name),
mlog.String("tableCollation", tableCollation),
mlog.String("tableCharSet", tableCharSet),
mlog.String("collation", collation),
mlog.String("charSet", charSet),
)
sql := fmt.Sprintf("ALTER TABLE %s CONVERT TO CHARACTER SET '%s' COLLATE '%s'", name, charSet, collation)
result, err := s.db.Exec(sql)
if err != nil {
merr.Append(err)
continue
}
num, err := result.RowsAffected()
if err != nil {
merr.Append(err)
}
if num > 0 {
s.logger.Debug("table collation and/or charSet fixed",
mlog.String("table_name", name),
)
}
}
return merr.ErrorOrNil()
}
func (s *SQLStore) getFocalBoardTableNames() ([]string, error) {
if s.dbType != model.MysqlDBType {
return nil, newErrInvalidDBType("getFocalBoardTableNames requires MySQL")
}
query := s.getQueryBuilder(s.db).
Select("table_name").
From("information_schema.tables").
Where(sq.Like{"table_name": s.tablePrefix + "%"}).
Where("table_schema=(SELECT DATABASE())")
rows, err := query.Query()
if err != nil {
return nil, fmt.Errorf("error fetching FocalBoard table names: %w", err)
}
defer rows.Close()
names := make([]string, 0)
for rows.Next() {
var tableName string
err := rows.Scan(&tableName)
if err != nil {
return nil, fmt.Errorf("cannot scan result while fetching table names: %w", err)
}
names = append(names, tableName)
}
return names, nil
}
func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, error) {
if s.dbType != model.MysqlDBType {
return "", "", newErrInvalidDBType("getCollationAndCharset requires MySQL")
}
query := s.getQueryBuilder(s.db).
Select("table_collation").
From("information_schema.tables").
Where(sq.Eq{"table_name": tableName}).
Where("table_schema=(SELECT DATABASE())")
row := query.QueryRow()
var collation string
err := row.Scan(&collation)
if err != nil {
return "", "", fmt.Errorf("error fetching collation for table %s: %w", tableName, err)
}
// obtains the charset from the first column that has it set
query = s.getQueryBuilder(s.db).
Select("CHARACTER_SET_NAME").
From("information_schema.columns").
Where(sq.Eq{
"table_name": tableName,
}).
Where("table_schema=(SELECT DATABASE())").
Where(sq.NotEq{"CHARACTER_SET_NAME": "NULL"}).
Limit(1)
row = query.QueryRow()
var charSet string
err = row.Scan(&charSet)
if err != nil {
return "", "", fmt.Errorf("error fetching charSet: %w", err)
}
return collation, charSet, nil
}
func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error {
setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey)
if err != nil {
return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
if currentMigration >= (deDuplicateCategoryBoards + 1) {
// if the migration for which we're fixing the data is already applied,
// no need to check fix anything
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
needed, err := s.doesDuplicateCategoryBoardsExist()
if err != nil {
return err
}
if !needed {
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
}
if s.dbType == model.MysqlDBType {
return s.runMySQLDeDuplicateCategoryBoardsMigration()
} else if s.dbType == model.PostgresDBType {
return s.runPostgresDeDuplicateCategoryBoardsMigration()
}
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) {
subQuery := s.getQueryBuilder(s.db).
Select("user_id", "board_id", "count(*) AS count").
From(s.tablePrefix+"category_boards").
GroupBy("user_id", "board_id").
Having("count(*) > 1")
query := s.getQueryBuilder(s.db).
Select("COUNT(user_id)").
FromSelect(subQuery, "duplicate_dataset")
row := query.QueryRow()
count := 0
if err := row.Scan(&count); err != nil {
s.logger.Error("Error occurred reading number of duplicate records in category_boards table", mlog.Err(err))
return false, err
}
return count > 0, nil
}
func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error {
query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " +
"(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}
func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE FROM " + s.tablePrefix + "category_boards USING duplicates " +
"WHERE " + s.tablePrefix + "category_boards.id = duplicates.id AND duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"strings"
"time"
"github.com/pkg/errors"
sq "github.com/Masterminds/squirrel"
_ "github.com/lib/pq" // postgres driver
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RetentionTableDeletionInfo struct {
Table string
PrimaryKeys []string
BoardIDColumn string
}
func (s *SQLStore) runDataRetention(db sq.BaseRunner, globalRetentionDate int64, batchSize int64) (int64, error) {
s.logger.Info("Start Boards Data Retention",
mlog.String("Global Retention Date", time.Unix(globalRetentionDate/1000, 0).String()),
mlog.Int64("Raw Date", globalRetentionDate))
deleteTables := []RetentionTableDeletionInfo{
{
Table: "blocks",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
{
Table: "blocks_history",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
{
Table: "boards",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "boards_history",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "board_members",
PrimaryKeys: []string{"board_id"},
BoardIDColumn: "board_id",
},
{
Table: "board_members_history",
PrimaryKeys: []string{"board_id"},
BoardIDColumn: "board_id",
},
{
Table: "sharing",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "category_boards",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
}
subBuilder := s.getQueryBuilder(db).
Select("board_id, MAX(update_at) AS maxDate").
From(s.tablePrefix + "blocks").
GroupBy("board_id")
subQuery, _, _ := subBuilder.ToSql()
builder := s.getQueryBuilder(db).
Select("id").
From(s.tablePrefix + "boards").
LeftJoin("( " + subQuery + " ) As subquery ON (subquery.board_id = id)").
Where(sq.Lt{"maxDate": globalRetentionDate}).
Where(sq.NotEq{"team_id": "0"}).
Where(sq.Eq{"is_template": false})
rows, err := builder.Query()
if err != nil {
s.logger.Error(`dataRetention subquery ERROR`, mlog.Err(err))
return 0, err
}
defer s.CloseRows(rows)
deleteIds, err := idsFromRows(rows)
if err != nil {
return 0, err
}
totalAffected := 0
if len(deleteIds) > 0 {
for _, table := range deleteTables {
affected, err := s.genericRetentionPoliciesDeletion(db, table, deleteIds, batchSize)
if err != nil {
return int64(totalAffected), err
}
totalAffected += int(affected)
}
}
s.logger.Info("Complete Boards Data Retention",
mlog.Int("Total deletion ids", len(deleteIds)),
mlog.Int("TotalAffected", totalAffected))
return int64(totalAffected), nil
}
func idsFromRows(rows *sql.Rows) ([]string, error) {
deleteIds := []string{}
for rows.Next() {
var boardID string
err := rows.Scan(
&boardID,
)
if err != nil {
return nil, err
}
deleteIds = append(deleteIds, boardID)
}
return deleteIds, nil
}
// genericRetentionPoliciesDeletion actually executes the DELETE query
// using a sq.SelectBuilder which selects the rows to delete.
func (s *SQLStore) genericRetentionPoliciesDeletion(
db sq.BaseRunner,
info RetentionTableDeletionInfo,
deleteIds []string,
batchSize int64,
) (int64, error) {
whereClause := info.BoardIDColumn + " IN ('" + strings.Join(deleteIds, "','") + "')"
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + info.Table).
Where(whereClause)
if batchSize > 0 {
deleteQuery.Limit(uint64(batchSize))
primaryKeysStr := "(" + strings.Join(info.PrimaryKeys, ",") + ")"
if s.dbType != model.MysqlDBType {
selectQuery := s.getQueryBuilder(db).
Select(primaryKeysStr).
From(s.tablePrefix + info.Table).
Where(whereClause).
Limit(uint64(batchSize))
selectString, _, _ := selectQuery.ToSql()
deleteQuery = s.getQueryBuilder(db).
Delete(s.tablePrefix + info.Table).
Where(primaryKeysStr + " IN (" + selectString + ")")
}
}
var totalRowsAffected int64
var batchRowsAffected int64
for {
result, err := deleteQuery.Exec()
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+info.Table)
}
batchRowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+info.Table)
}
totalRowsAffected += batchRowsAffected
if batchRowsAffected != batchSize {
break
}
}
return totalRowsAffected, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"errors"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mm_model.FileInfo) error {
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"file_info").
Columns(
"id",
"create_at",
"name",
"extension",
"size",
"delete_at",
"path",
"archived",
).
Values(
fileInfo.Id,
fileInfo.CreateAt,
fileInfo.Name,
fileInfo.Extension,
fileInfo.Size,
fileInfo.DeleteAt,
fileInfo.Path,
false,
)
if _, err := query.Exec(); err != nil {
s.logger.Error(
"failed to save fileinfo",
mlog.String("file_name", fileInfo.Name),
mlog.Int64("size", fileInfo.Size),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mm_model.FileInfo, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"create_at",
"delete_at",
"name",
"extension",
"size",
"archived",
"path",
).
From(s.tablePrefix + "file_info").
Where(sq.Eq{"Id": id})
row := query.QueryRow()
fileInfo := mm_model.FileInfo{}
err := row.Scan(
&fileInfo.Id,
&fileInfo.CreateAt,
&fileInfo.DeleteAt,
&fileInfo.Name,
&fileInfo.Extension,
&fileInfo.Size,
&fileInfo.Archived,
&fileInfo.Path,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, model.NewErrNotFound("file info ID=" + id)
}
s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err))
return nil, err
}
return &fileInfo, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func legacyBoardFields(prefix string) []string {
// substitute new columns with `"\"\""` (empty string) so as to allow
// row scan to continue to work with new models.
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"modified_by",
"type",
"''", // substitute for minimum_role column.
"title",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"create_at",
"update_at",
"delete_at",
}
if prefix == "" {
return fields
}
prefixedFields := make([]string, len(fields))
for i, field := range fields {
switch {
case strings.HasPrefix(field, "COALESCE("):
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
case field == "''":
prefixedFields[i] = field
default:
prefixedFields[i] = prefix + field
}
}
return prefixedFields
}
// legacyBlocksFromRows is the old getBlock version that still uses
// the old block model. This method is kept to enable the unique IDs
// data migration.
//
//nolint:unused
func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
results := []*model.Block{}
for rows.Next() {
var block model.Block
var fieldsJSON string
var modifiedBy sql.NullString
var insertAt string
err := rows.Scan(
&block.ID,
&block.ParentID,
&block.BoardID,
&block.CreatedBy,
&modifiedBy,
&block.Schema,
&block.Type,
&block.Title,
&fieldsJSON,
&insertAt,
&block.CreateAt,
&block.UpdateAt,
&block.DeleteAt,
&block.WorkspaceID)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err))
return nil, err
}
if modifiedBy.Valid {
block.ModifiedBy = modifiedBy.String
}
err = json.Unmarshal([]byte(fieldsJSON), &block.Fields)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err))
return nil, err
}
results = append(results, &block)
}
return results, nil
}
// getLegacyBlock is the old getBlock version that still uses the old
// block model. This method is kept to enable the unique IDs data
// migration.
//
//nolint:unused
func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID string) (*model.Block, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"COALESCE(fields, '{}')",
"insert_at",
"create_at",
"update_at",
"delete_at",
"COALESCE(workspace_id, '0')",
).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": blockID}).
Where(sq.Eq{"coalesce(workspace_id, '0')": workspaceID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlock ERROR`, mlog.Err(err))
return nil, err
}
blocks, err := s.legacyBlocksFromRows(rows)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
return nil, nil
}
return blocks[0], nil
}
// insertLegacyBlock is the old insertBlock version that still uses
// the old block model. This method is kept to enable the unique IDs
// data migration.
//
//nolint:unused
func (s *SQLStore) insertLegacyBlock(db sq.BaseRunner, workspaceID string, block *model.Block, userID string) error {
if block.BoardID == "" {
return ErrEmptyBoardID{}
}
fieldsJSON, err := json.Marshal(block.Fields)
if err != nil {
return err
}
existingBlock, err := s.getLegacyBlock(db, workspaceID, block.ID)
if err != nil {
return err
}
block.UpdateAt = utils.GetMillis()
block.ModifiedBy = userID
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(
"workspace_id",
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"fields",
"create_at",
"update_at",
"delete_at",
)
insertQueryValues := map[string]interface{}{
"workspace_id": workspaceID,
"id": block.ID,
"parent_id": block.ParentID,
"root_id": block.BoardID,
s.escapeField("schema"): block.Schema,
"type": block.Type,
"title": block.Title,
"fields": fieldsJSON,
"delete_at": block.DeleteAt,
"created_by": block.CreatedBy,
"modified_by": block.ModifiedBy,
"create_at": block.CreateAt,
"update_at": block.UpdateAt,
}
if existingBlock != nil {
// block with ID exists, so this is an update operation
query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks").
Where(sq.Eq{"id": block.ID}).
Where(sq.Eq{"COALESCE(workspace_id, '0')": workspaceID}).
Set("parent_id", block.ParentID).
Set("root_id", block.BoardID).
Set("modified_by", block.ModifiedBy).
Set(s.escapeField("schema"), block.Schema).
Set("type", block.Type).
Set("title", block.Title).
Set("fields", fieldsJSON).
Set("update_at", block.UpdateAt).
Set("delete_at", block.DeleteAt)
if _, err := query.Exec(); err != nil {
s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err))
return err
}
} else {
block.CreatedBy = userID
block.CreateAt = utils.GetMillis()
insertQueryValues["created_by"] = block.CreatedBy
insertQueryValues["create_at"] = block.CreateAt
insertQueryValues["update_at"] = block.UpdateAt
insertQueryValues["modified_by"] = block.ModifiedBy
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks")
if _, err := query.Exec(); err != nil {
return err
}
}
// writing block history
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history")
if _, err := query.Exec(); err != nil {
return err
}
return nil
}
func (s *SQLStore) getLegacyBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) {
return s.getBoardsFieldsByCondition(db, legacyBoardFields(""), conditions...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"context"
"database/sql"
"embed"
"errors"
"fmt"
"strings"
"text/template"
sq "github.com/Masterminds/squirrel"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/morph"
drivers "github.com/mattermost/morph/drivers"
mysql "github.com/mattermost/morph/drivers/mysql"
postgres "github.com/mattermost/morph/drivers/postgres"
embedded "github.com/mattermost/morph/sources/embedded"
_ "github.com/lib/pq" // postgres driver
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
//go:embed migrations/*.sql
var Assets embed.FS
const (
uniqueIDsMigrationRequiredVersion = 14
teamLessBoardsMigrationRequiredVersion = 18
categoriesUUIDIDMigrationRequiredVersion = 20
deDuplicateCategoryBoards = 35
tempSchemaMigrationTableName = "temp_schema_migration"
)
var errChannelCreatorNotInTeam = errors.New("channel creator not found in user teams")
// migrations in MySQL need to run with the multiStatements flag
// enabled, so this method creates a new connection ensuring that it's
// enabled.
func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
connectionString := s.connectionString
if s.dbType == model.MysqlDBType {
var err error
connectionString, err = sqlstore.ResetReadTimeout(connectionString)
if err != nil {
return nil, err
}
connectionString, err = sqlstore.AppendMultipleStatementsFlag(connectionString)
if err != nil {
return nil, err
}
}
var settings mm_model.SqlSettings
settings.SetDefaults(false)
if s.configFn != nil {
settings = s.configFn().SqlSettings
}
*settings.DriverName = s.dbType
db := sqlstore.SetupConnection("master", connectionString, &settings)
return db, nil
}
func (s *SQLStore) Migrate() error {
if err := s.EnsureSchemaMigrationFormat(); err != nil {
return err
}
defer func() {
// the old schema migration table deletion happens after the
// migrations have run, to be able to recover its information
// in case there would be errors during the process.
if err := s.deleteOldSchemaMigrationTable(); err != nil {
s.logger.Error("cannot delete the old schema migration table", mlog.Err(err))
}
}()
var driver drivers.Driver
var err error
var db *sql.DB
s.logger.Debug("Getting migrations connection")
db, err = s.getMigrationConnection()
if err != nil {
return err
}
defer func() {
s.logger.Debug("Closing migrations connection")
db.Close()
}()
if s.dbType == model.PostgresDBType {
driver, err = postgres.WithInstance(db)
if err != nil {
return err
}
}
if s.dbType == model.MysqlDBType {
driver, err = mysql.WithInstance(db)
if err != nil {
return err
}
}
assetsList, err := Assets.ReadDir("migrations")
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, dirEntry := range assetsList {
assetNamesForDriver[i] = dirEntry.Name()
}
params := map[string]interface{}{
"prefix": s.tablePrefix,
"postgres": s.dbType == model.PostgresDBType,
"mysql": s.dbType == model.MysqlDBType,
"plugin": s.isPlugin,
"singleUser": s.isSingleUser,
}
migrationAssets := &embedded.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
asset, mErr := Assets.ReadFile("migrations/" + name)
if mErr != nil {
return nil, mErr
}
tmpl, pErr := template.New("sql").Funcs(s.GetTemplateHelperFuncs()).Parse(string(asset))
if pErr != nil {
return nil, pErr
}
buffer := bytes.NewBufferString("")
err = tmpl.Execute(buffer, params)
if err != nil {
return nil, err
}
s.logger.Trace("migration template",
mlog.String("name", name),
mlog.String("sql", buffer.String()),
)
return buffer.Bytes(), nil
},
}
src, err := embedded.WithInstance(migrationAssets)
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLock("boards-lock-key"),
morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", s.tablePrefix)),
morph.SetStatementTimeoutInSeconds(1000000),
}
s.logger.Debug("Creating migration engine")
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer func() {
s.logger.Debug("Closing migration engine")
engine.Close()
}()
return s.runMigrationSequence(engine, driver)
}
// runMigrationSequence executes all the migrations in order, both
// plain SQL and data migrations.
func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driver) error {
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, uniqueIDsMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunUniqueIDsMigration(); mErr != nil {
return fmt.Errorf("error running unique IDs migration: %w", mErr)
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, teamLessBoardsMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunTeamLessBoardsMigration(); mErr != nil {
return fmt.Errorf("error running teamless boards migration: %w", mErr)
}
if mErr := s.RunDeletedMembershipBoardsMigration(); mErr != nil {
return fmt.Errorf("error running deleted membership boards migration: %w", mErr)
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, categoriesUUIDIDMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunCategoryUUIDIDMigration(); mErr != nil {
return fmt.Errorf("error running categoryID migration: %w", mErr)
}
appliedMigrations, err := driver.AppliedMigrations()
if err != nil {
return err
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, deDuplicateCategoryBoards); mErr != nil {
return mErr
}
currentMigrationVersion := len(appliedMigrations)
if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); mErr != nil {
return mErr
}
s.logger.Debug("== Applying all remaining migrations ====================",
mlog.Int("current_version", len(appliedMigrations)),
)
if err := engine.ApplyAll(); err != nil {
return err
}
// always run the collations & charset fix-ups
if mErr := s.RunFixCollationsAndCharsetsMigration(); mErr != nil {
return fmt.Errorf("error running fix collations and charsets migration: %w", mErr)
}
return nil
}
func (s *SQLStore) ensureMigrationsAppliedUpToVersion(engine *morph.Morph, driver drivers.Driver, version int) error {
applied, err := driver.AppliedMigrations()
if err != nil {
return err
}
currentVersion := len(applied)
s.logger.Debug("== Ensuring migrations applied up to version ====================",
mlog.Int("version", version),
mlog.Int("current_version", currentVersion))
// if the target version is below or equal to the current one, do
// not migrate either because is not needed (both are equal) or
// because it would downgrade the database (is below)
if version <= currentVersion {
s.logger.Debug("-- There is no need of applying any migration --------------------")
return nil
}
for _, migration := range applied {
s.logger.Debug("-- Found applied migration --------------------", mlog.Uint32("version", migration.Version), mlog.String("name", migration.Name))
}
if _, err = engine.Apply(version - currentVersion); err != nil {
return err
}
return nil
}
func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap {
funcs := template.FuncMap{
"addColumnIfNeeded": s.genAddColumnIfNeeded,
"dropColumnIfNeeded": s.genDropColumnIfNeeded,
"createIndexIfNeeded": s.genCreateIndexIfNeeded,
"renameTableIfNeeded": s.genRenameTableIfNeeded,
"renameColumnIfNeeded": s.genRenameColumnIfNeeded,
"doesTableExist": s.doesTableExist,
"doesColumnExist": s.doesColumnExist,
"addConstraintIfNeeded": s.genAddConstraintIfNeeded,
}
return funcs
}
func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constraint string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"column_name": columnName,
"data_type": datatype,
"constraint": constraint,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[column_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] ADD COLUMN [[column_name]] [[data_type]] [[constraint]];'
));
PREPARE addColumnIfNeeded FROM @stmt;
EXECUTE addColumnIfNeeded;
DEALLOCATE PREPARE addColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nALTER TABLE %s ADD COLUMN IF NOT EXISTS %s %s %s;\n", normTableName, columnName, datatype, constraint), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"column_name": columnName,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[column_name]]'
) > 0,
'ALTER TABLE [[norm_table_name]] DROP COLUMN [[column_name]];',
'SELECT 1;'
));
PREPARE dropColumnIfNeeded FROM @stmt;
EXECUTE dropColumnIfNeeded;
DEALLOCATE PREPARE dropColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nALTER TABLE %s DROP COLUMN IF EXISTS %s;\n", normTableName, columnName), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genCreateIndexIfNeeded(tableName, columns string) (string, error) {
indexName := getIndexName(tableName, columns)
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"index_name": indexName,
"columns": columns,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(index_name) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND index_name = '[[index_name]]'
) > 0,
'SELECT 1;',
'CREATE INDEX [[index_name]] ON [[norm_table_name]] ([[columns]]);'
));
PREPARE createIndexIfNeeded FROM @stmt;
EXECUTE createIndexIfNeeded;
DEALLOCATE PREPARE createIndexIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nCREATE INDEX IF NOT EXISTS %s ON %s (%s);\n", indexName, normTableName, columns), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (string, error) {
oldTableName = addPrefixIfNeeded(oldTableName, s.tablePrefix)
newTableName = addPrefixIfNeeded(newTableName, s.tablePrefix)
normOldTableName := normalizeTablename(s.schemaName, oldTableName)
vars := map[string]string{
"schema": s.schemaName,
"table_name": newTableName,
"norm_old_table_name": normOldTableName,
"new_table_name": newTableName,
}
switch s.dbType {
case model.MysqlDBType:
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
) > 0,
'SELECT 1;',
'RENAME TABLE [[norm_old_table_name]] TO [[new_table_name]];'
));
PREPARE renameTableIfNeeded FROM @stmt;
EXECUTE renameTableIfNeeded;
DEALLOCATE PREPARE renameTableIfNeeded;
`, vars), nil
case model.PostgresDBType:
return replaceVars(`
do $$
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = '[[new_table_name]]'
AND table_schema = '[[schema]]'
) = 0 then
ALTER TABLE [[norm_old_table_name]] RENAME TO [[new_table_name]];
end if;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnName, dataType string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"old_column_name": oldColumnName,
"new_column_name": newColumnName,
"data_type": dataType,
}
switch s.dbType {
case model.MysqlDBType:
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[new_column_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] CHANGE [[old_column_name]] [[new_column_name]] [[data_type]];'
));
PREPARE renameColumnIfNeeded FROM @stmt;
EXECUTE renameColumnIfNeeded;
DEALLOCATE PREPARE renameColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return replaceVars(`
do $$
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[new_column_name]]'
) = 0 then
ALTER TABLE [[norm_table_name]] RENAME COLUMN [[old_column_name]] TO [[new_column_name]];
end if;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) doesTableExist(tableName string) (bool, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
query := s.getQueryBuilder(s.db).
Select("table_name").
From("INFORMATION_SCHEMA.TABLES").
Where(sq.Eq{
"table_name": tableName,
"table_schema": s.schemaName,
})
rows, err := query.Query()
if err != nil {
s.logger.Error(`doesTableExist ERROR`, mlog.Err(err))
return false, err
}
defer s.CloseRows(rows)
exists := rows.Next()
sql, _, _ := query.ToSql()
s.logger.Trace("doesTableExist",
mlog.String("table", tableName),
mlog.Bool("exists", exists),
mlog.String("sql", sql),
)
return exists, nil
}
func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
query := s.getQueryBuilder(s.db).
Select("table_name").
From("INFORMATION_SCHEMA.COLUMNS").
Where(sq.Eq{
"table_name": tableName,
"table_schema": s.schemaName,
"column_name": columnName,
})
rows, err := query.Query()
if err != nil {
s.logger.Error(`doesColumnExist ERROR`, mlog.Err(err))
return false, err
}
defer s.CloseRows(rows)
exists := rows.Next()
sql, _, _ := query.ToSql()
s.logger.Trace("doesColumnExist",
mlog.String("table", tableName),
mlog.String("column", columnName),
mlog.Bool("exists", exists),
mlog.String("sql", sql),
)
return exists, nil
}
func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
var query string
vars := map[string]string{
"schema": s.schemaName,
"constraint_name": constraintName,
"constraint_type": constraintType,
"table_name": tableName,
"constraint_definition": constraintDefinition,
"norm_table_name": normTableName,
}
switch s.dbType {
case model.MysqlDBType:
query = replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];'
));
PREPARE addConstraintIfNeeded FROM @stmt;
EXECUTE addConstraintIfNeeded;
DEALLOCATE PREPARE addConstraintIfNeeded;
`, vars)
case model.PostgresDBType:
query = replaceVars(`
DO
$$
BEGIN
IF NOT EXISTS (
SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) THEN
ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];
END IF;
END;
$$
LANGUAGE plpgsql;
`, vars)
}
return query, nil
}
func addPrefixIfNeeded(s, prefix string) string {
if !strings.HasPrefix(s, prefix) {
return prefix + s
}
return s
}
func normalizeTablename(schemaName, tableName string) string {
if schemaName != "" && !strings.HasPrefix(tableName, schemaName+".") {
tableName = schemaName + "." + tableName
}
return tableName
}
func getIndexName(tableName string, columns string) string {
var sb strings.Builder
_, _ = sb.WriteString("idx_")
_, _ = sb.WriteString(tableName)
// allow developers to separate column names with spaces and/or commas
columns = strings.ReplaceAll(columns, ",", " ")
cols := strings.Split(columns, " ")
for _, s := range cols {
sub := strings.TrimSpace(s)
if sub == "" {
continue
}
_, _ = sb.WriteString("_")
_, _ = sb.WriteString(s)
}
return sb.String()
}
// replaceVars replaces instances of variable placeholders with the
// values provided via a map. Variable placeholders are of the form
// `[[var_name]]`.
func replaceVars(s string, vars map[string]string) string {
for key, val := range vars {
placeholder := "[[" + key + "]]"
val = strings.ReplaceAll(val, "'", "\\'")
s = strings.ReplaceAll(s, placeholder, val)
}
return s
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var notificationHintFields = []string{
"block_type",
"block_id",
"modified_by_id",
"create_at",
"notify_at",
}
func valuesForNotificationHint(hint *model.NotificationHint) []interface{} {
return []interface{}{
hint.BlockType,
hint.BlockID,
hint.ModifiedByID,
hint.CreateAt,
hint.NotifyAt,
}
}
func (s *SQLStore) notificationHintFromRows(rows *sql.Rows) ([]*model.NotificationHint, error) {
hints := []*model.NotificationHint{}
for rows.Next() {
var hint model.NotificationHint
err := rows.Scan(
&hint.BlockType,
&hint.BlockID,
&hint.ModifiedByID,
&hint.CreateAt,
&hint.NotifyAt,
)
if err != nil {
return nil, err
}
hints = append(hints, &hint)
}
return hints, nil
}
// upsertNotificationHint creates or updates a notification hint. When updating the `notify_at` is set
// to the current time plus `notifyFreq`.
func (s *SQLStore) upsertNotificationHint(db sq.BaseRunner, hint *model.NotificationHint, notifyFreq time.Duration) (*model.NotificationHint, error) {
if err := hint.IsValid(); err != nil {
return nil, err
}
hint.CreateAt = utils.GetMillis()
notifyAt := utils.GetMillisForTime(time.Now().Add(notifyFreq))
hint.NotifyAt = notifyAt
query := s.getQueryBuilder(db).Insert(s.tablePrefix + "notification_hints").
Columns(notificationHintFields...).
Values(valuesForNotificationHint(hint)...)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE notify_at = ?", notifyAt)
} else {
query = query.Suffix("ON CONFLICT (block_id) DO UPDATE SET notify_at = ?", notifyAt)
}
if _, err := query.Exec(); err != nil {
s.logger.Error("Cannot upsert notification hint",
mlog.String("block_id", hint.BlockID),
mlog.Err(err),
)
return nil, err
}
return hint, nil
}
// deleteNotificationHint deletes the notification hint for the specified block.
func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, blockID string) error {
query := s.getQueryBuilder(db).
Delete(s.tablePrefix + "notification_hints").
Where(sq.Eq{"block_id": blockID})
result, err := query.Exec()
if err != nil {
return err
}
count, err := result.RowsAffected()
if err != nil {
return err
}
if count == 0 {
return model.NewErrNotFound("notification hint BlockID=" + blockID)
}
return nil
}
// getNotificationHint fetches the notification hint for the specified block.
func (s *SQLStore) getNotificationHint(db sq.BaseRunner, blockID string) (*model.NotificationHint, error) {
query := s.getQueryBuilder(db).
Select(notificationHintFields...).
From(s.tablePrefix + "notification_hints").
Where(sq.Eq{"block_id": blockID})
rows, err := query.Query()
if err != nil {
s.logger.Error("Cannot fetch notification hint",
mlog.String("block_id", blockID),
mlog.Err(err),
)
return nil, err
}
defer s.CloseRows(rows)
hint, err := s.notificationHintFromRows(rows)
if err != nil {
s.logger.Error("Cannot get notification hint",
mlog.String("block_id", blockID),
mlog.Err(err),
)
return nil, err
}
if len(hint) == 0 {
return nil, model.NewErrNotFound("notification hint BlockID=" + blockID)
}
return hint[0], nil
}
// getNextNotificationHint fetches the next scheduled notification hint. If remove is true
// then the hint is removed from the database as well, as if popping from a stack.
func (s *SQLStore) getNextNotificationHint(db sq.BaseRunner, remove bool) (*model.NotificationHint, error) {
selectQuery := s.getQueryBuilder(db).
Select(notificationHintFields...).
From(s.tablePrefix + "notification_hints").
OrderBy("notify_at").
Limit(1)
rows, err := selectQuery.Query()
if err != nil {
s.logger.Error("Cannot fetch next notification hint",
mlog.Err(err),
)
return nil, err
}
defer s.CloseRows(rows)
hints, err := s.notificationHintFromRows(rows)
if err != nil {
s.logger.Error("Cannot get next notification hint",
mlog.Err(err),
)
return nil, err
}
if len(hints) == 0 {
return nil, model.NewErrNotFound("next notification hint")
}
hint := hints[0]
if remove {
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "notification_hints").
Where(sq.Eq{"block_id": hint.BlockID})
result, err := deleteQuery.Exec()
if err != nil {
return nil, fmt.Errorf("cannot delete while getting next notification hint: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return nil, fmt.Errorf("cannot verify delete while getting next notification hint: %w", err)
}
if rows == 0 {
// another node likely has grabbed this hint for processing concurrently; let that node handle it
// and we'll return an error here so we try again.
return nil, model.NewErrNotFound("notification hint")
}
}
return hint, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// servicesAPI is the interface required my the Params to interact with the mattermost-server.
// You can use plugin-api or product-api adapter implementations.
type servicesAPI interface {
GetChannelByID(string) (*mm_model.Channel, error)
}
type Params struct {
DBType string
ConnectionString string
TablePrefix string
Logger mlog.LoggerIFace
DB *sql.DB
IsPlugin bool
IsSingleUser bool
ServicesAPI servicesAPI
SkipMigrations bool
ConfigFn func() *mm_model.Config
}
func (p Params) CheckValid() error {
return nil
}
type ErrStoreParam struct {
name string
issue string
}
func (e ErrStoreParam) Error() string {
return fmt.Sprintf("invalid store params: %s %s", e.name, e.issue)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make generate" from the Store interface
// DO NOT EDIT
// To add a public method, create an entry in the Store interface,
// prefix it with a @withTransaction comment if you need it to be
// transactional and then add a private method in the store itself
// with db sq.BaseRunner as the first parameter before running `make
// generate`
package sqlstore
import (
"context"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, boardIDs []string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.addUpdateCategoryBoard(tx, userID, categoryID, boardIDs)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "AddUpdateCategoryBoard"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) CanSeeUser(seerID string, seenID string) (bool, error) {
return s.canSeeUser(s.db, seerID, seenID)
}
func (s *SQLStore) CleanUpSessions(expireTime int64) error {
return s.cleanUpSessions(s.db, expireTime)
}
func (s *SQLStore) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, txErr
}
result, err := s.createBoardsAndBlocks(tx, bab, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocks"))
}
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return result, nil
}
func (s *SQLStore) CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, nil, txErr
}
result, resultVar1, err := s.createBoardsAndBlocksWithAdmin(tx, bab, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocksWithAdmin"))
}
return nil, nil, err
}
if err := tx.Commit(); err != nil {
return nil, nil, err
}
return result, resultVar1, nil
}
func (s *SQLStore) CreateCategory(category model.Category) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.createCategory(tx, category)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateCategory"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) CreateSession(session *model.Session) error {
return s.createSession(s.db, session)
}
func (s *SQLStore) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
return s.createSubscription(s.db, sub)
}
func (s *SQLStore) CreateUser(user *model.User) (*model.User, error) {
return s.createUser(s.db, user)
}
func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.deleteBlock(tx, blockID, modifiedBy)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBlock"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) DeleteBlockRecord(blockID string, modifiedBy string) error {
return s.deleteBlockRecord(s.db, blockID, modifiedBy)
}
func (s *SQLStore) DeleteBoard(boardID string, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.deleteBoard(tx, boardID, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoard"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) DeleteBoardRecord(boardID string, modifiedBy string) error {
return s.deleteBoardRecord(s.db, boardID, modifiedBy)
}
func (s *SQLStore) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.deleteBoardsAndBlocks(tx, dbab, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoardsAndBlocks"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) DeleteCategory(categoryID string, userID string, teamID string) error {
return s.deleteCategory(s.db, categoryID, userID, teamID)
}
func (s *SQLStore) DeleteMember(boardID string, userID string) error {
return s.deleteMember(s.db, boardID, userID)
}
func (s *SQLStore) DeleteNotificationHint(blockID string) error {
return s.deleteNotificationHint(s.db, blockID)
}
func (s *SQLStore) DeleteSession(sessionID string) error {
return s.deleteSession(s.db, sessionID)
}
func (s *SQLStore) DeleteSubscription(blockID string, subscriberID string) error {
return s.deleteSubscription(s.db, blockID, subscriberID)
}
func (s *SQLStore) DropAllTables() error {
return s.dropAllTables(s.db)
}
func (s *SQLStore) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, txErr
}
result, err := s.duplicateBlock(tx, boardID, blockID, userID, asTemplate)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBlock"))
}
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return result, nil
}
func (s *SQLStore) DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, nil, txErr
}
result, resultVar1, err := s.duplicateBoard(tx, boardID, userID, toTeam, asTemplate)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBoard"))
}
return nil, nil, err
}
if err := tx.Commit(); err != nil {
return nil, nil, err
}
return result, resultVar1, nil
}
func (s *SQLStore) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
return s.getActiveUserCount(s.db, updatedSecondsAgo)
}
func (s *SQLStore) GetAllTeams() ([]*model.Team, error) {
return s.getAllTeams(s.db)
}
func (s *SQLStore) GetBlock(blockID string) (*model.Block, error) {
return s.getBlock(s.db, blockID)
}
func (s *SQLStore) GetBlockCountsByType() (map[string]int64, error) {
return s.getBlockCountsByType(s.db)
}
func (s *SQLStore) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
return s.getBlockHistory(s.db, blockID, opts)
}
func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
return s.getBlockHistoryDescendants(s.db, boardID, opts)
}
func (s *SQLStore) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
return s.getBlockHistoryNewestChildren(s.db, parentID, opts)
}
func (s *SQLStore) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) {
return s.getBlocks(s.db, opts)
}
func (s *SQLStore) GetBlocksByIDs(ids []string) ([]*model.Block, error) {
return s.getBlocksByIDs(s.db, ids)
}
func (s *SQLStore) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
return s.getBlocksComplianceHistory(s.db, opts)
}
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]*model.Block, error) {
return s.getBlocksForBoard(s.db, boardID)
}
func (s *SQLStore) GetBlocksWithParent(boardID string, parentID string) ([]*model.Block, error) {
return s.getBlocksWithParent(s.db, boardID, parentID)
}
func (s *SQLStore) GetBlocksWithParentAndType(boardID string, parentID string, blockType string) ([]*model.Block, error) {
return s.getBlocksWithParentAndType(s.db, boardID, parentID, blockType)
}
func (s *SQLStore) GetBlocksWithType(boardID string, blockType string) ([]*model.Block, error) {
return s.getBlocksWithType(s.db, boardID, blockType)
}
func (s *SQLStore) GetBoard(id string) (*model.Board, error) {
return s.getBoard(s.db, id)
}
func (s *SQLStore) GetBoardAndCard(block *model.Block) (*model.Board, *model.Block, error) {
return s.getBoardAndCard(s.db, block)
}
func (s *SQLStore) GetBoardAndCardByID(blockID string) (*model.Board, *model.Block, error) {
return s.getBoardAndCardByID(s.db, blockID)
}
func (s *SQLStore) GetBoardCount() (int64, error) {
return s.getBoardCount(s.db)
}
func (s *SQLStore) GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) {
return s.getBoardHistory(s.db, boardID, opts)
}
func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
return s.getBoardMemberHistory(s.db, boardID, userID, limit)
}
func (s *SQLStore) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
return s.getBoardsComplianceHistory(s.db, opts)
}
func (s *SQLStore) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
return s.getBoardsForCompliance(s.db, opts)
}
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) {
return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards)
}
func (s *SQLStore) GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error) {
return s.getBoardsInTeamByIds(s.db, boardIDs, teamID)
}
func (s *SQLStore) GetCardLimitTimestamp() (int64, error) {
return s.getCardLimitTimestamp(s.db)
}
func (s *SQLStore) GetCategory(id string) (*model.Category, error) {
return s.getCategory(s.db, id)
}
func (s *SQLStore) GetChannel(teamID string, channelID string) (*mm_model.Channel, error) {
return s.getChannel(s.db, teamID, channelID)
}
func (s *SQLStore) GetCloudLimits() (*mm_model.ProductLimits, error) {
return s.getCloudLimits(s.db)
}
func (s *SQLStore) GetFileInfo(id string) (*mm_model.FileInfo, error) {
return s.getFileInfo(s.db, id)
}
func (s *SQLStore) GetLicense() *mm_model.License {
return s.getLicense(s.db)
}
func (s *SQLStore) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) {
return s.getMemberForBoard(s.db, boardID, userID)
}
func (s *SQLStore) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
return s.getMembersForBoard(s.db, boardID)
}
func (s *SQLStore) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
return s.getMembersForUser(s.db, userID)
}
func (s *SQLStore) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) {
return s.getNextNotificationHint(s.db, remove)
}
func (s *SQLStore) GetNotificationHint(blockID string) (*model.NotificationHint, error) {
return s.getNotificationHint(s.db, blockID)
}
func (s *SQLStore) GetRegisteredUserCount() (int, error) {
return s.getRegisteredUserCount(s.db)
}
func (s *SQLStore) GetSession(token string, expireTime int64) (*model.Session, error) {
return s.getSession(s.db, token, expireTime)
}
func (s *SQLStore) GetSharing(rootID string) (*model.Sharing, error) {
return s.getSharing(s.db, rootID)
}
func (s *SQLStore) GetSubTree2(boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
return s.getSubTree2(s.db, boardID, blockID, opts)
}
func (s *SQLStore) GetSubscribersCountForBlock(blockID string) (int, error) {
return s.getSubscribersCountForBlock(s.db, blockID)
}
func (s *SQLStore) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) {
return s.getSubscribersForBlock(s.db, blockID)
}
func (s *SQLStore) GetSubscription(blockID string, subscriberID string) (*model.Subscription, error) {
return s.getSubscription(s.db, blockID, subscriberID)
}
func (s *SQLStore) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) {
return s.getSubscriptions(s.db, subscriberID)
}
func (s *SQLStore) GetSystemSetting(key string) (string, error) {
return s.getSystemSetting(s.db, key)
}
func (s *SQLStore) GetSystemSettings() (map[string]string, error) {
return s.getSystemSettings(s.db)
}
func (s *SQLStore) GetTeam(ID string) (*model.Team, error) {
return s.getTeam(s.db, ID)
}
func (s *SQLStore) GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
return s.getTeamBoardsInsights(s.db, teamID, since, offset, limit, boardIDs)
}
func (s *SQLStore) GetTeamCount() (int64, error) {
return s.getTeamCount(s.db)
}
func (s *SQLStore) GetTeamsForUser(userID string) ([]*model.Team, error) {
return s.getTeamsForUser(s.db, userID)
}
func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Board, error) {
return s.getTemplateBoards(s.db, teamID, userID)
}
func (s *SQLStore) GetUsedCardsCount() (int, error) {
return s.getUsedCardsCount(s.db)
}
func (s *SQLStore) GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
return s.getUserBoardsInsights(s.db, teamID, userID, since, offset, limit, boardIDs)
}
func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) {
return s.getUserByEmail(s.db, email)
}
func (s *SQLStore) GetUserByID(userID string) (*model.User, error) {
return s.getUserByID(s.db, userID)
}
func (s *SQLStore) GetUserByUsername(username string) (*model.User, error) {
return s.getUserByUsername(s.db, username)
}
func (s *SQLStore) GetUserCategories(userID string, teamID string) ([]model.Category, error) {
return s.getUserCategories(s.db, userID, teamID)
}
func (s *SQLStore) GetUserCategoryBoards(userID string, teamID string) ([]model.CategoryBoards, error) {
return s.getUserCategoryBoards(s.db, userID, teamID)
}
func (s *SQLStore) GetUserPreferences(userID string) (mm_model.Preferences, error) {
return s.getUserPreferences(s.db, userID)
}
func (s *SQLStore) GetUserTimezone(userID string) (string, error) {
return s.getUserTimezone(s.db, userID)
}
func (s *SQLStore) GetUsersByTeam(teamID string, asGuestID string, showEmail bool, showName bool) ([]*model.User, error) {
return s.getUsersByTeam(s.db, teamID, asGuestID, showEmail, showName)
}
func (s *SQLStore) GetUsersList(userIDs []string, showEmail bool, showName bool) ([]*model.User, error) {
return s.getUsersList(s.db, userIDs, showEmail, showName)
}
func (s *SQLStore) InsertBlock(block *model.Block, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.insertBlock(tx, block, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlock"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) InsertBlocks(blocks []*model.Block, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.insertBlocks(tx, blocks, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlocks"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) InsertBoard(board *model.Board, userID string) (*model.Board, error) {
return s.insertBoard(s.db, board, userID)
}
func (s *SQLStore) InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, nil, txErr
}
result, resultVar1, err := s.insertBoardWithAdmin(tx, board, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBoardWithAdmin"))
}
return nil, nil, err
}
if err := tx.Commit(); err != nil {
return nil, nil, err
}
return result, resultVar1, nil
}
func (s *SQLStore) PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.patchBlock(tx, blockID, blockPatch, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlock"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.patchBlocks(tx, blockPatches, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlocks"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, txErr
}
result, err := s.patchBoard(tx, boardID, boardPatch, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoard"))
}
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return result, nil
}
func (s *SQLStore) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return nil, txErr
}
result, err := s.patchBoardsAndBlocks(tx, pbab, userID)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoardsAndBlocks"))
}
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return result, nil
}
func (s *SQLStore) PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) {
return s.patchUserPreferences(s.db, userID, patch)
}
func (s *SQLStore) PostMessage(message string, postType string, channelID string) error {
return s.postMessage(s.db, message, postType, channelID)
}
func (s *SQLStore) RefreshSession(session *model.Session) error {
return s.refreshSession(s.db, session)
}
func (s *SQLStore) RemoveDefaultTemplates(boards []*model.Board) error {
return s.removeDefaultTemplates(s.db, boards)
}
func (s *SQLStore) ReorderCategories(userID string, teamID string, newCategoryOrder []string) ([]string, error) {
return s.reorderCategories(s.db, userID, teamID, newCategoryOrder)
}
func (s *SQLStore) ReorderCategoryBoards(categoryID string, newBoardsOrder []string) ([]string, error) {
return s.reorderCategoryBoards(s.db, categoryID, newBoardsOrder)
}
func (s *SQLStore) RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error) {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return 0, txErr
}
result, err := s.runDataRetention(tx, globalRetentionDate, batchSize)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "RunDataRetention"))
}
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, err
}
return result, nil
}
func (s *SQLStore) SaveFileInfo(fileInfo *mm_model.FileInfo) error {
return s.saveFileInfo(s.db, fileInfo)
}
func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) {
return s.saveMember(s.db, bm)
}
func (s *SQLStore) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) {
return s.searchBoardsForUser(s.db, term, searchField, userID, includePublicBoards)
}
func (s *SQLStore) SearchBoardsForUserInTeam(teamID string, term string, userID string) ([]*model.Board, error) {
return s.searchBoardsForUserInTeam(s.db, teamID, term, userID)
}
func (s *SQLStore) SearchUserChannels(teamID string, userID string, query string) ([]*mm_model.Channel, error) {
return s.searchUserChannels(s.db, teamID, userID, query)
}
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots bool, showEmail bool, showName bool) ([]*model.User, error) {
return s.searchUsersByTeam(s.db, teamID, searchQuery, asGuestID, excludeBots, showEmail, showName)
}
func (s *SQLStore) SendMessage(message string, postType string, receipts []string) error {
return s.sendMessage(s.db, message, postType, receipts)
}
func (s *SQLStore) SetBoardVisibility(userID string, categoryID string, boardID string, visible bool) error {
return s.setBoardVisibility(s.db, userID, categoryID, boardID, visible)
}
func (s *SQLStore) SetSystemSetting(key string, value string) error {
return s.setSystemSetting(s.db, key, value)
}
func (s *SQLStore) UndeleteBlock(blockID string, modifiedBy string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.undeleteBlock(tx, blockID, modifiedBy)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "UndeleteBlock"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) UndeleteBoard(boardID string, modifiedBy string) error {
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
err := s.undeleteBoard(tx, boardID, modifiedBy)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "UndeleteBoard"))
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (s *SQLStore) UpdateCardLimitTimestamp(cardLimit int) (int64, error) {
return s.updateCardLimitTimestamp(s.db, cardLimit)
}
func (s *SQLStore) UpdateCategory(category model.Category) error {
return s.updateCategory(s.db, category)
}
func (s *SQLStore) UpdateSession(session *model.Session) error {
return s.updateSession(s.db, session)
}
func (s *SQLStore) UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error {
return s.updateSubscribersNotifiedAt(s.db, blockID, notifiedAt)
}
func (s *SQLStore) UpdateUser(user *model.User) (*model.User, error) {
return s.updateUser(s.db, user)
}
func (s *SQLStore) UpdateUserPassword(username string, password string) error {
return s.updateUserPassword(s.db, username, password)
}
func (s *SQLStore) UpdateUserPasswordByID(userID string, password string) error {
return s.updateUserPasswordByID(s.db, userID, password)
}
func (s *SQLStore) UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) {
return s.upsertNotificationHint(s.db, hint, notificationFreq)
}
func (s *SQLStore) UpsertSharing(sharing model.Sharing) error {
return s.upsertSharing(s.db, sharing)
}
func (s *SQLStore) UpsertTeamSettings(team model.Team) error {
return s.upsertTeamSettings(s.db, team)
}
func (s *SQLStore) UpsertTeamSignupToken(team model.Team) error {
return s.upsertTeamSignupToken(s.db, team)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"fmt"
"io"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/morph/models"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// EnsureSchemaMigrationFormat checks the schema migrations table
// format and, if it's not using the new shape, it migrates the old
// one's status before initializing the migrations engine.
func (s *SQLStore) EnsureSchemaMigrationFormat() error {
migrationNeeded, err := s.isSchemaMigrationNeeded()
if err != nil {
return err
}
if !migrationNeeded {
s.logger.Info("Schema migration table is correct format")
return nil
}
s.logger.Info("Migrating schema migration to new format")
legacySchemaVersion, err := s.getLegacySchemaVersion()
if err != nil {
return err
}
migrations, err := getEmbeddedMigrations()
if err != nil {
return err
}
filteredMigrations := filterMigrations(migrations, legacySchemaVersion)
if err := s.createTempSchemaTable(); err != nil {
return err
}
s.logger.Info("Populating the temporal schema table", mlog.Uint32("legacySchemaVersion", legacySchemaVersion), mlog.Int("migrations", len(filteredMigrations)))
if err := s.populateTempSchemaTable(filteredMigrations); err != nil {
return err
}
if err := s.useNewSchemaTable(); err != nil {
return err
}
return nil
}
// getEmbeddedMigrations returns a list of the embedded migrations
// using the morph migration format. The migrations do not have the
// contents set, as the goal is to obtain a list of them.
func getEmbeddedMigrations() ([]*models.Migration, error) {
assetsList, err := Assets.ReadDir("migrations")
if err != nil {
return nil, err
}
migrations := []*models.Migration{}
for _, f := range assetsList {
m, err := models.NewMigration(io.NopCloser(&bytes.Buffer{}), f.Name())
if err != nil {
return nil, err
}
if m.Direction != models.Up {
continue
}
migrations = append(migrations, m)
}
return migrations, nil
}
// filterMigrations takes the whole list of migrations parsed from the
// embedded directory and returns a filtered list that only contains
// one migration per version and those migrations that have already
// run based on the legacySchemaVersion.
func filterMigrations(migrations []*models.Migration, legacySchemaVersion uint32) []*models.Migration {
filteredMigrations := []*models.Migration{}
for _, migration := range migrations {
// we only take into account up migrations to avoid duplicates
if migration.Direction != models.Up {
continue
}
// we're only interested on registering migrations that
// already run, so we skip those above the legacy version
if migration.Version > legacySchemaVersion {
continue
}
filteredMigrations = append(filteredMigrations, migration)
}
return filteredMigrations
}
func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) {
// Check if `name` column exists on schema version table.
// This column exists only for the new schema version table.
query := s.getQueryBuilder(s.db).
Select("COLUMN_NAME").
From("information_schema.COLUMNS").
Where(sq.Eq{
"TABLE_NAME": s.tablePrefix + "schema_migrations",
})
switch s.dbType {
case model.MysqlDBType:
query = query.Where(sq.Eq{"TABLE_SCHEMA": s.schemaName})
case model.PostgresDBType:
query = query.Where(sq.Eq{"TABLE_SCHEMA": "current_schema()"})
}
rows, err := query.Query()
if err != nil {
s.logger.Error("failed to fetch columns in schema_migrations table", mlog.Err(err))
return false, err
}
defer s.CloseRows(rows)
data := []string{}
for rows.Next() {
var columnName string
err := rows.Scan(&columnName)
if err != nil {
s.logger.Error("error scanning rows from schema_migrations table definition", mlog.Err(err))
return false, err
}
data = append(data, columnName)
}
if len(data) == 0 {
// if no data then table does not exist and therefore a schema migration is not needed.
return false, nil
}
for _, columnName := range data {
// look for a column named 'name', if found then no migration is needed
if strings.ToLower(columnName) == "name" {
return false, nil
}
}
return true, nil
}
func (s *SQLStore) getLegacySchemaVersion() (uint32, error) {
query := s.getQueryBuilder(s.db).
Select("version").
From(s.tablePrefix + "schema_migrations")
row := query.QueryRow()
var version uint32
if err := row.Scan(&version); err != nil {
s.logger.Error("error fetching legacy schema version", mlog.Err(err))
return version, err
}
return version, nil
}
func (s *SQLStore) createTempSchemaTable() error {
// squirrel doesn't support DDL query in query builder
// so, we need to use a plain old string
query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (Version bigint NOT NULL, Name varchar(64) NOT NULL, PRIMARY KEY (Version))", s.tablePrefix+tempSchemaMigrationTableName)
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("failed to create temporary schema migration table", mlog.Err(err))
s.logger.Error("createTempSchemaTable error " + err.Error())
return err
}
return nil
}
func (s *SQLStore) populateTempSchemaTable(migrations []*models.Migration) error {
query := s.getQueryBuilder(s.db).
Insert(s.tablePrefix+tempSchemaMigrationTableName).
Columns("Version", "Name")
for _, migration := range migrations {
s.logger.Info("-- Registering migration", mlog.Uint32("version", migration.Version), mlog.String("name", migration.Name))
query = query.Values(migration.Version, migration.Name)
}
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to insert migration records into temporary schema table", mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) useNewSchemaTable() error {
// first delete the old table, then
// rename the new table to old table's name
// renaming old schema migration table. Will delete later once the migration is
// complete, just in case.
var query string
if s.dbType == model.MysqlDBType {
query = fmt.Sprintf("RENAME TABLE `%sschema_migrations` TO `%sschema_migrations_old_temp`", s.tablePrefix, s.tablePrefix)
} else {
query = fmt.Sprintf("ALTER TABLE %sschema_migrations RENAME TO %sschema_migrations_old_temp", s.tablePrefix, s.tablePrefix)
}
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("failed to rename old schema migration table", mlog.Err(err))
return err
}
// renaming new temp table to old table's name
if s.dbType == model.MysqlDBType {
query = fmt.Sprintf("RENAME TABLE `%s%s` TO `%sschema_migrations`", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix)
} else {
query = fmt.Sprintf("ALTER TABLE %s%s RENAME TO %sschema_migrations", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix)
}
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("failed to rename temp schema table", mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) deleteOldSchemaMigrationTable() error {
query := "DROP TABLE IF EXISTS " + s.tablePrefix + "schema_migrations_old_temp"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("failed to delete old temp schema migrations table", mlog.Err(err))
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"encoding/json"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
// GetActiveUserCount returns the number of users with active sessions within N seconds ago.
func (s *SQLStore) getActiveUserCount(db sq.BaseRunner, updatedSecondsAgo int64) (int, error) {
query := s.getQueryBuilder(db).
Select("count(distinct user_id)").
From(s.tablePrefix + "sessions").
Where(sq.Gt{"update_at": utils.GetMillis() - utils.SecondsToMillis(updatedSecondsAgo)})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *SQLStore) getSession(db sq.BaseRunner, token string, expireTimeSeconds int64) (*model.Session, error) {
query := s.getQueryBuilder(db).
Select("id", "token", "user_id", "auth_service", "props").
From(s.tablePrefix + "sessions").
Where(sq.Eq{"token": token}).
Where(sq.Gt{"update_at": utils.GetMillis() - utils.SecondsToMillis(expireTimeSeconds)})
row := query.QueryRow()
session := model.Session{}
var propsBytes []byte
err := row.Scan(&session.ID, &session.Token, &session.UserID, &session.AuthService, &propsBytes)
if err != nil {
return nil, err
}
err = json.Unmarshal(propsBytes, &session.Props)
if err != nil {
return nil, err
}
return &session, nil
}
func (s *SQLStore) createSession(db sq.BaseRunner, session *model.Session) error {
now := utils.GetMillis()
propsBytes, err := json.Marshal(session.Props)
if err != nil {
return err
}
query := s.getQueryBuilder(db).Insert(s.tablePrefix+"sessions").
Columns("id", "token", "user_id", "auth_service", "props", "create_at", "update_at").
Values(session.ID, session.Token, session.UserID, session.AuthService, propsBytes, now, now)
_, err = query.Exec()
return err
}
func (s *SQLStore) refreshSession(db sq.BaseRunner, session *model.Session) error {
now := utils.GetMillis()
query := s.getQueryBuilder(db).Update(s.tablePrefix+"sessions").
Where(sq.Eq{"token": session.Token}).
Set("update_at", now)
_, err := query.Exec()
return err
}
func (s *SQLStore) updateSession(db sq.BaseRunner, session *model.Session) error {
now := utils.GetMillis()
propsBytes, err := json.Marshal(session.Props)
if err != nil {
return err
}
query := s.getQueryBuilder(db).Update(s.tablePrefix+"sessions").
Where(sq.Eq{"token": session.Token}).
Set("update_at", now).
Set("props", propsBytes)
_, err = query.Exec()
return err
}
func (s *SQLStore) deleteSession(db sq.BaseRunner, sessionID string) error {
query := s.getQueryBuilder(db).Delete(s.tablePrefix + "sessions").
Where(sq.Eq{"id": sessionID})
_, err := query.Exec()
return err
}
func (s *SQLStore) cleanUpSessions(db sq.BaseRunner, expireTimeSeconds int64) error {
query := s.getQueryBuilder(db).Delete(s.tablePrefix + "sessions").
Where(sq.Lt{"update_at": utils.GetMillis() - utils.SecondsToMillis(expireTimeSeconds)})
_, err := query.Exec()
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
)
func (s *SQLStore) upsertSharing(db sq.BaseRunner, sharing model.Sharing) error {
now := utils.GetMillis()
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"sharing").
Columns(
"id",
"enabled",
"token",
"modified_by",
"update_at",
).
Values(
sharing.ID,
sharing.Enabled,
sharing.Token,
sharing.ModifiedBy,
now,
)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE enabled = ?, token = ?, modified_by = ?, update_at = ?",
sharing.Enabled, sharing.Token, sharing.ModifiedBy, now)
} else {
query = query.Suffix(
`ON CONFLICT (id)
DO UPDATE SET enabled = EXCLUDED.enabled, token = EXCLUDED.token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`,
)
}
_, err := query.Exec()
return err
}
func (s *SQLStore) getSharing(db sq.BaseRunner, boardID string) (*model.Sharing, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"enabled",
"token",
"modified_by",
"update_at",
).
From(s.tablePrefix + "sharing").
Where(sq.Eq{"id": boardID})
row := query.QueryRow()
sharing := model.Sharing{}
err := row.Scan(
&sharing.ID,
&sharing.Enabled,
&sharing.Token,
&sharing.ModifiedBy,
&sharing.UpdateAt,
)
if err != nil {
return nil, err
}
return &sharing, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"net/url"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// SQLStore is a SQL database.
type SQLStore struct {
db *sql.DB
dbType string
tablePrefix string
connectionString string
isPlugin bool
isSingleUser bool
logger mlog.LoggerIFace
servicesAPI servicesAPI
isBinaryParam bool
schemaName string
configFn func() *mm_model.Config
}
// New creates a new SQL implementation of the store.
func New(params Params) (*SQLStore, error) {
if err := params.CheckValid(); err != nil {
return nil, err
}
params.Logger.Info("connectDatabase", mlog.String("dbType", params.DBType))
store := &SQLStore{
// TODO: add replica DB support too.
db: params.DB,
dbType: params.DBType,
tablePrefix: params.TablePrefix,
connectionString: params.ConnectionString,
logger: params.Logger,
isPlugin: params.IsPlugin,
isSingleUser: params.IsSingleUser,
servicesAPI: params.ServicesAPI,
configFn: params.ConfigFn,
}
var err error
store.isBinaryParam, err = store.computeBinaryParam()
if err != nil {
params.Logger.Error(`Cannot compute binary parameter`, mlog.Err(err))
return nil, err
}
store.schemaName, err = store.GetSchemaName()
if err != nil {
params.Logger.Error(`Cannot get schema name`, mlog.Err(err))
return nil, err
}
if !params.SkipMigrations {
if mErr := store.Migrate(); mErr != nil {
params.Logger.Error(`Table creation / migration failed`, mlog.Err(mErr))
return nil, mErr
}
}
return store, nil
}
func (s *SQLStore) IsMariaDB() bool {
if s.dbType != model.MysqlDBType {
return false
}
row := s.db.QueryRow("SELECT Version()")
var version string
if err := row.Scan(&version); err != nil {
s.logger.Error("error checking database version", mlog.Err(err))
return false
}
return strings.Contains(strings.ToLower(version), "mariadb")
}
// computeBinaryParam returns whether the data source uses binary_parameters
// when using Postgres.
func (s *SQLStore) computeBinaryParam() (bool, error) {
if s.dbType != model.PostgresDBType {
return false, nil
}
url, err := url.Parse(s.connectionString)
if err != nil {
return false, err
}
return url.Query().Get("binary_parameters") == "yes", nil
}
// Shutdown close the connection with the store.
func (s *SQLStore) Shutdown() error {
return s.db.Close()
}
// DBHandle returns the raw sql.DB handle.
// It is used by the mattermostauthlayer to run their own
// raw SQL queries.
func (s *SQLStore) DBHandle() *sql.DB {
return s.db
}
// DBType returns the DB driver used for the store.
func (s *SQLStore) DBType() string {
return s.dbType
}
func (s *SQLStore) getQueryBuilder(db sq.BaseRunner) sq.StatementBuilderType {
builder := sq.StatementBuilder
if s.dbType == model.PostgresDBType {
builder = builder.PlaceholderFormat(sq.Dollar)
}
return builder.RunWith(db)
}
func (s *SQLStore) escapeField(fieldName string) string { //nolint:unparam
if s.dbType == model.MysqlDBType {
return "`" + fieldName + "`"
}
if s.dbType == model.PostgresDBType {
return "\"" + fieldName + "\""
}
return fieldName
}
func (s *SQLStore) concatenationSelector(field string, delimiter string) string {
if s.dbType == model.PostgresDBType {
return fmt.Sprintf("string_agg(%s, '%s')", field, delimiter)
}
if s.dbType == model.MysqlDBType {
return fmt.Sprintf("GROUP_CONCAT(%s SEPARATOR '%s')", field, delimiter)
}
return ""
}
func (s *SQLStore) elementInColumn(column string) string {
if s.dbType == model.MysqlDBType {
return fmt.Sprintf("instr(%s, ?) > 0", column)
}
if s.dbType == model.PostgresDBType {
return fmt.Sprintf("position(? in %s) > 0", column)
}
return ""
}
func (s *SQLStore) getLicense(db sq.BaseRunner) *mm_model.License {
return nil
}
func (s *SQLStore) getCloudLimits(db sq.BaseRunner) (*mm_model.ProductLimits, error) {
return nil, nil
}
func (s *SQLStore) searchUserChannels(db sq.BaseRunner, teamID, userID, query string) ([]*mm_model.Channel, error) {
return nil, store.NewNotSupportedError("search user channels not supported on standalone mode")
}
func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mm_model.Channel, error) {
return nil, store.NewNotSupportedError("get channel not supported on standalone mode")
}
func (s *SQLStore) DBVersion() string {
var version string
var row *sql.Row
switch s.dbType {
case model.MysqlDBType:
row = s.db.QueryRow("SELECT VERSION()")
case model.PostgresDBType:
row = s.db.QueryRow("SHOW server_version")
default:
return ""
}
if err := row.Scan(&version); err != nil {
s.logger.Error("error checking database version", mlog.Err(err))
return ""
}
return version
}
func (s *SQLStore) dropAllTables(db sq.BaseRunner) error {
if s.DBType() == model.PostgresDBType {
_, err := db.Exec(`DO
$func$
BEGIN
EXECUTE
(SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE'
FROM pg_class
WHERE relkind = 'r' -- only tables
AND relnamespace = 'public'::regnamespace
AND NOT relname = 'schema_migrations'
);
END
$func$;`)
if err != nil {
return err
}
} else {
rows, err := db.Query(`show tables`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return err
}
if table != s.tablePrefix+"schema_migrations" {
if _, err := db.Exec(`TRUNCATE TABLE ` + table); err != nil {
return err
}
}
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var subscriptionFields = []string{
"block_type",
"block_id",
"subscriber_type",
"subscriber_id",
"notified_at",
"create_at",
"delete_at",
}
func valuesForSubscription(sub *model.Subscription) []interface{} {
return []interface{}{
sub.BlockType,
sub.BlockID,
sub.SubscriberType,
sub.SubscriberID,
sub.NotifiedAt,
sub.CreateAt,
sub.DeleteAt,
}
}
func (s *SQLStore) subscriptionsFromRows(rows *sql.Rows) ([]*model.Subscription, error) {
subscriptions := []*model.Subscription{}
for rows.Next() {
var sub model.Subscription
err := rows.Scan(
&sub.BlockType,
&sub.BlockID,
&sub.SubscriberType,
&sub.SubscriberID,
&sub.NotifiedAt,
&sub.CreateAt,
&sub.DeleteAt,
)
if err != nil {
return nil, err
}
subscriptions = append(subscriptions, &sub)
}
return subscriptions, nil
}
// createSubscription creates a new subscription, or returns an existing subscription
// for the block & subscriber.
func (s *SQLStore) createSubscription(db sq.BaseRunner, sub *model.Subscription) (*model.Subscription, error) {
if err := sub.IsValid(); err != nil {
return nil, err
}
now := model.GetMillis()
subAdd := *sub
subAdd.NotifiedAt = now // notified_at set so first notification doesn't pick up all history
subAdd.CreateAt = now
subAdd.DeleteAt = 0
query := s.getQueryBuilder(db).
Insert(s.tablePrefix + "subscriptions").
Columns(subscriptionFields...).
Values(valuesForSubscription(&subAdd)...)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE delete_at = 0, notified_at = ?", now)
} else {
query = query.Suffix("ON CONFLICT (block_id,subscriber_id) DO UPDATE SET delete_at = 0, notified_at = ?", now)
}
if _, err := query.Exec(); err != nil {
s.logger.Error("Cannot create subscription",
mlog.String("block_id", sub.BlockID),
mlog.String("subscriber_id", sub.SubscriberID),
mlog.Err(err),
)
return nil, err
}
return &subAdd, nil
}
// deleteSubscription soft deletes the subscription for a specific block and subscriber.
func (s *SQLStore) deleteSubscription(db sq.BaseRunner, blockID string, subscriberID string) error {
now := model.GetMillis()
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"subscriptions").
Set("delete_at", now).
Where(sq.Eq{"block_id": blockID}).
Where(sq.Eq{"subscriber_id": subscriberID})
result, err := query.Exec()
if err != nil {
return err
}
count, err := result.RowsAffected()
if err != nil {
return err
}
if count == 0 {
message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID)
return model.NewErrNotFound(message)
}
return nil
}
// getSubscription fetches the subscription for a specific block and subscriber.
func (s *SQLStore) getSubscription(db sq.BaseRunner, blockID string, subscriberID string) (*model.Subscription, error) {
query := s.getQueryBuilder(db).
Select(subscriptionFields...).
From(s.tablePrefix + "subscriptions").
Where(sq.Eq{"block_id": blockID}).
Where(sq.Eq{"subscriber_id": subscriberID}).
Where(sq.Eq{"delete_at": 0})
rows, err := query.Query()
if err != nil {
s.logger.Error("Cannot fetch subscription for block & subscriber",
mlog.String("block_id", blockID),
mlog.String("subscriber_id", subscriberID),
mlog.Err(err),
)
return nil, err
}
defer s.CloseRows(rows)
subscriptions, err := s.subscriptionsFromRows(rows)
if err != nil {
s.logger.Error("Cannot get subscription for block & subscriber",
mlog.String("block_id", blockID),
mlog.String("subscriber_id", subscriberID),
mlog.Err(err),
)
return nil, err
}
if len(subscriptions) == 0 {
message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID)
return nil, model.NewErrNotFound(message)
}
return subscriptions[0], nil
}
// getSubscriptions fetches all subscriptions for a specific subscriber.
func (s *SQLStore) getSubscriptions(db sq.BaseRunner, subscriberID string) ([]*model.Subscription, error) {
query := s.getQueryBuilder(db).
Select(subscriptionFields...).
From(s.tablePrefix + "subscriptions").
Where(sq.Eq{"subscriber_id": subscriberID}).
Where(sq.Eq{"delete_at": 0})
rows, err := query.Query()
if err != nil {
s.logger.Error("Cannot fetch subscriptions for subscriber",
mlog.String("subscriber_id", subscriberID),
mlog.Err(err),
)
return nil, err
}
defer s.CloseRows(rows)
return s.subscriptionsFromRows(rows)
}
// getSubscribersForBlock fetches all subscribers for a block.
func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, blockID string) ([]*model.Subscriber, error) {
query := s.getQueryBuilder(db).
Select(
"subscriber_type",
"subscriber_id",
"notified_at",
).
From(s.tablePrefix + "subscriptions").
Where(sq.Eq{"block_id": blockID}).
Where(sq.Eq{"delete_at": 0}).
OrderBy("notified_at")
rows, err := query.Query()
if err != nil {
s.logger.Error("Cannot fetch subscribers for block",
mlog.String("block_id", blockID),
mlog.Err(err),
)
return nil, err
}
defer s.CloseRows(rows)
subscribers := []*model.Subscriber{}
for rows.Next() {
var sub model.Subscriber
err := rows.Scan(
&sub.SubscriberType,
&sub.SubscriberID,
&sub.NotifiedAt,
)
if err != nil {
return nil, err
}
subscribers = append(subscribers, &sub)
}
return subscribers, nil
}
// getSubscribersCountForBlock returns a count of all subscribers for a block.
func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, blockID string) (int, error) {
query := s.getQueryBuilder(db).
Select("count(subscriber_id)").
From(s.tablePrefix + "subscriptions").
Where(sq.Eq{"block_id": blockID}).
Where(sq.Eq{"delete_at": 0})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
s.logger.Error("Cannot count subscribers for block",
mlog.String("block_id", blockID),
mlog.Err(err),
)
return 0, err
}
return count, nil
}
// updateSubscribersNotifiedAt updates the notified_at field of all subscribers for a block.
func (s *SQLStore) updateSubscribersNotifiedAt(db sq.BaseRunner, blockID string, notifiedAt int64) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"subscriptions").
Set("notified_at", notifiedAt).
Where(sq.Eq{"block_id": blockID}).
Where(sq.Eq{"delete_at": 0})
if _, err := query.Exec(); err != nil {
s.logger.Error("UpdateSubscribersNotifiedAt error occurred while updating subscriber(s)",
mlog.String("blockID", blockID),
mlog.Err(err),
)
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (s *SQLStore) getSystemSetting(db sq.BaseRunner, key string) (string, error) {
scanner := s.getQueryBuilder(db).
Select("value").
From(s.tablePrefix + "system_settings").
Where(sq.Eq{"id": key}).
QueryRow()
var result string
err := scanner.Scan(&result)
if err != nil && !model.IsErrNotFound(err) {
return "", err
}
return result, nil
}
func (s *SQLStore) getSystemSettings(db sq.BaseRunner) (map[string]string, error) {
query := s.getQueryBuilder(db).Select("*").From(s.tablePrefix + "system_settings")
rows, err := query.Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
results := map[string]string{}
for rows.Next() {
var id string
var value string
err := rows.Scan(&id, &value)
if err != nil {
return nil, err
}
results[id] = value
}
return results, nil
}
func (s *SQLStore) setSystemSetting(db sq.BaseRunner, id, value string) error {
query := s.getQueryBuilder(db).Insert(s.tablePrefix+"system_settings").Columns("id", "value").Values(id, value)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value)
} else {
query = query.Suffix("ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value")
}
_, err := query.Exec()
if err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
sq "github.com/Masterminds/squirrel"
)
var (
teamFields = []string{
"id",
"signup_token",
"COALESCE(settings, '{}')",
"modified_by",
"update_at",
}
)
func (s *SQLStore) upsertTeamSignupToken(db sq.BaseRunner, team model.Team) error {
now := utils.GetMillis()
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"teams").
Columns(
"id",
"signup_token",
"modified_by",
"update_at",
).
Values(
team.ID,
team.SignupToken,
team.ModifiedBy,
now,
)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE signup_token = ?, modified_by = ?, update_at = ?",
team.SignupToken, team.ModifiedBy, now)
} else {
query = query.Suffix(
`ON CONFLICT (id)
DO UPDATE SET signup_token = EXCLUDED.signup_token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`,
)
}
_, err := query.Exec()
return err
}
func (s *SQLStore) upsertTeamSettings(db sq.BaseRunner, team model.Team) error {
now := utils.GetMillis()
signupToken := utils.NewID(utils.IDTypeToken)
settingsJSON, err := json.Marshal(team.Settings)
if err != nil {
return err
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"teams").
Columns(
"id",
"signup_token",
"settings",
"modified_by",
"update_at",
).
Values(
team.ID,
signupToken,
settingsJSON,
team.ModifiedBy,
now,
)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE settings = ?, modified_by = ?, update_at = ?", settingsJSON, team.ModifiedBy, now)
} else {
query = query.Suffix(
`ON CONFLICT (id)
DO UPDATE SET settings = EXCLUDED.settings, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`,
)
}
_, err = query.Exec()
return err
}
func (s *SQLStore) getTeam(db sq.BaseRunner, id string) (*model.Team, error) {
var settingsJSON string
query := s.getQueryBuilder(db).
Select(
"id",
"signup_token",
"COALESCE(settings, '{}')",
"modified_by",
"update_at",
).
From(s.tablePrefix + "teams").
Where(sq.Eq{"id": id})
row := query.QueryRow()
team := model.Team{}
err := row.Scan(
&team.ID,
&team.SignupToken,
&settingsJSON,
&team.ModifiedBy,
&team.UpdateAt,
)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(settingsJSON), &team.Settings)
if err != nil {
s.logger.Error(`ERROR GetTeam settings json.Unmarshal`, mlog.Err(err))
return nil, err
}
return &team, nil
}
func (s *SQLStore) getTeamsForUser(db sq.BaseRunner, _ string) ([]*model.Team, error) {
return s.getAllTeams(db)
}
func (s *SQLStore) getTeamCount(db sq.BaseRunner) (int64, error) {
query := s.getQueryBuilder(db).
Select(
"COUNT(*) AS count",
).
From(s.tablePrefix + "teams")
rows, err := query.Query()
if err != nil {
s.logger.Error("ERROR GetTeamCount", mlog.Err(err))
return 0, err
}
defer s.CloseRows(rows)
var count int64
rows.Next()
err = rows.Scan(&count)
if err != nil {
s.logger.Error("Failed to fetch team count", mlog.Err(err))
return 0, err
}
return count, nil
}
func (s *SQLStore) teamsFromRows(rows *sql.Rows) ([]*model.Team, error) {
teams := []*model.Team{}
for rows.Next() {
var team model.Team
var settingsBytes []byte
err := rows.Scan(
&team.ID,
&team.SignupToken,
&settingsBytes,
&team.ModifiedBy,
&team.UpdateAt,
)
if err != nil {
return nil, err
}
err = json.Unmarshal(settingsBytes, &team.Settings)
if err != nil {
return nil, err
}
teams = append(teams, &team)
}
return teams, nil
}
func (s *SQLStore) getAllTeams(db sq.BaseRunner) ([]*model.Team, error) {
query := s.getQueryBuilder(db).
Select(teamFields...).
From(s.tablePrefix + "teams")
rows, err := query.Query()
if err != nil {
s.logger.Error("ERROR GetAllTeams", mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
teams, err := s.teamsFromRows(rows)
if err != nil {
return nil, err
}
return teams, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"errors"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
ErrUnsupportedDatabaseType = errors.New("database type is unsupported")
)
// removeDefaultTemplates deletes all the default templates and their children.
func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, boards []*model.Board) error {
count := 0
for _, board := range boards {
if board.CreatedBy != model.SystemUserID {
continue
}
// default template deletion does not need to go to blocks_history
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "boards").
Where(sq.Eq{"id": board.ID}).
Where(sq.Eq{"is_template": true})
if _, err := deleteQuery.Exec(); err != nil {
return fmt.Errorf("cannot delete default template %s: %w", board.ID, err)
}
deleteQuery = s.getQueryBuilder(db).
Delete(s.tablePrefix + "blocks").
Where(sq.Or{
sq.Eq{"parent_id": board.ID},
sq.Eq{"root_id": board.ID},
sq.Eq{"board_id": board.ID},
})
if _, err := deleteQuery.Exec(); err != nil {
return fmt.Errorf("cannot delete default template %s: %w", board.ID, err)
}
s.logger.Trace("removed default template block",
mlog.String("board_id", board.ID),
)
count++
}
s.logger.Debug("Removed default templates", mlog.Int("count", count))
return nil
}
// getTemplateBoards fetches all template boards .
func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID, userID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("")...).
From(s.tablePrefix+"boards as b").
LeftJoin(s.tablePrefix+"board_members as bm on b.id = bm.board_id and bm.user_id = ?", userID).
Where(sq.Eq{"is_template": true}).
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Or{
// this is to include public templates even if there is not board_member entry
sq.And{
sq.Eq{"bm.board_id": nil},
sq.Eq{"b.type": model.BoardTypeOpen},
},
sq.And{
sq.NotEq{"bm.board_id": nil},
},
})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getTemplateBoards ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
userTemplates, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
return userTemplates, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"os"
"testing"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mgdelacroix/foundation"
"github.com/stretchr/testify/require"
)
type storeType struct {
Name string
ConnString string
Store store.Store
Logger *mlog.Logger
}
var mainStoreTypes []*storeType
func NewStoreType(name string, driver string, skipMigrations bool) *storeType {
settings := storetest.MakeSqlSettings(driver, false)
connectionString := *settings.DataSource
logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)
sqlDB, err := sql.Open(driver, connectionString)
if err != nil {
panic(fmt.Sprintf("cannot open database: %s", err))
}
err = sqlDB.Ping()
if err != nil {
panic(fmt.Sprintf("cannot ping database: %s", err))
}
storeParams := Params{
DBType: driver,
ConnectionString: connectionString,
SkipMigrations: skipMigrations,
TablePrefix: "focalboard_",
Logger: logger,
DB: sqlDB,
IsPlugin: false, // ToDo: to be removed
}
store, err := New(storeParams)
if err != nil {
panic(fmt.Sprintf("cannot create store: %s", err))
}
return &storeType{name, connectionString, store, logger}
}
func initStores(skipMigrations bool) []*storeType {
var storeTypes []*storeType
if os.Getenv("IS_CI") == "true" {
switch os.Getenv("MM_SQLSETTINGS_DRIVERNAME") {
case "mysql":
storeTypes = append(storeTypes, NewStoreType("MySQL", model.MysqlDBType, skipMigrations))
case "postgres":
storeTypes = append(storeTypes, NewStoreType("PostgreSQL", model.PostgresDBType, skipMigrations))
}
} else {
storeTypes = append(storeTypes,
NewStoreType("PostgreSQL", model.PostgresDBType, skipMigrations),
NewStoreType("MySQL", model.MysqlDBType, skipMigrations),
)
}
return storeTypes
}
func RunStoreTests(t *testing.T, f func(*testing.T, store.Store)) {
for _, st := range mainStoreTypes {
st := st
require.NoError(t, st.Store.DropAllTables())
t.Run(st.Name, func(t *testing.T) {
f(t, st.Store)
})
}
}
func RunStoreTestsWithSqlStore(t *testing.T, f func(*testing.T, *SQLStore)) {
for _, st := range mainStoreTypes {
st := st
sqlstore := st.Store.(*SQLStore)
require.NoError(t, sqlstore.DropAllTables())
t.Run(st.Name, func(t *testing.T) {
f(t, sqlstore)
})
}
}
// RunStoreTestsWithFoundation executes a test for all store types. It
// requires a new instance of each store type as migration tests
// cannot reuse old stores with already run migrations
func RunStoreTestsWithFoundation(t *testing.T, f func(*testing.T, *foundation.Foundation)) {
storeTypes := initStores(true)
for _, st := range storeTypes {
st := st
t.Run(st.Name, func(t *testing.T) {
sqlstore := st.Store.(*SQLStore)
f(t, foundation.New(t, NewBoardsMigrator(sqlstore)))
})
require.NoError(t, st.Store.Shutdown())
require.NoError(t, st.Logger.Shutdown())
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"errors"
"fmt"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
errUnsupportedOperation = errors.New("unsupported operation")
)
type UserNotFoundError struct {
id string
}
func (unf UserNotFoundError) Error() string {
return fmt.Sprintf("user not found (%s)", unf.id)
}
func (s *SQLStore) getRegisteredUserCount(db sq.BaseRunner) (int, error) {
query := s.getQueryBuilder(db).
Select("count(*)").
From(s.tablePrefix + "users").
Where(sq.Eq{"delete_at": 0})
row := query.QueryRow()
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *SQLStore) getUserByCondition(db sq.BaseRunner, condition sq.Eq) (*model.User, error) {
users, err := s.getUsersByCondition(db, condition, 0)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, model.NewErrNotFound("user")
}
return users[0], nil
}
func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition interface{}, limit uint64) ([]*model.User, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"username",
"email",
"password",
"mfa_secret",
"auth_service",
"auth_data",
"create_at",
"update_at",
"delete_at",
).
From(s.tablePrefix + "users").
Where(sq.Eq{"delete_at": 0}).
Where(condition)
if limit != 0 {
query = query.Limit(limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getUsersByCondition ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
users, err := s.usersFromRows(rows)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, model.NewErrNotFound("user")
}
return users, nil
}
func (s *SQLStore) getUserByID(db sq.BaseRunner, userID string) (*model.User, error) {
return s.getUserByCondition(db, sq.Eq{"id": userID})
}
func (s *SQLStore) getUsersList(db sq.BaseRunner, userIDs []string, _, _ bool) ([]*model.User, error) {
users, err := s.getUsersByCondition(db, sq.Eq{"id": userIDs}, 0)
if err != nil {
return nil, err
}
if len(users) != len(userIDs) {
return users, model.NewErrNotAllFound("user", userIDs)
}
return users, nil
}
func (s *SQLStore) getUserByEmail(db sq.BaseRunner, email string) (*model.User, error) {
return s.getUserByCondition(db, sq.Eq{"email": email})
}
func (s *SQLStore) getUserByUsername(db sq.BaseRunner, username string) (*model.User, error) {
return s.getUserByCondition(db, sq.Eq{"username": username})
}
func (s *SQLStore) createUser(db sq.BaseRunner, user *model.User) (*model.User, error) {
now := utils.GetMillis()
user.CreateAt = now
user.UpdateAt = now
user.DeleteAt = 0
query := s.getQueryBuilder(db).Insert(s.tablePrefix+"users").
Columns("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "create_at", "update_at", "delete_at").
Values(user.ID, user.Username, user.Email, user.Password, user.MfaSecret, user.AuthService, user.AuthData, user.CreateAt, user.UpdateAt, user.DeleteAt)
_, err := query.Exec()
return user, err
}
func (s *SQLStore) updateUser(db sq.BaseRunner, user *model.User) (*model.User, error) {
now := utils.GetMillis()
user.UpdateAt = now
query := s.getQueryBuilder(db).Update(s.tablePrefix+"users").
Set("username", user.Username).
Set("email", user.Email).
Set("update_at", user.UpdateAt).
Where(sq.Eq{"id": user.ID})
result, err := query.Exec()
if err != nil {
return nil, err
}
rowCount, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowCount < 1 {
return nil, UserNotFoundError{user.ID}
}
return user, nil
}
func (s *SQLStore) updateUserPassword(db sq.BaseRunner, username, password string) error {
now := utils.GetMillis()
query := s.getQueryBuilder(db).Update(s.tablePrefix+"users").
Set("password", password).
Set("update_at", now).
Where(sq.Eq{"username": username})
result, err := query.Exec()
if err != nil {
return err
}
rowCount, err := result.RowsAffected()
if err != nil {
return err
}
if rowCount < 1 {
return UserNotFoundError{username}
}
return nil
}
func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password string) error {
now := utils.GetMillis()
query := s.getQueryBuilder(db).Update(s.tablePrefix+"users").
Set("password", password).
Set("update_at", now).
Where(sq.Eq{"id": userID})
result, err := query.Exec()
if err != nil {
return err
}
rowCount, err := result.RowsAffected()
if err != nil {
return err
}
if rowCount < 1 {
return UserNotFoundError{userID}
}
return nil
}
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string, _, _ bool) ([]*model.User, error) {
users, err := s.getUsersByCondition(db, nil, 0)
if model.IsErrNotFound(err) {
return []*model.User{}, nil
}
return users, err
}
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string, _, _, _ bool) ([]*model.User, error) {
users, err := s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
if model.IsErrNotFound(err) {
return []*model.User{}, nil
}
return users, err
}
func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
users := []*model.User{}
for rows.Next() {
var user model.User
err := rows.Scan(
&user.ID,
&user.Username,
&user.Email,
&user.Password,
&user.MfaSecret,
&user.AuthService,
&user.AuthData,
&user.CreateAt,
&user.UpdateAt,
&user.DeleteAt,
)
if err != nil {
return nil, err
}
users = append(users, &user)
}
return users, nil
}
func (s *SQLStore) patchUserPreferences(db sq.BaseRunner, userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) {
preferences, err := s.getUserPreferences(db, userID)
if err != nil {
return nil, err
}
if len(patch.UpdatedFields) > 0 {
for key, value := range patch.UpdatedFields {
preference := mm_model.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
Value: value,
}
if err := s.updateUserPreference(db, preference); err != nil {
return nil, err
}
newPreferences := mm_model.Preferences{}
for _, existingPreference := range preferences {
if preference.Name != existingPreference.Name {
newPreferences = append(newPreferences, existingPreference)
}
}
newPreferences = append(newPreferences, preference)
preferences = newPreferences
}
}
if len(patch.DeletedFields) > 0 {
for _, key := range patch.DeletedFields {
preference := mm_model.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
}
if err := s.deleteUserPreference(db, preference); err != nil {
return nil, err
}
newPreferences := mm_model.Preferences{}
for _, existingPreference := range preferences {
if preference.Name != existingPreference.Name {
newPreferences = append(newPreferences, existingPreference)
}
}
preferences = newPreferences
}
}
return preferences, nil
}
func (s *SQLStore) updateUserPreference(db sq.BaseRunner, preference mm_model.Preference) error {
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"preferences").
Columns("UserId", "Category", "Name", "Value").
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
switch s.dbType {
case model.MysqlDBType:
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
case model.PostgresDBType:
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
default:
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
if _, err := query.Exec(); err != nil {
return fmt.Errorf("failed to upsert user preference in database: userID: %s name: %s value: %s error: %w", preference.UserId, preference.Name, preference.Value, err)
}
return nil
}
func (s *SQLStore) deleteUserPreference(db sq.BaseRunner, preference mm_model.Preference) error {
query := s.getQueryBuilder(db).
Delete(s.tablePrefix + "preferences").
Where(sq.Eq{"UserId": preference.UserId}).
Where(sq.Eq{"Category": preference.Category}).
Where(sq.Eq{"Name": preference.Name})
if _, err := query.Exec(); err != nil {
return fmt.Errorf("failed to delete user preference from database: %w", err)
}
return nil
}
func (s *SQLStore) canSeeUser(db sq.BaseRunner, seerID string, seenID string) (bool, error) {
return true, nil
}
func (s *SQLStore) sendMessage(db sq.BaseRunner, message, postType string, receipts []string) error {
return errUnsupportedOperation
}
func (s *SQLStore) postMessage(db sq.BaseRunner, message, postType string, channel string) error {
return errUnsupportedOperation
}
func (s *SQLStore) getUserTimezone(_ sq.BaseRunner, _ string) (string, error) {
return "", errUnsupportedOperation
}
func (s *SQLStore) getUserPreferences(db sq.BaseRunner, userID string) (mm_model.Preferences, error) {
query := s.getQueryBuilder(db).
Select("userid", "category", "name", "value").
From(s.tablePrefix + "preferences").
Where(sq.Eq{
"userid": userID,
"category": model.PreferencesCategoryFocalboard,
})
rows, err := query.Query()
if err != nil {
s.logger.Error("failed to fetch user preferences", mlog.String("user_id", userID), mlog.Err(err))
return nil, err
}
defer rows.Close()
preferences, err := s.preferencesFromRows(rows)
if err != nil {
return nil, err
}
return preferences, nil
}
func (s *SQLStore) preferencesFromRows(rows *sql.Rows) ([]mm_model.Preference, error) {
preferences := []mm_model.Preference{}
for rows.Next() {
var preference mm_model.Preference
err := rows.Scan(
&preference.UserId,
&preference.Category,
&preference.Name,
&preference.Value,
)
if err != nil {
s.logger.Error("failed to scan row for user preference", mlog.Err(err))
return nil, err
}
preferences = append(preferences, preference)
}
return preferences, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) CloseRows(rows *sql.Rows) {
if err := rows.Close(); err != nil {
s.logger.Error("error closing MattermostAuthLayer row set", mlog.Err(err))
}
}
func (s *SQLStore) IsErrNotFound(err error) bool {
return model.IsErrNotFound(err)
}
func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
if s.isBinaryParam {
b = append([]byte{0x01}, b...)
}
return b, nil
}
func PrepareNewTestDatabase(dbType string) (connectionString string, err error) {
if dbType == "" {
dbType = model.PostgresDBType
}
var dbName string
var rootUser string
// docker unit tests take priority over any DSN env vars
var template string
switch dbType {
case model.MysqlDBType:
template = "%s:mostest@tcp(localhost:3306)/%s?charset=utf8mb4,utf8&writeTimeout=30s"
rootUser = "root"
case model.PostgresDBType:
template = "postgres://%s:mostest@localhost:5432/%s?sslmode=disable\u0026connect_timeout=10"
rootUser = "mmuser"
default:
return "", newErrInvalidDBType(dbType)
}
connectionString = fmt.Sprintf(template, rootUser, "mattermost_test")
// create a new database each run
sqlDB, err := sql.Open(dbType, connectionString)
if err != nil {
return "", fmt.Errorf("cannot connect to %s database: %w", dbType, err)
}
defer sqlDB.Close()
err = sqlDB.Ping()
if err != nil {
return "", fmt.Errorf("cannot ping %s database: %w", dbType, err)
}
dbName = "testdb_" + utils.NewID(utils.IDTypeNone)[:8]
_, err = sqlDB.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName))
if err != nil {
return "", fmt.Errorf("cannot create %s database %s: %w", dbType, dbName, err)
}
if dbType != model.PostgresDBType {
_, err = sqlDB.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO mmuser;", dbName))
if err != nil {
return "", fmt.Errorf("cannot grant permissions on %s database %s: %w", dbType, dbName, err)
}
}
connectionString = fmt.Sprintf(template, "mmuser", dbName)
return connectionString, nil
}
type ErrInvalidDBType struct {
dbType string
}
func newErrInvalidDBType(dbType string) error {
return ErrInvalidDBType{
dbType: dbType,
}
}
func (e ErrInvalidDBType) Error() string {
return "unsupported database type: " + e.dbType
}
// deleteBoardRecord deletes a boards record without deleting any child records in the blocks table.
// FOR UNIT TESTING ONLY.
func (s *SQLStore) deleteBoardRecord(db sq.BaseRunner, boardID string, modifiedBy string) error {
return s.deleteBoardAndChildren(db, boardID, modifiedBy, true)
}
// deleteBlockRecord deletes a blocks record without deleting any child records in the blocks table.
// FOR UNIT TESTING ONLY.
func (s *SQLStore) deleteBlockRecord(db sq.BaseRunner, blockID, modifiedBy string) error {
return s.deleteBlockAndChildren(db, blockID, modifiedBy, true)
}
func (s *SQLStore) castInt(val int64, as string) string {
if s.dbType == model.MysqlDBType {
return fmt.Sprintf("cast(%d as unsigned) AS %s", val, as)
}
return fmt.Sprintf("cast(%d as bigint) AS %s", val, as)
}
func (s *SQLStore) GetSchemaName() (string, error) {
var query sq.SelectBuilder
switch s.dbType {
case model.MysqlDBType:
query = s.getQueryBuilder(s.db).Select("DATABASE()")
case model.PostgresDBType:
query = s.getQueryBuilder(s.db).Select("current_schema()")
default:
return "", ErrUnsupportedDatabaseType
}
scanner := query.QueryRow()
var result string
err := scanner.Scan(&result)
if err != nil && !model.IsErrNotFound(err) {
return "", err
}
return result, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -copyright_file=../../../copyright.txt -destination=mockstore/mockstore.go -package mockstore . Store
//go:generate go run ./generators/main.go
package store
import (
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
const CardLimitTimestampSystemKey = "card_limit_timestamp"
// Store represents the abstraction of the data storage.
type Store interface {
GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error)
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]*model.Block, error)
GetBlocksWithParent(boardID, parentID string) ([]*model.Block, error)
GetBlocksByIDs(ids []string) ([]*model.Block, error)
GetBlocksWithType(boardID, blockType string) ([]*model.Block, error)
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error)
GetBlocksForBoard(boardID string) ([]*model.Block, error)
// @withTransaction
InsertBlock(block *model.Block, userID string) error
// @withTransaction
DeleteBlock(blockID string, modifiedBy string) error
// @withTransaction
InsertBlocks(blocks []*model.Block, userID string) error
// @withTransaction
UndeleteBlock(blockID string, modifiedBy string) error
// @withTransaction
UndeleteBoard(boardID string, modifiedBy string) error
GetBlockCountsByType() (map[string]int64, error)
GetBoardCount() (int64, error)
GetBlock(blockID string) (*model.Block, error)
// @withTransaction
PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error)
GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error)
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
GetBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error)
// @withTransaction
DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error)
// @withTransaction
DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error)
// @withTransaction
PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error
Shutdown() error
GetSystemSetting(key string) (string, error)
GetSystemSettings() (map[string]string, error)
SetSystemSetting(key, value string) error
GetRegisteredUserCount() (int, error)
GetUserByID(userID string) (*model.User, error)
GetUsersList(userIDs []string, showEmail, showName bool) ([]*model.User, error)
GetUserByEmail(email string) (*model.User, error)
GetUserByUsername(username string) (*model.User, error)
CreateUser(user *model.User) (*model.User, error)
UpdateUser(user *model.User) (*model.User, error)
UpdateUserPassword(username, password string) error
UpdateUserPasswordByID(userID, password string) error
GetUsersByTeam(teamID string, asGuestID string, showEmail, showName bool) ([]*model.User, error)
SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots bool, showEmail, showName bool) ([]*model.User, error)
PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error)
GetUserPreferences(userID string) (mm_model.Preferences, error)
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
GetSession(token string, expireTime int64) (*model.Session, error)
CreateSession(session *model.Session) error
RefreshSession(session *model.Session) error
UpdateSession(session *model.Session) error
DeleteSession(sessionID string) error
CleanUpSessions(expireTime int64) error
UpsertSharing(sharing model.Sharing) error
GetSharing(rootID string) (*model.Sharing, error)
UpsertTeamSignupToken(team model.Team) error
UpsertTeamSettings(team model.Team) error
GetTeam(ID string) (*model.Team, error)
GetTeamsForUser(userID string) ([]*model.Team, error)
GetAllTeams() ([]*model.Team, error)
GetTeamCount() (int64, error)
InsertBoard(board *model.Board, userID string) (*model.Board, error)
// @withTransaction
InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error)
// @withTransaction
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
GetBoard(id string) (*model.Board, error)
GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error)
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
// @withTransaction
DeleteBoard(boardID, userID string) error
SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
DeleteMember(boardID, userID string) error
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
GetMembersForUser(userID string) ([]*model.BoardMember, error)
CanSeeUser(seerID string, seenID string) (bool, error)
SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error)
SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error)
// @withTransaction
CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error)
// @withTransaction
CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error)
// @withTransaction
PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error)
// @withTransaction
DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error
GetCategory(id string) (*model.Category, error)
GetUserCategories(userID, teamID string) ([]model.Category, error)
// @withTransaction
CreateCategory(category model.Category) error
UpdateCategory(category model.Category) error
DeleteCategory(categoryID, userID, teamID string) error
ReorderCategories(userID, teamID string, newCategoryOrder []string) ([]string, error)
GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error)
GetFileInfo(id string) (*mm_model.FileInfo, error)
SaveFileInfo(fileInfo *mm_model.FileInfo) error
// @withTransaction
AddUpdateCategoryBoard(userID, categoryID string, boardIDs []string) error
ReorderCategoryBoards(categoryID string, newBoardsOrder []string) ([]string, error)
SetBoardVisibility(userID, categoryID, boardID string, visible bool) error
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
DeleteSubscription(blockID string, subscriberID string) error
GetSubscription(blockID string, subscriberID string) (*model.Subscription, error)
GetSubscriptions(subscriberID string) ([]*model.Subscription, error)
GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error)
GetSubscribersCountForBlock(blockID string) (int, error)
UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error
UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error)
DeleteNotificationHint(blockID string) error
GetNotificationHint(blockID string) (*model.NotificationHint, error)
GetNextNotificationHint(remove bool) (*model.NotificationHint, error)
RemoveDefaultTemplates(boards []*model.Board) error
GetTemplateBoards(teamID, userID string) ([]*model.Board, error)
// @withTransaction
RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error)
GetUsedCardsCount() (int, error)
GetCardLimitTimestamp() (int64, error)
UpdateCardLimitTimestamp(cardLimit int) (int64, error)
DBType() string
DBVersion() string
GetLicense() *mm_model.License
GetCloudLimits() (*mm_model.ProductLimits, error)
SearchUserChannels(teamID, userID, query string) ([]*mm_model.Channel, error)
GetChannel(teamID, channelID string) (*mm_model.Channel, error)
PostMessage(message, postType, channelID string) error
SendMessage(message, postType string, receipts []string) error
// Insights
GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetUserTimezone(userID string) (string, error)
// Compliance
GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error)
GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error)
GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error)
// For unit testing only
DeleteBoardRecord(boardID, modifiedBy string) error
DeleteBlockRecord(blockID, modifiedBy string) error
DropAllTables() error
}
type NotSupportedError struct {
msg string
}
func NewNotSupportedError(msg string) NotSupportedError {
return NotSupportedError{msg: msg}
}
func (pe NotSupportedError) Error() string {
return pe.msg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"os"
"strings"
"time"
rudder "github.com/rudderlabs/analytics-go"
"github.com/mattermost/mattermost-server/v6/server/boards/services/scheduler"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
rudderKey = "placeholder_boards_rudder_key"
rudderDataplaneURL = "placeholder_rudder_dataplane_url"
timeBetweenTelemetryChecks = 10 * time.Minute
)
type TrackerFunc func() (Tracker, error)
type Tracker map[string]interface{}
type Service struct {
trackers map[string]TrackerFunc
logger mlog.LoggerIFace
rudderClient rudder.Client
telemetryID string
timestampLastTelemetrySent time.Time
}
type RudderConfig struct {
RudderKey string
DataplaneURL string
}
func New(telemetryID string, logger mlog.LoggerIFace) *Service {
service := &Service{
logger: logger,
telemetryID: telemetryID,
trackers: map[string]TrackerFunc{},
}
return service
}
func (ts *Service) RegisterTracker(name string, f TrackerFunc) {
ts.trackers[name] = f
}
func (ts *Service) getRudderConfig() RudderConfig {
if !strings.Contains(rudderKey, "placeholder") && !strings.Contains(rudderDataplaneURL, "placeholder") {
return RudderConfig{rudderKey, rudderDataplaneURL}
}
if os.Getenv("RUDDER_KEY") != "" && os.Getenv("RUDDER_DATAPLANE_URL") != "" {
return RudderConfig{os.Getenv("RUDDER_KEY"), os.Getenv("RUDDER_DATAPLANE_URL")}
}
return RudderConfig{}
}
func (ts *Service) sendDailyTelemetry(override bool) {
config := ts.getRudderConfig()
if (config.DataplaneURL != "" && config.RudderKey != "") || override {
ts.initRudder(config.DataplaneURL, config.RudderKey)
for name, tracker := range ts.trackers {
m, err := tracker()
if err != nil {
ts.logger.Error("Error fetching telemetry data", mlog.String("name", name), mlog.Err(err))
continue
}
ts.sendTelemetry(name, m)
}
}
}
func (ts *Service) sendTelemetry(event string, properties map[string]interface{}) {
if ts.rudderClient != nil {
var context *rudder.Context
_ = ts.rudderClient.Enqueue(rudder.Track{
Event: event,
UserId: ts.telemetryID,
Properties: properties,
Context: context,
})
}
}
func (ts *Service) initRudder(endpoint, rudderKey string) {
if ts.rudderClient == nil {
config := rudder.Config{}
config.Logger = rudder.StdLogger(ts.logger.StdLogger(mlog.LvlFBTelemetry))
config.Endpoint = endpoint
// For testing
if endpoint != rudderDataplaneURL {
config.Verbose = true
config.BatchSize = 1
}
client, err := rudder.NewWithConfig(rudderKey, endpoint, config)
if err != nil {
ts.logger.Fatal("Failed to create Rudder instance")
return
}
_ = client.Enqueue(rudder.Identify{
UserId: ts.telemetryID,
})
ts.rudderClient = client
}
}
func (ts *Service) doTelemetryIfNeeded(firstRun time.Time) {
hoursSinceFirstServerRun := time.Since(firstRun).Hours()
// Send once every 10 minutes for the first hour
if hoursSinceFirstServerRun < 1 {
ts.doTelemetry()
return
}
// Send once every hour thereafter for the first 12 hours
if hoursSinceFirstServerRun <= 12 && time.Since(ts.timestampLastTelemetrySent) >= time.Hour {
ts.doTelemetry()
return
}
// Send at the 24 hour mark and every 24 hours after
if hoursSinceFirstServerRun > 12 && time.Since(ts.timestampLastTelemetrySent) >= 24*time.Hour {
ts.doTelemetry()
return
}
}
func (ts *Service) RunTelemetryJob(firstRunMillis int64) {
// Send on boot
ts.doTelemetry()
scheduler.CreateRecurringTask("Telemetry", func() {
ts.doTelemetryIfNeeded(utils.TimeFromMillis(firstRunMillis))
}, timeBetweenTelemetryChecks)
}
func (ts *Service) doTelemetry() {
ts.timestampLastTelemetrySent = time.Now()
ts.sendDailyTelemetry(false)
}
// Shutdown closes the telemetry client.
func (ts *Service) Shutdown() error {
if ts.rudderClient != nil {
return ts.rudderClient.Close()
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package webhook
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// NotifyUpdate calls webhooks.
func (wh *Client) NotifyUpdate(block *model.Block) {
if len(wh.config.WebhookUpdate) < 1 {
return
}
json, err := json.Marshal(block)
if err != nil {
wh.logger.Fatal("NotifyUpdate: json.Marshal", mlog.Err(err))
}
for _, url := range wh.config.WebhookUpdate {
resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
_, _ = io.ReadAll(resp.Body)
resp.Body.Close()
wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url))
}
}
// Client is a webhook client.
type Client struct {
config *config.Configuration
logger mlog.LoggerIFace
}
// NewClient creates a new Client.
func NewClient(config *config.Configuration, logger mlog.LoggerIFace) *Client {
return &Client{
config: config,
logger: logger,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"context"
"runtime/debug"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// CallbackFunc is a func that can enqueued in the callback queue and will be
// called when dequeued.
type CallbackFunc func() error
// CallbackQueue provides a simple thread pool for processing callbacks. Callbacks will
// be executed in the order in which they are enqueued, but no guarantees are provided
// regarding the order in which they finish (unless poolSize == 1).
type CallbackQueue struct {
name string
poolSize int
queue chan CallbackFunc
done chan struct{}
alive chan int
idone uint32
logger mlog.LoggerIFace
}
// NewCallbackQueue creates a new CallbackQueue and starts a thread pool to service it.
func NewCallbackQueue(name string, queueSize int, poolSize int, logger mlog.LoggerIFace) *CallbackQueue {
cn := &CallbackQueue{
name: name,
poolSize: poolSize,
queue: make(chan CallbackFunc, queueSize),
done: make(chan struct{}),
alive: make(chan int, poolSize),
logger: logger,
}
for i := 0; i < poolSize; i++ {
go cn.loop(i)
}
return cn
}
// Shutdown stops accepting enqueues and exits all pool threads. This method waits
// as long as the context allows for the threads to exit.
// Returns true if the pool exited, false on timeout.
func (cn *CallbackQueue) Shutdown(context context.Context) bool {
if !atomic.CompareAndSwapUint32(&cn.idone, 0, 1) {
// already shutdown
return true
}
// signal threads to exit
close(cn.done)
// wait for the threads to exit or timeout
count := 0
for count < cn.poolSize {
select {
case <-cn.alive:
count++
case <-context.Done():
return false
}
}
// try to drain any remaining callbacks
for {
select {
case f := <-cn.queue:
cn.exec(f)
case <-context.Done():
return false
default:
return true
}
}
}
// Enqueue adds a callback to the queue.
func (cn *CallbackQueue) Enqueue(f CallbackFunc) {
if atomic.LoadUint32(&cn.idone) != 0 {
cn.logger.Debug("CallbackQueue skipping enqueue, notifier is shutdown", mlog.String("name", cn.name))
return
}
select {
case cn.queue <- f:
default:
start := time.Now()
cn.queue <- f
dur := time.Since(start)
cn.logger.Warn("CallbackQueue queue backlog", mlog.String("name", cn.name), mlog.Duration("wait_time", dur))
}
}
func (cn *CallbackQueue) loop(id int) {
defer func() {
cn.logger.Trace("CallbackQueue thread exited", mlog.String("name", cn.name), mlog.Int("id", id))
cn.alive <- id
}()
for {
select {
case f := <-cn.queue:
cn.exec(f)
case <-cn.done:
return
}
}
}
func (cn *CallbackQueue) exec(f CallbackFunc) {
// don't let a panic in the callback exit the thread.
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
cn.logger.Error("CallbackQueue callback panic",
mlog.String("name", cn.name),
mlog.Any("panic", r),
mlog.String("stack", string(stack)),
)
}
}()
if err := f(); err != nil {
cn.logger.Error("CallbackQueue callback error", mlog.String("name", cn.name), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"os"
"strings"
)
// IsRunningUnitTests returns true if this instance of FocalBoard is running unit or integration tests.
func IsRunningUnitTests() bool {
testing := os.Getenv("FOCALBOARD_UNIT_TESTING")
if testing == "" {
return false
}
switch strings.ToLower(testing) {
case "1", "t", "y", "true", "yes":
return true
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import "fmt"
// MakeCardLink creates fully qualified card links based on card id and parents.
func MakeCardLink(serverRoot string, teamID string, boardID string, cardID string) string {
return fmt.Sprintf("%s/team/%s/%s/0/%s", serverRoot, teamID, boardID, cardID)
}
func MakeBoardLink(serverRoot string, teamID string, board string) string {
return fmt.Sprintf("%s/team/%s/%s", serverRoot, teamID, board)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import "github.com/stretchr/testify/mock"
var Anything = mock.MatchedBy(func(interface{}) bool { return true })
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"encoding/json"
"path"
"reflect"
"time"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
type IDType byte
const (
IDTypeNone IDType = '7'
IDTypeTeam IDType = 't'
IDTypeBoard IDType = 'b'
IDTypeCard IDType = 'c'
IDTypeView IDType = 'v'
IDTypeSession IDType = 's'
IDTypeUser IDType = 'u'
IDTypeToken IDType = 'k'
IDTypeBlock IDType = 'a'
IDTypeAttachment IDType = 'i'
)
// NewId is a globally unique identifier. It is a [A-Z0-9] string 27
// characters long. It is a UUID version 4 Guid that is zbased32 encoded
// with the padding stripped off, and a one character alpha prefix indicating the
// type of entity or a `7` if unknown type.
func NewID(idType IDType) string {
return string(idType) + mm_model.NewId()
}
// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
return mm_model.GetMillis()
}
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
return mm_model.GetMillisForTime(thisTime)
}
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
func GetTimeForMillis(millis int64) time.Time {
return mm_model.GetTimeForMillis(millis)
}
// SecondsToMillis is a convenience method to convert seconds to milliseconds.
func SecondsToMillis(seconds int64) int64 {
return seconds * 1000
}
func StructToMap(v interface{}) (m map[string]interface{}) {
b, _ := json.Marshal(v)
_ = json.Unmarshal(b, &m)
return
}
func intersection(a []interface{}, b []interface{}) []interface{} {
set := make([]interface{}, 0)
hash := make(map[interface{}]bool)
av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
for i := 0; i < av.Len(); i++ {
el := av.Index(i).Interface()
hash[el] = true
}
for i := 0; i < bv.Len(); i++ {
el := bv.Index(i).Interface()
if _, found := hash[el]; found {
set = append(set, el)
}
}
return set
}
func Intersection(x ...[]interface{}) []interface{} {
if len(x) == 0 {
return nil
}
if len(x) == 1 {
return x[0]
}
result := x[0]
i := 1
for i < len(x) {
result = intersection(result, x[i])
i++
}
return result
}
func IsCloudLicense(license *mm_model.License) bool {
return license != nil &&
license.Features != nil &&
license.Features.Cloud != nil &&
*license.Features.Cloud
}
func DedupeStringArr(arr []string) []string {
hashMap := map[string]bool{}
for _, item := range arr {
hashMap[item] = true
}
dedupedArr := make([]string, len(hashMap))
i := 0
for key := range hashMap {
dedupedArr[i] = key
i++
}
return dedupedArr
}
func GetBaseFilePath() string {
return path.Join("boards", time.Now().Format("20060102"))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// RoutedService defines the interface that is needed for any service to
// register themself in the web server to provide new endpoints. (see
// AddRoutes).
type RoutedService interface {
RegisterRoutes(*mux.Router)
}
// Server is the structure responsible for managing our http web server.
type Server struct {
http.Server
baseURL string
rootPath string
basePrefix string
port int
ssl bool
logger mlog.LoggerIFace
}
// NewServer creates a new instance of the webserver.
func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool, logger mlog.LoggerIFace) *Server {
r := mux.NewRouter()
basePrefix := os.Getenv("FOCALBOARD_HTTP_SERVER_BASEPATH")
if basePrefix != "" {
r = r.PathPrefix(basePrefix).Subrouter()
}
var addr string
if localOnly {
addr = fmt.Sprintf(`localhost:%d`, port)
} else {
addr = fmt.Sprintf(`:%d`, port)
}
baseURL := ""
url, err := url.Parse(serverRoot)
if err != nil {
logger.Error("Invalid ServerRoot setting", mlog.Err(err))
}
baseURL = url.Path
ws := &Server{
// (TODO: Add ReadHeaderTimeout)
Server: http.Server{ //nolint:gosec
Addr: addr,
Handler: r,
},
baseURL: baseURL,
rootPath: rootPath,
port: port,
ssl: ssl,
logger: logger,
basePrefix: basePrefix,
}
return ws
}
func (ws *Server) Router() *mux.Router {
return ws.Server.Handler.(*mux.Router)
}
// AddRoutes allows services to register themself in the webserver router and provide new endpoints.
func (ws *Server) AddRoutes(rs RoutedService) {
rs.RegisterRoutes(ws.Router())
}
func (ws *Server) registerRoutes() {
ws.Router().PathPrefix("/static").Handler(http.StripPrefix(ws.basePrefix+"/static/", http.FileServer(http.Dir(filepath.Join(ws.rootPath, "static")))))
ws.Router().PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
indexTemplate, err := template.New("index").ParseFiles(path.Join(ws.rootPath, "index.html"))
if err != nil {
ws.logger.Log(errorOrWarn(), "Unable to serve the index.html file", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
err = indexTemplate.ExecuteTemplate(w, "index.html", map[string]string{"BaseURL": ws.baseURL})
if err != nil {
ws.logger.Log(errorOrWarn(), "Unable to serve the index.html file", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
})
}
// Start runs the web server and start listening for connections.
func (ws *Server) Start() {
ws.registerRoutes()
if ws.port == -1 {
ws.logger.Debug("server not bind to any port")
return
}
isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem")
if isSSL {
ws.logger.Info("https server started", mlog.Int("port", ws.port))
go func() {
if err := ws.ListenAndServeTLS("./cert/cert.pem", "./cert/key.pem"); err != nil {
ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err))
}
}()
return
}
ws.logger.Info("http server started", mlog.Int("port", ws.port))
go func() {
if err := ws.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err))
}
ws.logger.Info("http server stopped")
}()
}
func (ws *Server) Shutdown() error {
return ws.Close()
}
// fileExists returns true if a file exists at the path.
func fileExists(path string) bool {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return false
}
return err == nil
}
// errorOrWarn returns a `warn` level if this server instance is running unit tests, otherwise `error`.
func errorOrWarn() mlog.Level {
unitTesting := strings.ToLower(strings.TrimSpace(os.Getenv("FOCALBOARD_UNIT_TESTING")))
if unitTesting == "1" || unitTesting == "y" || unitTesting == "t" {
return mlog.LvlWarn
}
return mlog.LvlError
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
package ws
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const websocketMessagePrefix = "custom_boards_"
var errMissingTeamInCommand = fmt.Errorf("command doesn't contain teamId")
type PluginAdapterInterface interface {
Adapter
OnWebSocketConnect(webConnID, userID string)
OnWebSocketDisconnect(webConnID, userID string)
WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest)
BroadcastConfigChange(clientConfig model.ClientConfig)
BroadcastBlockChange(teamID string, block *model.Block)
BroadcastBlockDelete(teamID, blockID, parentID string)
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
HandleClusterEvent(ev mm_model.PluginClusterEvent)
}
type PluginAdapter struct {
api servicesAPI
auth auth.AuthInterface
staleThreshold time.Duration
store Store
logger mlog.LoggerIFace
listenersMU sync.RWMutex
listeners map[string]*PluginAdapterClient
listenersByUserID map[string][]*PluginAdapterClient
subscriptionsMU sync.RWMutex
listenersByTeam map[string][]*PluginAdapterClient
listenersByBlock map[string][]*PluginAdapterClient
}
// servicesAPI is the interface required by the PluginAdapter to interact with
// the mattermost-server.
type servicesAPI interface {
PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast)
PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error
}
func NewPluginAdapter(api servicesAPI, auth auth.AuthInterface, store Store, logger mlog.LoggerIFace) *PluginAdapter {
return &PluginAdapter{
api: api,
auth: auth,
store: store,
staleThreshold: 5 * time.Minute,
logger: logger,
listeners: make(map[string]*PluginAdapterClient),
listenersByUserID: make(map[string][]*PluginAdapterClient),
listenersByTeam: make(map[string][]*PluginAdapterClient),
listenersByBlock: make(map[string][]*PluginAdapterClient),
listenersMU: sync.RWMutex{},
subscriptionsMU: sync.RWMutex{},
}
}
func (pa *PluginAdapter) GetListenerByWebConnID(webConnID string) (pac *PluginAdapterClient, ok bool) {
pa.listenersMU.RLock()
defer pa.listenersMU.RUnlock()
pac, ok = pa.listeners[webConnID]
return
}
func (pa *PluginAdapter) GetListenersByUserID(userID string) []*PluginAdapterClient {
pa.listenersMU.RLock()
defer pa.listenersMU.RUnlock()
return pa.listenersByUserID[userID]
}
func (pa *PluginAdapter) GetListenersByTeam(teamID string) []*PluginAdapterClient {
pa.subscriptionsMU.RLock()
defer pa.subscriptionsMU.RUnlock()
return pa.listenersByTeam[teamID]
}
func (pa *PluginAdapter) GetListenersByBlock(blockID string) []*PluginAdapterClient {
pa.subscriptionsMU.RLock()
defer pa.subscriptionsMU.RUnlock()
return pa.listenersByBlock[blockID]
}
func (pa *PluginAdapter) addListener(pac *PluginAdapterClient) {
pa.listenersMU.Lock()
defer pa.listenersMU.Unlock()
pa.listeners[pac.webConnID] = pac
pa.listenersByUserID[pac.userID] = append(pa.listenersByUserID[pac.userID], pac)
}
func (pa *PluginAdapter) removeListener(pac *PluginAdapterClient) {
pa.listenersMU.Lock()
defer pa.listenersMU.Unlock()
// team subscriptions
for _, team := range pac.teams {
pa.removeListenerFromTeam(pac, team)
}
// block subscriptions
for _, block := range pac.blocks {
pa.removeListenerFromBlock(pac, block)
}
// user ID list
newUserListeners := []*PluginAdapterClient{}
for _, listener := range pa.listenersByUserID[pac.userID] {
if listener.webConnID != pac.webConnID {
newUserListeners = append(newUserListeners, listener)
}
}
pa.listenersByUserID[pac.userID] = newUserListeners
delete(pa.listeners, pac.webConnID)
}
func (pa *PluginAdapter) removeExpiredForUserID(userID string) {
for _, pac := range pa.GetListenersByUserID(userID) {
if !pac.isActive() && pac.hasExpired(pa.staleThreshold) {
pa.removeListener(pac)
}
}
}
func (pa *PluginAdapter) removeListenerFromTeam(pac *PluginAdapterClient, teamID string) {
newTeamListeners := []*PluginAdapterClient{}
for _, listener := range pa.GetListenersByTeam(teamID) {
if listener.webConnID != pac.webConnID {
newTeamListeners = append(newTeamListeners, listener)
}
}
pa.subscriptionsMU.Lock()
pa.listenersByTeam[teamID] = newTeamListeners
pa.subscriptionsMU.Unlock()
pac.unsubscribeFromTeam(teamID)
}
func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, blockID string) {
newBlockListeners := []*PluginAdapterClient{}
for _, listener := range pa.GetListenersByBlock(blockID) {
if listener.webConnID != pac.webConnID {
newBlockListeners = append(newBlockListeners, listener)
}
}
pa.subscriptionsMU.Lock()
pa.listenersByBlock[blockID] = newBlockListeners
pa.subscriptionsMU.Unlock()
pac.unsubscribeFromBlock(blockID)
}
func (pa *PluginAdapter) subscribeListenerToTeam(pac *PluginAdapterClient, teamID string) {
if pac.isSubscribedToTeam(teamID) {
return
}
pa.subscriptionsMU.Lock()
pa.listenersByTeam[teamID] = append(pa.listenersByTeam[teamID], pac)
pa.subscriptionsMU.Unlock()
pac.subscribeToTeam(teamID)
}
func (pa *PluginAdapter) unsubscribeListenerFromTeam(pac *PluginAdapterClient, teamID string) {
if !pac.isSubscribedToTeam(teamID) {
return
}
pa.removeListenerFromTeam(pac, teamID)
}
func (pa *PluginAdapter) getUserIDsForTeam(teamID string) []string {
userMap := map[string]bool{}
for _, pac := range pa.GetListenersByTeam(teamID) {
if pac.isActive() {
userMap[pac.userID] = true
}
}
userIDs := []string{}
for userID := range userMap {
if pa.auth.DoesUserHaveTeamAccess(userID, teamID) {
userIDs = append(userIDs, userID)
}
}
return userIDs
}
func (pa *PluginAdapter) getUserIDsForTeamAndBoard(teamID, boardID string, ensureUserIDs ...string) []string {
userMap := map[string]bool{}
for _, pac := range pa.GetListenersByTeam(teamID) {
if pac.isActive() {
userMap[pac.userID] = true
}
}
members, err := pa.store.GetMembersForBoard(boardID)
if err != nil {
pa.logger.Error("error getting members for board",
mlog.String("method", "getUserIDsForTeamAndBoard"),
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
)
return nil
}
// the list of users would be the intersection between the ones
// that are connected to the team and the board members that need
// to see the updates
userIDs := []string{}
for _, member := range members {
for userID := range userMap {
if userID == member.UserID && pa.auth.DoesUserHaveTeamAccess(userID, teamID) {
userIDs = append(userIDs, userID)
}
}
}
// if we don't have to make sure that some IDs are included, we
// can return at this point
if len(ensureUserIDs) == 0 {
return userIDs
}
completeUserMap := map[string]bool{}
for _, id := range userIDs {
completeUserMap[id] = true
}
for _, id := range ensureUserIDs {
completeUserMap[id] = true
}
completeUserIDs := []string{}
for id := range completeUserMap {
completeUserIDs = append(completeUserIDs, id)
}
return completeUserIDs
}
//nolint:unused
func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) {
for _, blockID := range blockIDs {
if pac.isSubscribedToBlock(blockID) {
pa.removeListenerFromBlock(pac, blockID)
}
}
}
func (pa *PluginAdapter) OnWebSocketConnect(webConnID, userID string) {
if existingPAC, ok := pa.GetListenerByWebConnID(webConnID); ok {
pa.logger.Debug("inactive connection found for webconn, reusing",
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
)
atomic.StoreInt64(&existingPAC.inactiveAt, 0)
return
}
newPAC := &PluginAdapterClient{
inactiveAt: 0,
webConnID: webConnID,
userID: userID,
teams: []string{},
blocks: []string{},
}
pa.addListener(newPAC)
pa.removeExpiredForUserID(userID)
}
func (pa *PluginAdapter) OnWebSocketDisconnect(webConnID, userID string) {
pac, ok := pa.GetListenerByWebConnID(webConnID)
if !ok {
pa.logger.Debug("received a disconnect for an unregistered webconn",
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
)
return
}
atomic.StoreInt64(&pac.inactiveAt, mm_model.GetMillis())
}
func commandFromRequest(req *mm_model.WebSocketRequest) (*WebsocketCommand, error) {
c := &WebsocketCommand{Action: strings.TrimPrefix(req.Action, websocketMessagePrefix)}
if teamID, ok := req.Data["teamId"]; ok {
c.TeamID = teamID.(string)
} else {
return nil, errMissingTeamInCommand
}
if readToken, ok := req.Data["readToken"]; ok {
c.ReadToken = readToken.(string)
}
if blockIDs, ok := req.Data["blockIds"]; ok {
c.BlockIDs = blockIDs.([]string)
}
return c, nil
}
func (pa *PluginAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
pac, ok := pa.GetListenerByWebConnID(webConnID)
if !ok {
pa.logger.Debug("received a message for an unregistered webconn",
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
mlog.String("action", req.Action),
)
return
}
// only process messages using the plugin actions
if !strings.HasPrefix(req.Action, websocketMessagePrefix) {
return
}
command, err := commandFromRequest(req)
if err != nil {
pa.logger.Error("error getting command from request",
mlog.String("action", req.Action),
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
mlog.Err(err),
)
return
}
switch command.Action {
// The block-related commands are not implemented in the adapter
// as there is no such thing as unauthenticated websocket
// connections in plugin mode. Only a debug line is logged
case websocketActionSubscribeBlocks, websocketActionUnsubscribeBlocks:
pa.logger.Debug(`Command not implemented in plugin mode`,
mlog.String("command", command.Action),
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
mlog.String("teamID", command.TeamID),
)
case websocketActionSubscribeTeam:
pa.logger.Debug(`Command not implemented in plugin mode`,
mlog.String("command", command.Action),
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
mlog.String("teamID", command.TeamID),
)
if !pa.auth.DoesUserHaveTeamAccess(userID, command.TeamID) {
return
}
pa.subscribeListenerToTeam(pac, command.TeamID)
case websocketActionUnsubscribeTeam:
pa.logger.Debug(`Command: UNSUBSCRIBE_WORKSPACE`,
mlog.String("webConnID", webConnID),
mlog.String("userID", userID),
mlog.String("teamID", command.TeamID),
)
pa.unsubscribeListenerFromTeam(pac, command.TeamID)
}
}
// sendMessageToAll will send a websocket message to all clients on all nodes.
func (pa *PluginAdapter) sendMessageToAll(event string, payload map[string]interface{}) {
// Empty &mm_model.WebsocketBroadcast will send to all users
pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{})
}
func (pa *PluginAdapter) BroadcastConfigChange(pluginConfig model.ClientConfig) {
pa.sendMessageToAll(websocketActionUpdateConfig, utils.StructToMap(pluginConfig))
}
// sendUserMessageSkipCluster sends the message to specific users.
func (pa *PluginAdapter) sendUserMessageSkipCluster(event string, payload map[string]interface{}, userIDs ...string) {
for _, userID := range userIDs {
pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{UserId: userID})
}
}
// sendTeamMessageSkipCluster sends a message to all the users
// with a websocket client subscribed to a given team.
func (pa *PluginAdapter) sendTeamMessageSkipCluster(event, teamID string, payload map[string]interface{}) {
userIDs := pa.getUserIDsForTeam(teamID)
pa.sendUserMessageSkipCluster(event, payload, userIDs...)
}
// sendTeamMessage sends and propagates a message that is aimed
// for all the users that are subscribed to a given team.
func (pa *PluginAdapter) sendTeamMessage(event, teamID string, payload map[string]interface{}, ensureUserIDs ...string) {
go func() {
clusterMessage := &ClusterMessage{
TeamID: teamID,
Payload: payload,
EnsureUsers: ensureUserIDs,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendTeamMessageSkipCluster(event, teamID, payload)
}
// sendBoardMessageSkipCluster sends a message to all the users
// subscribed to a given team that belong to one of its boards.
func (pa *PluginAdapter) sendBoardMessageSkipCluster(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) {
userIDs := pa.getUserIDsForTeamAndBoard(teamID, boardID, ensureUserIDs...)
pa.sendUserMessageSkipCluster(websocketActionUpdateBoard, payload, userIDs...)
}
// sendBoardMessage sends and propagates a message that is aimed for
// all the users that are subscribed to the board's team and are
// members of it too.
func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) {
go func() {
clusterMessage := &ClusterMessage{
TeamID: teamID,
BoardID: boardID,
Payload: payload,
EnsureUsers: ensureUserIDs,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendBoardMessageSkipCluster(teamID, boardID, payload, ensureUserIDs...)
}
func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block *model.Block) {
pa.logger.Trace("BroadcastingBlockChange",
mlog.String("teamID", teamID),
mlog.String("boardID", block.BoardID),
mlog.String("blockID", block.ID),
)
message := UpdateBlockMsg{
Action: websocketActionUpdateBlock,
TeamID: teamID,
Block: block,
}
pa.sendBoardMessage(teamID, block.BoardID, utils.StructToMap(message))
}
func (pa *PluginAdapter) BroadcastCategoryChange(category model.Category) {
pa.logger.Debug("BroadcastCategoryChange",
mlog.String("userID", category.UserID),
mlog.String("teamID", category.TeamID),
mlog.String("categoryID", category.ID),
)
message := UpdateCategoryMessage{
Action: websocketActionUpdateCategory,
TeamID: category.TeamID,
Category: &category,
}
payload := utils.StructToMap(message)
go func() {
clusterMessage := &ClusterMessage{
Payload: payload,
UserID: category.UserID,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(websocketActionUpdateCategory, payload, category.UserID)
}
func (pa *PluginAdapter) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) {
pa.logger.Debug("BroadcastCategoryReorder",
mlog.String("userID", userID),
mlog.String("teamID", teamID),
)
message := CategoryReorderMessage{
Action: websocketActionReorderCategories,
CategoryOrder: categoryOrder,
TeamID: teamID,
}
payload := utils.StructToMap(message)
go func() {
clusterMessage := &ClusterMessage{
Payload: payload,
UserID: userID,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
}
func (pa *PluginAdapter) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardsOrder []string) {
pa.logger.Debug("BroadcastCategoryBoardsReorder",
mlog.String("userID", userID),
mlog.String("teamID", teamID),
mlog.String("categoryID", categoryID),
)
message := CategoryBoardReorderMessage{
Action: websocketActionReorderCategoryBoards,
CategoryID: categoryID,
BoardOrder: boardsOrder,
TeamID: teamID,
}
payload := utils.StructToMap(message)
go func() {
clusterMessage := &ClusterMessage{
Payload: payload,
UserID: userID,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
}
func (pa *PluginAdapter) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) {
pa.logger.Debug(
"BroadcastCategoryBoardChange",
mlog.String("userID", userID),
mlog.String("teamID", teamID),
mlog.Int("numEntries", len(boardCategories)),
)
message := UpdateCategoryMessage{
Action: websocketActionUpdateCategoryBoard,
TeamID: teamID,
BoardCategories: boardCategories,
}
payload := utils.StructToMap(message)
go func() {
clusterMessage := &ClusterMessage{
Payload: payload,
UserID: userID,
}
pa.sendMessageToCluster(clusterMessage)
}()
pa.sendUserMessageSkipCluster(websocketActionUpdateCategoryBoard, utils.StructToMap(message), userID)
}
func (pa *PluginAdapter) BroadcastBlockDelete(teamID, blockID, boardID string) {
now := utils.GetMillis()
block := &model.Block{}
block.ID = blockID
block.BoardID = boardID
block.UpdateAt = now
block.DeleteAt = now
pa.BroadcastBlockChange(teamID, block)
}
func (pa *PluginAdapter) BroadcastBoardChange(teamID string, board *model.Board) {
pa.logger.Debug("BroadcastingBoardChange",
mlog.String("teamID", teamID),
mlog.String("boardID", board.ID),
)
message := UpdateBoardMsg{
Action: websocketActionUpdateBoard,
TeamID: teamID,
Board: board,
}
pa.sendBoardMessage(teamID, board.ID, utils.StructToMap(message))
}
func (pa *PluginAdapter) BroadcastBoardDelete(teamID, boardID string) {
now := utils.GetMillis()
board := &model.Board{}
board.ID = boardID
board.TeamID = teamID
board.UpdateAt = now
board.DeleteAt = now
pa.BroadcastBoardChange(teamID, board)
}
func (pa *PluginAdapter) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) {
pa.logger.Debug("BroadcastingMemberChange",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.String("userID", member.UserID),
)
message := UpdateMemberMsg{
Action: websocketActionUpdateMember,
TeamID: teamID,
Member: member,
}
pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), member.UserID)
}
func (pa *PluginAdapter) BroadcastMemberDelete(teamID, boardID, userID string) {
pa.logger.Debug("BroadcastingMemberDelete",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.String("userID", userID),
)
message := UpdateMemberMsg{
Action: websocketActionDeleteMember,
TeamID: teamID,
Member: &model.BoardMember{UserID: userID, BoardID: boardID},
}
// when fetching the members of the board that should receive the
// member deletion message, the deleted member will not be one of
// them, so we need to ensure they receive the message
pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), userID)
}
func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) {
pa.logger.Debug("BroadcastingSubscriptionChange",
mlog.String("TeamID", teamID),
mlog.String("blockID", subscription.BlockID),
mlog.String("subscriberID", subscription.SubscriberID),
)
message := UpdateSubscription{
Action: websocketActionUpdateSubscription,
Subscription: subscription,
}
pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message))
}
func (pa *PluginAdapter) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
pa.logger.Debug("BroadcastCardLimitTimestampChange",
mlog.Int64("cardLimitTimestamp", cardLimitTimestamp),
)
message := UpdateCardLimitTimestamp{
Action: websocketActionUpdateCardLimitTimestamp,
Timestamp: cardLimitTimestamp,
}
pa.sendMessageToAll(websocketActionUpdateCardLimitTimestamp, utils.StructToMap(message))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package ws
import (
"sync"
"sync/atomic"
"time"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
type PluginAdapterClient struct {
inactiveAt int64
webConnID string
userID string
teams []string
blocks []string
mu sync.RWMutex
}
func (pac *PluginAdapterClient) isActive() bool {
return atomic.LoadInt64(&pac.inactiveAt) == 0
}
func (pac *PluginAdapterClient) hasExpired(threshold time.Duration) bool {
return !mm_model.GetTimeForMillis(atomic.LoadInt64(&pac.inactiveAt)).Add(threshold).After(time.Now())
}
func (pac *PluginAdapterClient) subscribeToTeam(teamID string) {
pac.mu.Lock()
defer pac.mu.Unlock()
pac.teams = append(pac.teams, teamID)
}
func (pac *PluginAdapterClient) unsubscribeFromTeam(teamID string) {
pac.mu.Lock()
defer pac.mu.Unlock()
newClientTeams := []string{}
for _, id := range pac.teams {
if id != teamID {
newClientTeams = append(newClientTeams, id)
}
}
pac.teams = newClientTeams
}
func (pac *PluginAdapterClient) unsubscribeFromBlock(blockID string) {
pac.mu.Lock()
defer pac.mu.Unlock()
newClientBlocks := []string{}
for _, id := range pac.blocks {
if id != blockID {
newClientBlocks = append(newClientBlocks, id)
}
}
pac.blocks = newClientBlocks
}
func (pac *PluginAdapterClient) isSubscribedToTeam(teamID string) bool {
pac.mu.RLock()
defer pac.mu.RUnlock()
for _, id := range pac.teams {
if id == teamID {
return true
}
}
return false
}
//nolint:unused
func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool {
pac.mu.RLock()
defer pac.mu.RUnlock()
for _, id := range pac.blocks {
if id == blockID {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package ws
import (
"encoding/json"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type ClusterMessage struct {
TeamID string
BoardID string
UserID string
Payload map[string]interface{}
EnsureUsers []string
}
func (pa *PluginAdapter) sendMessageToCluster(clusterMessage *ClusterMessage) {
const id = "websocket_message"
b, err := json.Marshal(clusterMessage)
if err != nil {
pa.logger.Error("couldn't get JSON bytes from cluster message",
mlog.String("id", id),
mlog.Err(err),
)
return
}
event := mm_model.PluginClusterEvent{Id: id, Data: b}
opts := mm_model.PluginClusterEventSendOptions{
SendType: mm_model.PluginClusterEventSendTypeReliable,
}
if err := pa.api.PublishPluginClusterEvent(event, opts); err != nil {
pa.logger.Error("error publishing cluster event",
mlog.String("id", id),
mlog.Err(err),
)
}
}
func (pa *PluginAdapter) HandleClusterEvent(ev mm_model.PluginClusterEvent) {
pa.logger.Debug("received cluster event", mlog.String("id", ev.Id))
var clusterMessage ClusterMessage
if err := json.Unmarshal(ev.Data, &clusterMessage); err != nil {
pa.logger.Error("cannot unmarshal cluster message data",
mlog.String("id", ev.Id),
mlog.Err(err),
)
return
}
if clusterMessage.BoardID != "" {
pa.sendBoardMessageSkipCluster(clusterMessage.TeamID, clusterMessage.BoardID, clusterMessage.Payload, clusterMessage.EnsureUsers...)
return
}
var action string
if actionRaw, ok := clusterMessage.Payload["action"]; ok {
if s, ok := actionRaw.(string); ok {
action = s
}
}
if action == "" {
// no action was specified in the event; assume block change and warn.
pa.logger.Warn("cannot determine action from cluster message data",
mlog.String("id", ev.Id),
mlog.Map("payload", clusterMessage.Payload),
)
return
}
if clusterMessage.UserID != "" {
pa.sendUserMessageSkipCluster(action, clusterMessage.Payload, clusterMessage.UserID)
return
}
pa.sendTeamMessageSkipCluster(action, clusterMessage.TeamID, clusterMessage.Payload)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package ws
import (
"encoding/json"
"net/http"
"sync"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (wss *websocketSession) WriteJSON(v interface{}) error {
wss.mu.Lock()
defer wss.mu.Unlock()
err := wss.conn.WriteJSON(v)
return err
}
func (wss *websocketSession) isSubscribedToTeam(teamID string) bool {
for _, id := range wss.teams {
if id == teamID {
return true
}
}
return false
}
func (wss *websocketSession) isSubscribedToBlock(blockID string) bool {
for _, id := range wss.blocks {
if id == blockID {
return true
}
}
return false
}
// Server is a WebSocket server.
type Server struct {
upgrader websocket.Upgrader
listeners map[*websocketSession]bool
listenersByTeam map[string][]*websocketSession
listenersByBlock map[string][]*websocketSession
mu sync.RWMutex
auth *auth.Auth
singleUserToken string
isMattermostAuth bool
logger mlog.LoggerIFace
store Store
}
type websocketSession struct {
conn *websocket.Conn
userID string
mu sync.Mutex
teams []string
blocks []string
}
func (wss *websocketSession) isAuthenticated() bool {
return wss.userID != ""
}
// NewServer creates a new Server.
func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger mlog.LoggerIFace, store Store) *Server {
return &Server{
listeners: make(map[*websocketSession]bool),
listenersByTeam: make(map[string][]*websocketSession),
listenersByBlock: make(map[string][]*websocketSession),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
},
auth: auth,
singleUserToken: singleUserToken,
isMattermostAuth: isMattermostAuth,
logger: logger,
store: store,
}
}
// RegisterRoutes registers routes.
func (ws *Server) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/ws", ws.handleWebSocket)
}
func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
// Upgrade initial GET request to a websocket
client, err := ws.upgrader.Upgrade(w, r, nil)
if err != nil {
ws.logger.Error("ERROR upgrading to websocket", mlog.Err(err))
return
}
// create an empty session with websocket client
wsSession := &websocketSession{
conn: client,
userID: "",
mu: sync.Mutex{},
teams: []string{},
blocks: []string{},
}
if ws.isMattermostAuth {
wsSession.userID = r.Header.Get("Mattermost-User-Id")
}
ws.addListener(wsSession)
// Make sure we close the connection when the function returns
defer func() {
ws.logger.Debug("DISCONNECT WebSocket", mlog.Stringer("client", wsSession.conn.RemoteAddr()))
// Remove session from listeners
ws.removeListener(wsSession)
wsSession.conn.Close()
}()
// Simple message handling loop
for {
_, p, err := wsSession.conn.ReadMessage()
if err != nil {
ws.logger.Error("ERROR WebSocket",
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
mlog.Err(err),
)
ws.removeListener(wsSession)
break
}
var command WebsocketCommand
err = json.Unmarshal(p, &command)
if err != nil {
// handle this error
ws.logger.Error(`ERROR webSocket parsing command`, mlog.String("json", string(p)))
continue
}
if command.Action == websocketActionAuth {
ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", wsSession.conn.RemoteAddr()))
ws.authenticateListener(wsSession, command.Token)
continue
}
// if the client wants to subscribe to a set of blocks and it
// is sending a read token, we don't need to check for
// authentication
if command.Action == websocketActionSubscribeBlocks {
ws.logger.Debug(`Command: SUBSCRIBE_BLOCKS`,
mlog.String("teamID", command.TeamID),
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
)
if !ws.isCommandReadTokenValid(command) {
ws.logger.Error(`Rejected invalid read token`,
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
mlog.String("action", command.Action),
mlog.String("readToken", command.ReadToken),
)
continue
}
ws.subscribeListenerToBlocks(wsSession, command.BlockIDs)
continue
}
if command.Action == websocketActionUnsubscribeBlocks {
ws.logger.Debug(`Command: UNSUBSCRIBE_BLOCKS`,
mlog.String("teamID", command.TeamID),
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
)
if !ws.isCommandReadTokenValid(command) {
ws.logger.Error(`Rejected invalid read token`,
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
mlog.String("action", command.Action),
mlog.String("readToken", command.ReadToken),
)
continue
}
ws.unsubscribeListenerFromBlocks(wsSession, command.BlockIDs)
continue
}
// if the command is not authenticated at this point, it will
// not be processed
if !wsSession.isAuthenticated() {
ws.logger.Error(`Rejected unauthenticated message`,
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
mlog.String("action", command.Action),
)
continue
}
switch command.Action {
case websocketActionSubscribeTeam:
ws.logger.Debug(`Command: SUBSCRIBE_TEAM`,
mlog.String("teamID", command.TeamID),
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
)
// if single user mode, check that the userID is valid and
// assume that the user has permission if so
if ws.singleUserToken != "" {
if wsSession.userID != model.SingleUser {
continue
}
// if not in single user mode validate that the session
// has permissions to the team
} else {
ws.logger.Debug("Not single user mode")
if !ws.auth.DoesUserHaveTeamAccess(wsSession.userID, command.TeamID) {
ws.logger.Error("WS user doesn't have team access", mlog.String("teamID", command.TeamID), mlog.String("userID", wsSession.userID))
continue
}
}
ws.subscribeListenerToTeam(wsSession, command.TeamID)
case websocketActionUnsubscribeTeam:
ws.logger.Debug(`Command: UNSUBSCRIBE_TEAM`,
mlog.String("teamID", command.TeamID),
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
)
ws.unsubscribeListenerFromTeam(wsSession, command.TeamID)
default:
ws.logger.Error(`ERROR webSocket command, invalid action`, mlog.String("action", command.Action))
}
}
}
// isCommandReadTokenValid ensures that a command contains a read
// token and a set of block ids that said token is valid for.
func (ws *Server) isCommandReadTokenValid(command WebsocketCommand) bool {
if command.TeamID == "" {
return false
}
boardID := ""
// all the blocks must be part of the same board
for _, blockID := range command.BlockIDs {
block, err := ws.store.GetBlock(blockID)
if err != nil {
return false
}
if boardID == "" {
boardID = block.BoardID
continue
}
if boardID != block.BoardID {
return false
}
}
// the read token must be valid for the board
isValid, err := ws.auth.IsValidReadToken(boardID, command.ReadToken)
if err != nil {
ws.logger.Error(`ERROR when checking token validity`,
mlog.String("teamID", command.TeamID),
mlog.Err(err),
)
return false
}
return isValid
}
// addListener adds a listener to the websocket server. The listener
// should not receive any update from the server until it subscribes
// itself to some entity changes. Adding a listener to the server
// doesn't mean that it's authenticated in any way.
func (ws *Server) addListener(listener *websocketSession) {
ws.mu.Lock()
defer ws.mu.Unlock()
ws.listeners[listener] = true
}
// removeListener removes a listener and all its subscriptions, if
// any, from the websockets server.
func (ws *Server) removeListener(listener *websocketSession) {
ws.mu.Lock()
defer ws.mu.Unlock()
// remove the listener from its subscriptions, if any
// team subscriptions
for _, team := range listener.teams {
ws.removeListenerFromTeam(listener, team)
}
// block subscriptions
for _, block := range listener.blocks {
ws.removeListenerFromBlock(listener, block)
}
delete(ws.listeners, listener)
}
// subscribeListenerToTeam safely modifies the listener and the
// server to subscribe the listener to a given team updates.
func (ws *Server) subscribeListenerToTeam(listener *websocketSession, teamID string) {
if listener.isSubscribedToTeam(teamID) {
return
}
ws.mu.Lock()
defer ws.mu.Unlock()
ws.listenersByTeam[teamID] = append(ws.listenersByTeam[teamID], listener)
listener.teams = append(listener.teams, teamID)
}
// unsubscribeListenerFromTeam safely modifies the listener and
// the server data structures to remove the link between the listener
// and a given team ID.
func (ws *Server) unsubscribeListenerFromTeam(listener *websocketSession, teamID string) {
if !listener.isSubscribedToTeam(teamID) {
return
}
ws.mu.Lock()
defer ws.mu.Unlock()
ws.removeListenerFromTeam(listener, teamID)
}
// subscribeListenerToBlocks safely modifies the listener and the
// server to subscribe the listener to a given set of block updates.
func (ws *Server) subscribeListenerToBlocks(listener *websocketSession, blockIDs []string) {
ws.mu.Lock()
defer ws.mu.Unlock()
for _, blockID := range blockIDs {
if listener.isSubscribedToBlock(blockID) {
continue
}
ws.listenersByBlock[blockID] = append(ws.listenersByBlock[blockID], listener)
listener.blocks = append(listener.blocks, blockID)
}
}
// unsubscribeListenerFromBlocks safely modifies the listener and the
// server data structures to remove the link between the listener and
// a given set of block IDs.
func (ws *Server) unsubscribeListenerFromBlocks(listener *websocketSession, blockIDs []string) {
ws.mu.Lock()
defer ws.mu.Unlock()
for _, blockID := range blockIDs {
if listener.isSubscribedToBlock(blockID) {
ws.removeListenerFromBlock(listener, blockID)
}
}
}
// removeListenerFromTeam removes the listener from both its own
// block subscribed list and the server listeners by team map.
func (ws *Server) removeListenerFromTeam(listener *websocketSession, teamID string) {
// we remove the listener from the team index
newTeamListeners := []*websocketSession{}
for _, l := range ws.listenersByTeam[teamID] {
if l != listener {
newTeamListeners = append(newTeamListeners, l)
}
}
ws.listenersByTeam[teamID] = newTeamListeners
// we remove the team from the listener subscription list
newListenerTeams := []string{}
for _, id := range listener.teams {
if id != teamID {
newListenerTeams = append(newListenerTeams, id)
}
}
listener.teams = newListenerTeams
}
// removeListenerFromBlock removes the listener from both its own
// block subscribed list and the server listeners by block map.
func (ws *Server) removeListenerFromBlock(listener *websocketSession, blockID string) {
// we remove the listener from the block index
newBlockListeners := []*websocketSession{}
for _, l := range ws.listenersByBlock[blockID] {
if l != listener {
newBlockListeners = append(newBlockListeners, l)
}
}
ws.listenersByBlock[blockID] = newBlockListeners
// we remove the block from the listener subscription list
newListenerBlocks := []string{}
for _, id := range listener.blocks {
if id != blockID {
newListenerBlocks = append(newListenerBlocks, id)
}
}
listener.blocks = newListenerBlocks
}
func (ws *Server) getUserIDForToken(token string) string {
if ws.singleUserToken != "" {
if token == ws.singleUserToken {
return model.SingleUser
}
return ""
}
session, err := ws.auth.GetSession(token)
if session == nil || err != nil {
return ""
}
return session.UserID
}
func (ws *Server) authenticateListener(wsSession *websocketSession, token string) {
ws.logger.Debug("authenticateListener",
mlog.String("token", token),
mlog.String("wsSession.userID", wsSession.userID),
)
if wsSession.isAuthenticated() {
// Do not allow multiple auth calls (for security)
ws.logger.Debug(
"authenticateListener: Ignoring already authenticated session",
mlog.String("userID", wsSession.userID),
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
)
return
}
// Authenticate session
userID := ws.getUserIDForToken(token)
if userID == "" {
wsSession.conn.Close()
return
}
// Authenticated
wsSession.userID = userID
ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.conn.RemoteAddr()))
}
// getListenersForBlock returns the listeners subscribed to a
// block changes.
func (ws *Server) getListenersForBlock(blockID string) []*websocketSession {
return ws.listenersByBlock[blockID]
}
// getListenersForTeam returns the listeners subscribed to a
// team changes.
func (ws *Server) getListenersForTeam(teamID string) []*websocketSession {
return ws.listenersByTeam[teamID]
}
// getListenersForTeamAndBoard returns the listeners subscribed to a
// team changes and members of a given board.
func (ws *Server) getListenersForTeamAndBoard(teamID, boardID string, ensureUsers ...string) []*websocketSession {
members, err := ws.store.GetMembersForBoard(boardID)
if err != nil {
ws.logger.Error("error getting members for board",
mlog.String("method", "getListenersForTeamAndBoard"),
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
)
return nil
}
memberMap := map[string]bool{}
for _, member := range members {
memberMap[member.UserID] = true
}
for _, id := range ensureUsers {
memberMap[id] = true
}
memberIDs := []string{}
for id := range memberMap {
memberIDs = append(memberIDs, id)
}
listeners := []*websocketSession{}
for _, memberID := range memberIDs {
for _, listener := range ws.listenersByTeam[teamID] {
if listener.userID == memberID {
listeners = append(listeners, listener)
}
}
}
return listeners
}
// BroadcastBlockDelete broadcasts delete messages to clients.
func (ws *Server) BroadcastBlockDelete(teamID, blockID, boardID string) {
now := utils.GetMillis()
block := &model.Block{}
block.ID = blockID
block.BoardID = boardID
block.UpdateAt = now
block.DeleteAt = now
ws.BroadcastBlockChange(teamID, block)
}
// BroadcastBlockChange broadcasts update messages to clients.
func (ws *Server) BroadcastBlockChange(teamID string, block *model.Block) {
blockIDsToNotify := []string{block.ID, block.ParentID}
message := UpdateBlockMsg{
Action: websocketActionUpdateBlock,
TeamID: teamID,
Block: block,
}
listeners := ws.getListenersForTeamAndBoard(teamID, block.BoardID)
ws.logger.Trace("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.String("boardID", block.BoardID),
)
for _, blockID := range blockIDsToNotify {
listeners = append(listeners, ws.getListenersForBlock(blockID)...)
ws.logger.Trace("listener(s) for blockID",
mlog.Int("listener_count", len(listeners)),
mlog.String("blockID", blockID),
)
}
for _, listener := range listeners {
ws.logger.Debug("Broadcast block change",
mlog.String("teamID", teamID),
mlog.String("blockID", block.ID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
err := listener.WriteJSON(message)
if err != nil {
ws.logger.Error("broadcast error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastCategoryChange(category model.Category) {
message := UpdateCategoryMessage{
Action: websocketActionUpdateCategory,
TeamID: category.TeamID,
Category: &category,
}
listeners := ws.getListenersForTeam(category.TeamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", category.TeamID),
mlog.String("categoryID", category.ID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast block change",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", category.TeamID),
mlog.String("categoryID", category.ID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
if err := listener.WriteJSON(message); err != nil {
ws.logger.Error("broadcast category change error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) {
message := CategoryReorderMessage{
Action: websocketActionReorderCategories,
CategoryOrder: categoryOrder,
TeamID: teamID,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast category order change",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
if err := listener.WriteJSON(message); err != nil {
ws.logger.Error("broadcast category order change error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardOrder []string) {
message := CategoryBoardReorderMessage{
Action: websocketActionReorderCategoryBoards,
CategoryID: categoryID,
BoardOrder: boardOrder,
TeamID: teamID,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast board category order change",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
if err := listener.WriteJSON(message); err != nil {
ws.logger.Error("broadcast category order change error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) {
message := UpdateCategoryMessage{
Action: websocketActionUpdateCategoryBoard,
TeamID: teamID,
BoardCategories: boardCategories,
}
listeners := ws.getListenersForTeam(teamID)
ws.logger.Debug("listener(s) for teamID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.Int("numEntries", len(boardCategories)),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast block change",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.Int("numEntries", len(boardCategories)),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
if err := listener.WriteJSON(message); err != nil {
ws.logger.Error("broadcast category change error", mlog.Err(err))
listener.conn.Close()
}
}
}
// BroadcastConfigChange broadcasts update messages to clients.
func (ws *Server) BroadcastConfigChange(clientConfig model.ClientConfig) {
message := UpdateClientConfig{
Action: websocketActionUpdateConfig,
ClientConfig: clientConfig,
}
listeners := ws.listeners
ws.logger.Debug("broadcasting config change to listener(s)",
mlog.Int("listener_count", len(listeners)),
)
for listener := range listeners {
ws.logger.Debug("Broadcast Config change",
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
err := listener.WriteJSON(message)
if err != nil {
ws.logger.Error("broadcast error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastBoardChange(teamID string, board *model.Board) {
message := UpdateBoardMsg{
Action: websocketActionUpdateBoard,
TeamID: teamID,
Board: board,
}
listeners := ws.getListenersForTeamAndBoard(teamID, board.ID)
ws.logger.Trace("listener(s) for teamID and boardID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.String("boardID", board.ID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast board change",
mlog.String("teamID", teamID),
mlog.String("boardID", board.ID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
err := listener.WriteJSON(message)
if err != nil {
ws.logger.Error("broadcast error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastBoardDelete(teamID, boardID string) {
now := utils.GetMillis()
board := &model.Board{}
board.ID = boardID
board.TeamID = teamID
board.UpdateAt = now
board.DeleteAt = now
ws.BroadcastBoardChange(teamID, board)
}
func (ws *Server) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) {
message := UpdateMemberMsg{
Action: websocketActionUpdateMember,
TeamID: teamID,
Member: member,
}
listeners := ws.getListenersForTeamAndBoard(teamID, boardID)
ws.logger.Trace("listener(s) for teamID and boardID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast member change",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
err := listener.WriteJSON(message)
if err != nil {
ws.logger.Error("broadcast error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) {
message := UpdateMemberMsg{
Action: websocketActionDeleteMember,
TeamID: teamID,
Member: &model.BoardMember{UserID: userID, BoardID: boardID},
}
// when fetching the members of the board that should receive the
// member deletion message, the deleted member will not be one of
// them, so we need to ensure they receive the message
listeners := ws.getListenersForTeamAndBoard(teamID, boardID, userID)
ws.logger.Trace("listener(s) for teamID and boardID",
mlog.Int("listener_count", len(listeners)),
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
)
for _, listener := range listeners {
ws.logger.Debug("Broadcast member removal",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
)
err := listener.WriteJSON(message)
if err != nil {
ws.logger.Error("broadcast error", mlog.Err(err))
listener.conn.Close()
}
}
}
func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
// not implemented for standalone server.
}
func (ws *Server) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
// not implemented for standalone server.
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/mux"
graphql "github.com/graph-gophers/graphql-go"
_ "github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Routes struct {
Root *mux.Router // ''
APIRoot *mux.Router // 'api/v4'
APIRoot5 *mux.Router // 'api/v5'
Users *mux.Router // 'api/v4/users'
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9\\_\\-\\.]+}'
UserByEmail *mux.Router // 'api/v4/users/email/{email:.+}'
Bots *mux.Router // 'api/v4/bots'
Bot *mux.Router // 'api/v4/bots/{bot_user_id:[A-Za-z0-9]+}'
Teams *mux.Router // 'api/v4/teams'
TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
TeamForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
UserThreads *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
UserThread *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members'
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
TeamMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/members'
Channels *mux.Router // 'api/v4/channels'
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts'
PostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
Uploads *mux.Router // 'api/v4/uploads'
Upload *mux.Router // 'api/v4/uploads/{upload_id:[A-Za-z0-9]+}'
Plugins *mux.Router // 'api/v4/plugins'
Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
PublicFile *mux.Router // '/files/{file_id:[A-Za-z0-9]+}/public'
Commands *mux.Router // 'api/v4/commands'
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
Hooks *mux.Router // 'api/v4/hooks'
IncomingHooks *mux.Router // 'api/v4/hooks/incoming'
IncomingHook *mux.Router // 'api/v4/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'api/v4/hooks/outgoing'
OutgoingHook *mux.Router // 'api/v4/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'api/v4/oauth'
OAuthApps *mux.Router // 'api/v4/oauth/apps'
OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}'
OpenGraph *mux.Router // 'api/v4/opengraph'
SAML *mux.Router // 'api/v4/saml'
Compliance *mux.Router // 'api/v4/compliance'
Cluster *mux.Router // 'api/v4/cluster'
Image *mux.Router // 'api/v4/image'
LDAP *mux.Router // 'api/v4/ldap'
Elasticsearch *mux.Router // 'api/v4/elasticsearch'
Bleve *mux.Router // 'api/v4/bleve'
DataRetention *mux.Router // 'api/v4/data_retention'
Brand *mux.Router // 'api/v4/brand'
System *mux.Router // 'api/v4/system'
Jobs *mux.Router // 'api/v4/jobs'
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
License *mux.Router // 'api/v4/license'
Public *mux.Router // 'api/v4/public'
Reactions *mux.Router // 'api/v4/reactions'
Roles *mux.Router // 'api/v4/roles'
Schemes *mux.Router // 'api/v4/schemes'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
EmojiByName *mux.Router // 'api/v4/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
ReactionByNameForPostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
TermsOfService *mux.Router // 'api/v4/terms_of_service'
Groups *mux.Router // 'api/v4/groups'
Cloud *mux.Router // 'api/v4/cloud'
Imports *mux.Router // 'api/v4/imports'
Exports *mux.Router // 'api/v4/exports'
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
Permissions *mux.Router // 'api/v4/permissions'
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
InsightsForUser *mux.Router // 'api/v4/users/me/top'
Usage *mux.Router // 'api/v4/usage'
WorkTemplates *mux.Router // 'api/v4/worktemplates'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
}
type API struct {
srv *app.Server
schema *graphql.Schema
BaseRoutes *Routes
}
func Init(srv *app.Server) (*API, error) {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.Router
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
api.BaseRoutes.TeamsForUser = api.BaseRoutes.User.PathPrefix("/teams").Subrouter()
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamForUser = api.BaseRoutes.TeamsForUser.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserThreads = api.BaseRoutes.TeamForUser.PathPrefix("/threads").Subrouter()
api.BaseRoutes.UserThread = api.BaseRoutes.TeamForUser.PathPrefix("/threads/{thread_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/members").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelForUser = api.BaseRoutes.User.PathPrefix("/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter()
api.BaseRoutes.ChannelCategories = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/categories").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostsForUser = api.BaseRoutes.User.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostForUser = api.BaseRoutes.PostsForUser.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Files = api.BaseRoutes.APIRoot.PathPrefix("/files").Subrouter()
api.BaseRoutes.File = api.BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PublicFile = api.BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.BaseRoutes.OAuth = api.BaseRoutes.APIRoot.PathPrefix("/oauth").Subrouter()
api.BaseRoutes.OAuthApps = api.BaseRoutes.OAuth.PathPrefix("/apps").Subrouter()
api.BaseRoutes.OAuthApp = api.BaseRoutes.OAuthApps.PathPrefix("/{app_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Compliance = api.BaseRoutes.APIRoot.PathPrefix("/compliance").Subrouter()
api.BaseRoutes.Cluster = api.BaseRoutes.APIRoot.PathPrefix("/cluster").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.Brand = api.BaseRoutes.APIRoot.PathPrefix("/brand").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Preferences = api.BaseRoutes.User.PathPrefix("/preferences").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Public = api.BaseRoutes.APIRoot.PathPrefix("/public").Subrouter()
api.BaseRoutes.Reactions = api.BaseRoutes.APIRoot.PathPrefix("/reactions").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.Elasticsearch = api.BaseRoutes.APIRoot.PathPrefix("/elasticsearch").Subrouter()
api.BaseRoutes.Bleve = api.BaseRoutes.APIRoot.PathPrefix("/bleve").Subrouter()
api.BaseRoutes.DataRetention = api.BaseRoutes.APIRoot.PathPrefix("/data_retention").Subrouter()
api.BaseRoutes.Emojis = api.BaseRoutes.APIRoot.PathPrefix("/emoji").Subrouter()
api.BaseRoutes.Emoji = api.BaseRoutes.APIRoot.PathPrefix("/emoji/{emoji_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.EmojiByName = api.BaseRoutes.Emojis.PathPrefix("/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.OpenGraph = api.BaseRoutes.APIRoot.PathPrefix("/opengraph").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Schemes = api.BaseRoutes.APIRoot.PathPrefix("/schemes").Subrouter()
api.BaseRoutes.Image = api.BaseRoutes.APIRoot.PathPrefix("/image").Subrouter()
api.BaseRoutes.TermsOfService = api.BaseRoutes.APIRoot.PathPrefix("/terms_of_service").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.Cloud = api.BaseRoutes.APIRoot.PathPrefix("/cloud").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter()
api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter()
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
api.InitChannel()
api.InitPost()
api.InitFile()
api.InitUpload()
api.InitSystem()
api.InitLicense()
api.InitConfig()
api.InitWebhook()
api.InitPreference()
api.InitSaml()
api.InitCompliance()
api.InitCluster()
api.InitLdap()
api.InitElasticsearch()
api.InitBleve()
api.InitDataRetention()
api.InitBrand()
api.InitJob()
api.InitCommand()
api.InitStatus()
api.InitWebSocket()
api.InitEmoji()
api.InitOAuth()
api.InitReaction()
api.InitOpenGraph()
api.InitPlugin()
api.InitRole()
api.InitScheme()
api.InitImage()
api.InitTermsOfService()
api.InitGroup()
api.InitAction()
api.InitCloud()
api.InitImport()
api.InitRemoteCluster()
api.InitSharedChannels()
api.InitPermissions()
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitWorkTemplate()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {
return nil, err
}
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
InitLocal(srv)
return api, nil
}
func InitLocal(srv *app.Server) *API {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.LocalRouter
api.BaseRoutes.APIRoot = srv.LocalRouter.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.InitUserLocal()
api.InitTeamLocal()
api.InitChannelLocal()
api.InitConfigLocal()
api.InitWebhookLocal()
api.InitPluginLocal()
api.InitCommandLocal()
api.InitLicenseLocal()
api.InitBotLocal()
api.InitGroupLocal()
api.InitLdapLocal()
api.InitSystemLocal()
api.InitPostLocal()
api.InitRoleLocal()
api.InitUploadLocal()
api.InitImportLocal()
api.InitExportLocal()
api.InitJobLocal()
api.InitSamlLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
return api
}
func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
app := app.New(app.ServerConnector(api.srv.Channels()))
web.Handle404(app, w, r)
}
var ReturnStatusOK = web.ReturnStatusOK
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
graphql "github.com/graph-gophers/graphql-go"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type TestHelper struct {
App *app.App
Server *app.Server
ConfigStore *config.Store
Context *request.Context
Client *model.Client4
GraphQLClient *graphQLClient
BasicUser *model.User
BasicUser2 *model.User
TeamAdminUser *model.User
BasicTeam *model.Team
BasicChannel *model.Channel
BasicPrivateChannel *model.Channel
BasicPrivateChannel2 *model.Channel
BasicDeletedChannel *model.Channel
BasicChannel2 *model.Channel
BasicPost *model.Post
Group *model.Group
SystemAdminClient *model.Client4
SystemAdminUser *model.User
tempWorkspace string
SystemManagerClient *model.Client4
SystemManagerUser *model.User
LocalClient *model.Client4
IncludeCacheLayer bool
LogBuffer *mlog.Buffer
TestLogger *mlog.Logger
}
var mainHelper *testlib.MainHelper
func SetMainHelper(mh *testlib.MainHelper) {
mainHelper = mh
}
func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, enterprise bool, includeCache bool,
updateConfig func(*model.Config), options []app.Option) *TestHelper {
tempWorkspace, err := os.MkdirTemp("", "apptest")
if err != nil {
panic(err)
}
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
memoryConfig := &model.Config{}
memoryConfig.SetDefaults()
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
memoryConfig.ServiceSettings.EnableLocalMode = model.NewBool(true)
*memoryConfig.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock")
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
if updateConfig != nil {
updateConfig(memoryConfig)
}
memoryStore.Set(memoryConfig)
configStore, err := config.NewStoreFromBacking(memoryStore, nil, false)
if err != nil {
panic(err)
}
options = append(options, app.ConfigStore(configStore))
if includeCache {
// Adds the cache layer to the test store
options = append(options, app.StoreOverrideWithCache(dbStore))
} else {
options = append(options, app.StoreOverride(dbStore))
}
buffer := &mlog.Buffer{}
testLogger, _ := mlog.NewLogger()
logCfg, _ := config.MloggerConfigFromLoggerConfig(&memoryConfig.LogSettings, nil, config.GetLogFileLocation)
if errCfg := testLogger.ConfigureTargets(logCfg, nil); errCfg != nil {
panic("failed to configure test logger: " + errCfg.Error())
}
if errW := mlog.AddWriterTarget(testLogger, buffer, true, mlog.StdAll...); errW != nil {
panic("failed to add writer target to test logger: " + errW.Error())
}
// lock logger config so server init cannot override it during testing.
testLogger.LockConfiguration()
options = append(options, app.SetLogger(testLogger))
s, err := app.NewServer(options...)
if err != nil {
panic(err)
}
th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())),
Server: s,
ConfigStore: configStore,
IncludeCacheLayer: includeCache,
Context: request.EmptyContext(testLogger),
TestLogger: testLogger,
LogBuffer: buffer,
}
th.Context.SetLogger(testLogger)
if s.Platform().SearchEngine != nil && s.Platform().SearchEngine.BleveEngine != nil && searchEngine != nil {
searchEngine.BleveEngine = s.Platform().SearchEngine.BleveEngine
}
if searchEngine != nil {
th.App.SetSearchEngine(searchEngine)
}
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.MaxUsersPerTeam = 50
*cfg.RateLimitSettings.Enable = false
*cfg.EmailSettings.SendEmailNotifications = true
*cfg.ServiceSettings.SiteURL = ""
// Disable sniffing, otherwise elastic client fails to connect to docker node
// More details: https://github.com/olivere/elastic/wiki/Sniffing
*cfg.ElasticsearchSettings.Sniff = false
*cfg.TeamSettings.EnableOpenServer = true
// Disable strict password requirements for test
*cfg.PasswordSettings.MinimumLength = 5
*cfg.PasswordSettings.Lowercase = false
*cfg.PasswordSettings.Uppercase = false
*cfg.PasswordSettings.Symbol = false
*cfg.PasswordSettings.Number = false
*cfg.ServiceSettings.ListenAddress = ":0"
})
if err := th.Server.Start(); err != nil {
panic(err)
}
Init(th.App.Srv())
web.New(th.App.Srv())
wsapi.Init(th.App.Srv())
if enterprise {
th.App.Srv().SetLicense(model.NewTestLicense())
} else {
th.App.Srv().SetLicense(nil)
}
th.Client = th.CreateClient()
th.GraphQLClient = newGraphQLClient(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
th.SystemAdminClient = th.CreateClient()
th.SystemManagerClient = th.CreateClient()
// Verify handling of the supported true/false values by randomizing on each run.
rand.Seed(time.Now().UTC().UnixNano())
trueValues := []string{"1", "t", "T", "TRUE", "true", "True"}
falseValues := []string{"0", "f", "F", "FALSE", "false", "False"}
trueString := trueValues[rand.Intn(len(trueValues))]
falseString := falseValues[rand.Intn(len(falseValues))]
mlog.Debug("Configured Client4 bool string values", mlog.String("true", trueString), mlog.String("false", falseString))
th.Client.SetBoolString(true, trueString)
th.Client.SetBoolString(false, falseString)
th.LocalClient = th.CreateLocalClient(*memoryConfig.ServiceSettings.LocalModeSocketLocation)
if th.tempWorkspace == "" {
th.tempWorkspace = tempWorkspace
}
return th
}
func SetupEnterprise(tb testing.TB, options ...app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, true, true, nil, options)
th.InitLogin()
return th
}
func Setup(tb testing.TB) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.InitLogin()
return th
}
func SetupAndApplyConfigBeforeLogin(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.App.UpdateConfig(updateConfig)
th.InitLogin()
return th
}
func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, updateConfig, nil)
th.InitLogin()
return th
}
func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithStoreMock(tb testing.TB) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupEnterpriseWithStoreMock(tb testing.TB, options ...app.Option) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil, options)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, options)
th.InitLogin()
return th
}
func (th *TestHelper) ShutdownApp() {
done := make(chan bool)
go func() {
th.Server.Shutdown()
close(done)
}()
select {
case <-done:
case <-time.After(30 * time.Second):
// panic instead of fatal to terminate all tests in this package, otherwise the
// still running App could spuriously fail subsequent tests.
panic("failed to shutdown App within 30 seconds")
}
}
func (th *TestHelper) TearDown() {
if th.IncludeCacheLayer {
// Clean all the caches
th.App.Srv().InvalidateAllCaches()
}
th.ShutdownApp()
}
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
var initBasicOnce sync.Once
var userCache struct {
SystemAdminUser *model.User
SystemManagerUser *model.User
TeamAdminUser *model.User
BasicUser *model.User
BasicUser2 *model.User
}
func (th *TestHelper) InitLogin() *TestHelper {
th.waitForConnectivity()
// create users once and cache them because password hashing is slow
initBasicOnce.Do(func() {
th.SystemAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemAdminUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
th.SystemAdminUser, _ = th.App.GetUser(th.SystemAdminUser.Id)
userCache.SystemAdminUser = th.SystemAdminUser.DeepCopy()
th.SystemManagerUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemManagerUser.Id, model.SystemUserRoleId+" "+model.SystemManagerRoleId, false)
th.SystemManagerUser, _ = th.App.GetUser(th.SystemManagerUser.Id)
userCache.SystemManagerUser = th.SystemManagerUser.DeepCopy()
th.TeamAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.TeamAdminUser.Id, model.SystemUserRoleId, false)
th.TeamAdminUser, _ = th.App.GetUser(th.TeamAdminUser.Id)
userCache.TeamAdminUser = th.TeamAdminUser.DeepCopy()
th.BasicUser = th.CreateUser()
th.BasicUser, _ = th.App.GetUser(th.BasicUser.Id)
userCache.BasicUser = th.BasicUser.DeepCopy()
th.BasicUser2 = th.CreateUser()
th.BasicUser2, _ = th.App.GetUser(th.BasicUser2.Id)
userCache.BasicUser2 = th.BasicUser2.DeepCopy()
})
// restore cached users
th.SystemAdminUser = userCache.SystemAdminUser.DeepCopy()
th.SystemManagerUser = userCache.SystemManagerUser.DeepCopy()
th.TeamAdminUser = userCache.TeamAdminUser.DeepCopy()
th.BasicUser = userCache.BasicUser.DeepCopy()
th.BasicUser2 = userCache.BasicUser2.DeepCopy()
users := []*model.User{th.SystemAdminUser, th.TeamAdminUser, th.BasicUser, th.BasicUser2, th.SystemManagerUser}
mainHelper.GetSQLStore().User().InsertUsers(users)
// restore non hashed password for login
th.SystemAdminUser.Password = "Pa$$word11"
th.TeamAdminUser.Password = "Pa$$word11"
th.BasicUser.Password = "Pa$$word11"
th.BasicUser2.Password = "Pa$$word11"
th.SystemManagerUser.Password = "Pa$$word11"
var wg sync.WaitGroup
wg.Add(2)
go func() {
th.LoginSystemAdmin()
wg.Done()
}()
go func() {
th.LoginTeamAdmin()
wg.Done()
}()
wg.Wait()
return th
}
func (th *TestHelper) InitBasic() *TestHelper {
th.BasicTeam = th.CreateTeam()
th.BasicChannel = th.CreatePublicChannel()
th.BasicPrivateChannel = th.CreatePrivateChannel()
th.BasicPrivateChannel2 = th.CreatePrivateChannel()
th.BasicDeletedChannel = th.CreatePublicChannel()
th.BasicChannel2 = th.CreatePublicChannel()
th.BasicPost = th.CreatePost()
th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
th.LinkUserToTeam(th.BasicUser2, th.BasicTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicDeletedChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicDeletedChannel, false)
th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId, false)
th.Client.DeleteChannel(th.BasicDeletedChannel.Id)
th.LoginBasic()
th.Group = th.CreateGroup()
return th
}
func (th *TestHelper) waitForConnectivity() {
for i := 0; i < 1000; i++ {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", th.App.Srv().ListenAddr.Port))
if err == nil {
conn.Close()
return
}
time.Sleep(time.Millisecond * 20)
}
panic("unable to connect")
}
func (th *TestHelper) CreateClient() *model.Client4 {
return model.NewAPIv4Client(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
}
// ToDo: maybe move this to NewAPIv4SocketClient and reuse it in mmctl
func (th *TestHelper) CreateLocalClient(socketPath string) *model.Client4 {
httpClient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
}
return &model.Client4{
APIURL: "http://_" + model.APIURLSuffix,
HTTPClient: httpClient,
}
}
func (th *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken)
}
func (th *TestHelper) CreateReliableWebSocketClient(connID string, seqNo int) (*model.WebSocketClient, error) {
return model.NewReliableWebSocketClientWithDialer(websocket.DefaultDialer, fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken, connID, seqNo, true)
}
func (th *TestHelper) CreateWebSocketSystemAdminClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.SystemAdminClient.AuthToken)
}
func (th *TestHelper) CreateWebSocketClientWithClient(client *model.Client4) (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), client.AuthToken)
}
func (th *TestHelper) CreateBotWithSystemAdminClient() *model.Bot {
return th.CreateBotWithClient((th.SystemAdminClient))
}
func (th *TestHelper) CreateBotWithClient(client *model.Client4) *model.Bot {
bot := &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
}
rbot, _, err := client.CreateBot(bot)
if err != nil {
panic(err)
}
return rbot
}
func (th *TestHelper) CreateUser() *model.User {
return th.CreateUserWithClient(th.Client)
}
func (th *TestHelper) CreateTeam() *model.Team {
return th.CreateTeamWithClient(th.Client)
}
func (th *TestHelper) CreateTeamWithClient(client *model.Client4) *model.Team {
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
}
rteam, _, err := client.CreateTeam(team)
if err != nil {
panic(err)
}
return rteam
}
func (th *TestHelper) CreateUserWithClient(client *model.Client4) *model.User {
id := model.NewId()
user := &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Pa$$word11",
}
ruser, _, err := client.CreateUser(user)
if err != nil {
panic(err)
}
ruser.Password = "Pa$$word11"
_, err = th.App.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil
}
return ruser
}
func (th *TestHelper) CreateUserWithAuth(authService string) *model.User {
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
EmailVerified: true,
AuthService: authService,
}
user, err := th.App.CreateUser(th.Context, user)
if err != nil {
panic(err)
}
return user
}
func (th *TestHelper) SetupLdapConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.LdapSettings.Enable = true
*cfg.LdapSettings.EnableSync = true
*cfg.LdapSettings.LdapServer = "dockerhost"
*cfg.LdapSettings.BaseDN = "dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindUsername = "cn=admin,dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindPassword = "mostest"
*cfg.LdapSettings.FirstNameAttribute = "cn"
*cfg.LdapSettings.LastNameAttribute = "sn"
*cfg.LdapSettings.NicknameAttribute = "cn"
*cfg.LdapSettings.EmailAttribute = "mail"
*cfg.LdapSettings.UsernameAttribute = "uid"
*cfg.LdapSettings.IdAttribute = "cn"
*cfg.LdapSettings.LoginIdAttribute = "uid"
*cfg.LdapSettings.SkipCertificateVerification = true
*cfg.LdapSettings.GroupFilter = ""
*cfg.LdapSettings.GroupDisplayNameAttribute = "cN"
*cfg.LdapSettings.GroupIdAttribute = "entRyUuId"
*cfg.LdapSettings.MaxPageSize = 0
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
}
func (th *TestHelper) SetupSamlConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.Enable = true
*cfg.SamlSettings.Verify = false
*cfg.SamlSettings.Encrypt = false
*cfg.SamlSettings.IdpURL = "https://does.notmatter.example"
*cfg.SamlSettings.IdpDescriptorURL = "https://localhost/adfs/services/trust"
*cfg.SamlSettings.AssertionConsumerServiceURL = "https://localhost/login/sso/saml"
*cfg.SamlSettings.ServiceProviderIdentifier = "https://localhost/login/sso/saml"
*cfg.SamlSettings.IdpCertificateFile = app.SamlIdpCertificateName
*cfg.SamlSettings.PrivateKeyFile = app.SamlPrivateKeyName
*cfg.SamlSettings.PublicCertificateFile = app.SamlPublicCertificateName
*cfg.SamlSettings.EmailAttribute = "Email"
*cfg.SamlSettings.UsernameAttribute = "Username"
*cfg.SamlSettings.FirstNameAttribute = "FirstName"
*cfg.SamlSettings.LastNameAttribute = "LastName"
*cfg.SamlSettings.NicknameAttribute = ""
*cfg.SamlSettings.PositionAttribute = ""
*cfg.SamlSettings.LocaleAttribute = ""
*cfg.SamlSettings.SignatureAlgorithm = model.SamlSettingsSignatureAlgorithmSha256
*cfg.SamlSettings.CanonicalAlgorithm = model.SamlSettingsCanonicalAlgorithmC14n11
})
th.App.Srv().SetLicense(model.NewTestLicense("saml"))
}
func (th *TestHelper) CreatePublicChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypeOpen)
}
func (th *TestHelper) CreatePrivateChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypePrivate)
}
func (th *TestHelper) CreateChannelWithClient(client *model.Client4, channelType model.ChannelType) *model.Channel {
return th.CreateChannelWithClientAndTeam(client, channelType, th.BasicTeam.Id)
}
func (th *TestHelper) CreateChannelWithClientAndTeam(client *model.Client4, channelType model.ChannelType, teamId string) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: GenerateTestChannelName(),
Type: channelType,
TeamId: teamId,
}
rchannel, _, err := client.CreateChannel(channel)
if err != nil {
panic(err)
}
return rchannel
}
func (th *TestHelper) CreatePost() *model.Post {
return th.CreatePostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreatePinnedPost() *model.Post {
return th.CreatePinnedPostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreateMessagePost(message string) *model.Post {
return th.CreateMessagePostWithClient(th.Client, th.BasicChannel, message)
}
func (th *TestHelper) CreatePostWithFiles(files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, th.BasicChannel, files...)
}
func (th *TestHelper) CreatePostInChannelWithFiles(channel *model.Channel, files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, channel, files...)
}
func (th *TestHelper) CreatePostWithFilesWithClient(client *model.Client4, channel *model.Channel, files ...*model.FileInfo) *model.Post {
var fileIds model.StringArray
for i := range files {
fileIds = append(fileIds, files[i].Id)
}
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + model.NewId(),
FileIds: fileIds,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePinnedPostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
IsPinned: true,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostWithClient(client *model.Client4, channel *model.Channel, message string) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: message,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostNoClient(channel *model.Channel, message string, createAtTime int64) *model.Post {
post, err := th.App.Srv().Store().Post().Save(&model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: message,
CreateAt: createAtTime,
})
if err != nil {
panic(err)
}
return post
}
func (th *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
var err *model.AppError
var channel *model.Channel
if channel, err = th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, user.Id); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) LoginBasic() {
th.LoginBasicWithClient(th.Client)
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
th.LoginBasicWithGraphQL()
}
}
func (th *TestHelper) LoginBasic2() {
th.LoginBasic2WithClient(th.Client)
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
th.LoginBasicWithGraphQL()
}
}
func (th *TestHelper) LoginTeamAdmin() {
th.LoginTeamAdminWithClient(th.Client)
}
func (th *TestHelper) LoginSystemAdmin() {
th.LoginSystemAdminWithClient(th.SystemAdminClient)
}
func (th *TestHelper) LoginSystemManager() {
th.LoginSystemManagerWithClient(th.SystemManagerClient)
}
func (th *TestHelper) LoginBasicWithClient(client *model.Client4) {
_, _, err := client.Login(th.BasicUser.Email, th.BasicUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasicWithGraphQL() {
_, _, err := th.GraphQLClient.login(th.BasicUser.Email, th.BasicUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasic2WithClient(client *model.Client4) {
_, _, err := client.Login(th.BasicUser2.Email, th.BasicUser2.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginTeamAdminWithClient(client *model.Client4) {
_, _, err := client.Login(th.TeamAdminUser.Email, th.TeamAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemManagerWithClient(client *model.Client4) {
_, _, err := client.Login(th.SystemManagerUser.Email, th.SystemManagerUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemAdminWithClient(client *model.Client4) {
_, _, err := client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) UpdateActiveUser(user *model.User, active bool) {
_, err := th.App.UpdateActive(th.Context, user, active)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
_, err := th.App.JoinUserToTeam(th.Context, team, user, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) UnlinkUserFromTeam(user *model.User, team *model.Team) {
err := th.App.RemoveUserFromTeam(th.Context, team.Id, user.Id, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
member, err := th.App.AddUserToChannel(th.Context, user, channel, false)
if err != nil {
panic(err)
}
return member
}
func (th *TestHelper) RemoveUserFromChannel(user *model.User, channel *model.Channel) {
err := th.App.RemoveUserFromChannel(th.Context, user.Id, "", channel)
if err != nil {
panic(err)
}
}
func (th *TestHelper) GenerateTestEmail() string {
if *th.App.Config().EmailSettings.SMTPServer != "localhost" && os.Getenv("CI_INBUCKET_PORT") == "" {
return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
}
return strings.ToLower(model.NewId() + "@localhost")
}
func (th *TestHelper) CreateGroup() *model.Group {
id := model.NewId()
group := &model.Group{
Name: model.NewString("n-" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
}
group, err := th.App.CreateGroup(group)
if err != nil {
panic(err)
}
return group
}
// TestForSystemAdminAndLocal runs a test function for both
// SystemAdmin and Local clients. Several endpoints work in the same
// way when used by a fully privileged user and through the local
// mode, so this helper facilitates checking both
func (th *TestHelper) TestForSystemAdminAndLocal(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
// TestForAllClients runs a test function for all the clients
// registered in the TestHelper
func (th *TestHelper) TestForAllClients(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"Client", func(t *testing.T) {
f(t, th.Client)
})
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
func GenerateTestUsername() string {
return "fakeuser" + model.NewRandomString(10)
}
func GenerateTestTeamName() string {
return "faketeam" + model.NewRandomString(6)
}
func GenerateTestChannelName() string {
return "fakechannel" + model.NewRandomString(10)
}
func GenerateTestAppName() string {
return "fakeoauthapp" + model.NewRandomString(10)
}
func GenerateTestId() string {
return model.NewId()
}
func CheckUserSanitization(tb testing.TB, user *model.User) {
tb.Helper()
require.Equal(tb, "", user.Password, "password wasn't blank")
require.Empty(tb, user.AuthData, "auth data wasn't blank")
require.Equal(tb, "", user.MfaSecret, "mfa secret wasn't blank")
}
func CheckEtag(tb testing.TB, data any, resp *model.Response) {
tb.Helper()
require.Empty(tb, data)
require.Equal(tb, http.StatusNotModified, resp.StatusCode, "wrong status code for etag")
}
func checkHTTPStatus(tb testing.TB, resp *model.Response, expectedStatus int) {
tb.Helper()
require.NotNilf(tb, resp, "Unexpected nil response, expected http status:%v", expectedStatus)
require.Equalf(tb, expectedStatus, resp.StatusCode, "Expected http status:%v, got %v", expectedStatus, resp.StatusCode)
}
func CheckOKStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusOK)
}
func CheckCreatedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusCreated)
}
func CheckForbiddenStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusForbidden)
}
func CheckUnauthorizedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusUnauthorized)
}
func CheckNotFoundStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotFound)
}
func CheckBadRequestStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusBadRequest)
}
func CheckNotImplementedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotImplemented)
}
func CheckRequestEntityTooLargeStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusRequestEntityTooLarge)
}
func CheckInternalErrorStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusInternalServerError)
}
func CheckServiceUnavailableStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusServiceUnavailable)
}
func CheckErrorID(tb testing.TB, err error, errorId string) {
tb.Helper()
require.Error(tb, err, "should have errored with id: %s", errorId)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, errorId, appError.Id, "incorrect error id, actual: %s, expected: %s", appError.Id, errorId)
}
func CheckErrorMessage(tb testing.TB, err error, message string) {
tb.Helper()
require.Error(tb, err, "should have errored with message: %s", message)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, message, appError.Message, "incorrect error message, actual: %s, expected: %s", appError.Id, message)
}
func CheckStartsWith(tb testing.TB, value, prefix, message string) {
tb.Helper()
require.True(tb, strings.HasPrefix(value, prefix), message, value)
}
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func s3New(endpoint, accessKey, secretKey string, secure bool, signV2 bool, region string) (*s3.Client, error) {
var creds *credentials.Credentials
if signV2 {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV4)
}
opts := s3.Options{
Creds: creds,
Secure: secure,
Region: region,
}
return s3.New(endpoint, &opts)
}
func (th *TestHelper) cleanupTestFile(info *model.FileInfo) error {
cfg := th.App.Config()
if *cfg.FileSettings.DriverName == model.ImageDriverS3 {
endpoint := *cfg.FileSettings.AmazonS3Endpoint
accessKey := *cfg.FileSettings.AmazonS3AccessKeyId
secretKey := *cfg.FileSettings.AmazonS3SecretAccessKey
secure := *cfg.FileSettings.AmazonS3SSL
signV2 := *cfg.FileSettings.AmazonS3SignV2
region := *cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return err
}
bucket := *cfg.FileSettings.AmazonS3Bucket
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.Path, s3.RemoveObjectOptions{}); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.ThumbnailPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.PreviewPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
} else if *cfg.FileSettings.DriverName == model.ImageDriverLocal {
if err := os.Remove(*cfg.FileSettings.Directory + info.Path); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.ThumbnailPath); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.PreviewPath); err != nil {
return err
}
}
}
return nil
}
func (th *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Channel) {
if cm, err := th.App.Srv().Store().Channel().GetMember(context.Background(), channel.Id, user.Id); err == nil {
cm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Channel().UpdateMember(cm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(context.Background(), team.Id, user.Id); err == nil {
tm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Team().UpdateMember(tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToNonTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(context.Background(), team.Id, user.Id); err == nil {
tm.SchemeAdmin = false
if _, err = th.App.Srv().Store().Team().UpdateMember(tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) SaveDefaultRolePermissions() map[string][]string {
results := make(map[string][]string)
for _, roleName := range []string{
"system_user",
"system_admin",
"team_user",
"team_admin",
"channel_user",
"channel_admin",
} {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
results[roleName] = role.Permissions
}
return results
}
func (th *TestHelper) RestoreDefaultRolePermissions(data map[string][]string) {
for roleName, permissions := range data {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") {
continue
}
role.Permissions = permissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
}
func (th *TestHelper) RemovePermissionFromRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
var newPermissions []string
for _, p := range role.Permissions {
if p != permission {
newPermissions = append(newPermissions, p)
}
}
if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") {
return
}
role.Permissions = newPermissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) AddPermissionToRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
for _, existingPermission := range role.Permissions {
if existingPermission == permission {
return
}
}
role.Permissions = append(role.Permissions, permission)
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) SetupTeamScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeTeam)
}
func (th *TestHelper) SetupChannelScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeChannel)
}
func (th *TestHelper) SetupScheme(scope string) *model.Scheme {
scheme, err := th.App.CreateScheme(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Scope: scope,
})
if err != nil {
panic(err)
}
return scheme
}
func (th *TestHelper) MakeGraphQLRequest(input *graphQLInput) (*graphql.Response, error) {
url := fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port) + model.APIURLSuffixV5 + "/graphql"
buf, err := json.Marshal(input)
if err != nil {
panic(err)
}
resp, err := th.GraphQLClient.doAPIRequest("POST", url, bytes.NewReader(buf), map[string]string{})
if err != nil {
panic(err)
}
defer closeBody(resp)
var gqlResp *graphql.Response
err = json.NewDecoder(resp.Body).Decode(&gqlResp)
return gqlResp, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBleve() {
api.BaseRoutes.Bleve.Handle("/purge_indexes", api.APISessionRequired(purgeBleveIndexes)).Methods("POST")
}
func purgeBleveIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeBleveIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeBleveIndexes) {
c.SetPermissionError(model.PermissionPurgeBleveIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeBleveIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeBleveIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitBot() {
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(createBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(patchBot)).Methods("PUT")
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(getBot)).Methods("GET")
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(getBots)).Methods("GET")
api.BaseRoutes.Bot.Handle("/disable", api.APISessionRequired(disableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/enable", api.APISessionRequired(enableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APISessionRequired(convertBotToUser)).Methods("POST")
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APISessionRequired(assignBot)).Methods("POST")
}
func createBot(c *Context, w http.ResponseWriter, r *http.Request) {
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
bot := &model.Bot{
OwnerId: c.AppContext.Session().UserId,
}
bot.Patch(botPatch)
auditRec := c.MakeAuditRecord("createBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateBot) {
c.SetPermissionError(model.PermissionCreateBot)
return
}
if user, err := c.App.GetUser(c.AppContext.Session().UserId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionCreateBot)
return
}
}
if !*c.App.Config().ServiceSettings.EnableBotAccountCreation {
c.Err = model.NewAppError("createBot", "api.bot.create_disabled", nil, "", http.StatusForbidden)
return
}
createdBot, appErr := c.App.CreateBot(c.AppContext, bot)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventObjectType("bot")
auditRec.AddEventResultState(createdBot) // overwrite meta
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(createdBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
auditRec := c.MakeAuditRecord("patchBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameterAuditable(auditRec, "bot", botPatch)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
updatedBot, appErr := c.App.PatchBot(botUserId, botPatch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedBot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(updatedBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
bot, appErr := c.App.GetBot(botUserId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Allow access to any bot.
} else if bot.OwnerId == c.AppContext.Session().UserId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// Pretend like the bot doesn't exist at all to avoid revealing that the
// user is a bot. It's kind of silly in this case, sine we created the bot,
// but we don't have read bot permissions.
c.Err = model.MakeBotNotFoundError(botUserId)
return
}
} else {
// Pretend like the bot doesn't exist at all, to avoid revealing that the
// user is a bot.
c.Err = model.MakeBotNotFoundError(botUserId)
return
}
if c.HandleEtag(bot.Etag(), "Get Bot", w, r) {
return
}
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBots(c *Context, w http.ResponseWriter, r *http.Request) {
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
onlyOrphaned, _ := strconv.ParseBool(r.URL.Query().Get("only_orphaned"))
var OwnerId string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Get bots created by any user.
OwnerId = ""
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// Only get bots created by this user.
OwnerId = c.AppContext.Session().UserId
} else {
c.SetPermissionError(model.PermissionReadBots)
return
}
bots, appErr := c.App.GetBots(&model.BotGetOptions{
Page: c.Params.Page,
PerPage: c.Params.PerPage,
OwnerId: OwnerId,
IncludeDeleted: includeDeleted,
OnlyOrphaned: onlyOrphaned,
})
if appErr != nil {
c.Err = appErr
return
}
if c.HandleEtag(bots.Etag(), "Get Bots", w, r) {
return
}
if err := json.NewEncoder(w).Encode(bots); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func disableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, false)
}
func enableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, true)
}
func updateBotActive(c *Context, w http.ResponseWriter, active bool) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
auditRec := c.MakeAuditRecord("updateBotActive", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "enable", active)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
bot, err := c.App.UpdateBotActive(c.AppContext, botUserId, active)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func assignBot(c *Context, w http.ResponseWriter, _ *http.Request) {
c.RequireUserId()
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("assignBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "user_id", userId)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
if user, err := c.App.GetUser(userId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionAssignBot)
return
}
}
bot, err := c.App.UpdateBotOwner(botUserId, userId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertBotToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
bot, err := c.App.GetBot(c.Params.BotUserId, false)
if err != nil {
c.Err = err
return
}
var userPatch model.UserPatch
jsonErr := json.NewDecoder(r.Body).Decode(&userPatch)
if jsonErr != nil || userPatch.Password == nil || *userPatch.Password == "" {
c.SetInvalidParamWithErr("userPatch", jsonErr)
return
}
systemAdmin, _ := strconv.ParseBool(r.URL.Query().Get("set_system_admin"))
auditRec := c.MakeAuditRecord("convertBotToUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
audit.AddEventParameterAuditable(auditRec, "user_patch", &userPatch)
audit.AddEventParameter(auditRec, "set_system_admin", systemAdmin)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
user, err := c.App.ConvertBotToUser(c.AppContext, bot, &userPatch, systemAdmin)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitBotLocal() {
api.BaseRoutes.Bot.Handle("", api.APILocal(getBot)).Methods("GET")
api.BaseRoutes.Bot.Handle("", api.APILocal(patchBot)).Methods("PUT")
api.BaseRoutes.Bot.Handle("/disable", api.APILocal(disableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/enable", api.APILocal(enableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APILocal(convertBotToUser)).Methods("POST")
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APILocal(assignBot)).Methods("POST")
api.BaseRoutes.Bots.Handle("", api.APILocal(getBots)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBrand() {
api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
}
func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required
img, err := c.App.GetBrandImage()
if err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write(nil)
return
}
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.parse.app_error", nil, "", http.StatusBadRequest)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("uploadBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.SaveBrandImage(imageArray[0]); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
ReturnStatusOK(w)
}
func deleteBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.DeleteBrandImage(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitChannel() {
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(getAllChannels)).Methods("GET")
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(createChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/direct", api.APISessionRequired(createDirectChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchAllChannels)).Methods("POST")
api.BaseRoutes.Channels.Handle("/group/search", api.APISessionRequiredDisableWhenBusy(searchGroupChannels)).Methods("POST")
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
api.BaseRoutes.ChannelsForTeam.Handle("", api.APISessionRequired(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APISessionRequired(getDeletedChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APISessionRequired(getPrivateChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.APISessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search_archived", api.APISessionRequiredDisableWhenBusy(searchArchivedChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.APISessionRequired(autocompleteChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.APISessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET")
api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForTeamForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/channels", api.APISessionRequired(getChannelsForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(getCategoriesForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(createCategoryForTeamForUser)).Methods("POST")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(updateCategoriesForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/order", api.APISessionRequired(getCategoryOrderForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("/order", api.APISessionRequired(updateCategoryOrderForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(getCategoryForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(updateCategoryForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(deleteCategoryForTeamForUser)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(getChannel)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(updateChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/patch", api.APISessionRequired(patchChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/privacy", api.APISessionRequired(updateChannelPrivacy)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/restore", api.APISessionRequired(restoreChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(deleteChannel)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("/stats", api.APISessionRequired(getChannelStats)).Methods("GET")
api.BaseRoutes.Channel.Handle("/pinned", api.APISessionRequired(getPinnedPosts)).Methods("GET")
api.BaseRoutes.Channel.Handle("/timezones", api.APISessionRequired(getChannelMembersTimezones)).Methods("GET")
api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.APISessionRequired(channelMembersMinusGroupMembers)).Methods("GET")
api.BaseRoutes.Channel.Handle("/move", api.APISessionRequired(moveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/member_counts_by_group", api.APISessionRequired(channelMemberCountsByGroup)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/unread", api.APISessionRequired(getChannelUnread)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APISessionRequired(getChannelByName)).Methods("GET")
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APISessionRequired(getChannelByNameForTeamName)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("", api.APISessionRequired(getChannelMembers)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("/ids", api.APISessionRequired(getChannelMembersByIds)).Methods("POST")
api.BaseRoutes.ChannelMembers.Handle("", api.APISessionRequired(addChannelMember)).Methods("POST")
api.BaseRoutes.ChannelMembersForUser.Handle("", api.APISessionRequired(getChannelMembersForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelMember.Handle("", api.APISessionRequired(getChannelMember)).Methods("GET")
api.BaseRoutes.ChannelMember.Handle("", api.APISessionRequired(removeChannelMember)).Methods("DELETE")
api.BaseRoutes.ChannelMember.Handle("/roles", api.APISessionRequired(updateChannelMemberRoles)).Methods("PUT")
api.BaseRoutes.ChannelMember.Handle("/schemeRoles", api.APISessionRequired(updateChannelMemberSchemeRoles)).Methods("PUT")
api.BaseRoutes.ChannelMember.Handle("/notify_props", api.APISessionRequired(updateChannelMemberNotifyProps)).Methods("PUT")
api.BaseRoutes.ChannelModerations.Handle("", api.APISessionRequired(getChannelModerations)).Methods("GET")
api.BaseRoutes.ChannelModerations.Handle("/patch", api.APISessionRequired(patchChannelModerations)).Methods("PUT")
}
func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
auditRec := c.MakeAuditRecord("createChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePublicChannel) {
c.SetPermissionError(model.PermissionCreatePublicChannel)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePrivateChannel) {
c.SetPermissionError(model.PermissionCreatePrivateChannel)
return
}
sc, appErr := c.App.CreateChannelWithUser(c.AppContext, channel, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
// The channel being updated in the payload must be the same one as indicated in the URL.
if channel.Id != c.Params.ChannelId {
c.SetInvalidParam("channel_id")
return
}
auditRec := c.MakeAuditRecord("updateChannel", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
defer c.LogAuditRec(auditRec)
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, channel.Id)
if appErr != nil {
c.Err = appErr
return
}
oldChannel := originalOldChannel.DeepCopy()
auditRec.AddEventPriorState(oldChannel)
switch oldChannel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if oldChannel.DeleteAt > 0 {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.deleted.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.Type != "" && channel.Type != oldChannel.Type {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.typechange.app_error", nil, "", http.StatusBadRequest)
return
}
if oldChannel.Name == model.DefaultChannelName {
if channel.Name != "" && channel.Name != oldChannel.Name {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.tried.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return
}
}
oldChannel.Header = channel.Header
oldChannel.Purpose = channel.Purpose
oldChannelDisplayName := oldChannel.DisplayName
if channel.DisplayName != "" {
oldChannel.DisplayName = channel.DisplayName
}
if channel.Name != "" {
oldChannel.Name = channel.Name
audit.AddEventParameter(auditRec, "new_channel_name", oldChannel.Name)
}
if channel.GroupConstrained != nil {
oldChannel.GroupConstrained = channel.GroupConstrained
}
updatedChannel, appErr := c.App.UpdateChannel(c.AppContext, oldChannel)
if appErr != nil {
c.Err = appErr
return
}
if oldChannelDisplayName != channel.DisplayName {
if err := c.App.PostUpdateChannelDisplayNameMessage(c.AppContext, c.AppContext.Session().UserId, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
c.Logger.Warn("Error while posting channel display name message", mlog.Err(err))
}
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(oldChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateChannelPrivacy", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
c.SetInvalidParam("privacy")
return
}
audit.AddEventParameter(auditRec, "privacy", privacy)
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(channel)
if model.ChannelType(privacy) == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPrivateChannelToPublic) {
c.SetPermissionError(model.PermissionConvertPrivateChannelToPublic)
return
}
if model.ChannelType(privacy) == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPublicChannelToPrivate) {
c.SetPermissionError(model.PermissionConvertPublicChannelToPrivate)
return
}
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
channel.Type = model.ChannelType(privacy)
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, user)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + updatedChannel.Name)
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
oldChannel := originalOldChannel.DeepCopy()
auditRec := c.MakeAuditRecord("patchChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", patch)
auditRec.AddEventPriorState(oldChannel)
switch oldChannel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
if _, appErr = c.App.GetChannelMember(c.AppContext, c.Params.ChannelId, c.AppContext.Session().UserId); appErr != nil {
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if oldChannel.Name == model.DefaultChannelName {
if patch.Name != nil && *patch.Name != oldChannel.Name {
c.Err = model.NewAppError("patchChannel", "api.channel.update_channel.tried.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return
}
}
rchannel, appErr := c.App.PatchChannel(c.AppContext, oldChannel, patch, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(rchannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
teamId := channel.TeamId
auditRec := c.MakeAuditRecord("restoreChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddEventPriorState(channel)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
channel, err = c.App.RestoreChannel(c.AppContext, channel, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
allowed := false
if len(userIds) != 2 {
c.SetInvalidParam("user_ids")
return
}
for _, id := range userIds {
if !model.IsValidId(id) {
c.SetInvalidParam("user_id")
return
}
if id == c.AppContext.Session().UserId {
allowed = true
}
}
auditRec := c.MakeAuditRecord("createDirectChannel", audit.Fail)
audit.AddEventParameter(auditRec, "user_ids", userIds)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateDirectChannel) {
c.SetPermissionError(model.PermissionCreateDirectChannel)
return
}
if !allowed && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
otherUserId := userIds[0]
if c.AppContext.Session().UserId == otherUserId {
otherUserId = userIds[1]
}
audit.AddEventParameter(auditRec, "user_id", otherUserId)
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, otherUserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
sc, err := c.App.GetOrCreateDirectChannel(c.AppContext, userIds[0], userIds[1])
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) {
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
groupChannels, appErr := c.App.SearchGroupChannels(c.AppContext, c.AppContext.Session().UserId, props.Term)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(groupChannels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
found := false
for _, id := range userIds {
if !model.IsValidId(id) {
c.SetInvalidParam("user_id")
return
}
if id == c.AppContext.Session().UserId {
found = true
}
}
if !found {
userIds = append(userIds, c.AppContext.Session().UserId)
}
auditRec := c.MakeAuditRecord("createGroupChannel", audit.Fail)
audit.AddEventParameter(auditRec, "user_ids", userIds)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateGroupChannel) {
c.SetPermissionError(model.PermissionCreateGroupChannel)
return
}
canSeeAll := true
for _, id := range userIds {
if c.AppContext.Session().UserId != id {
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
if err != nil {
c.Err = err
return
}
if !canSee {
canSeeAll = false
}
}
}
if !canSeeAll {
c.SetPermissionError(model.PermissionViewMembers)
return
}
groupChannel, err := c.App.CreateGroupChannel(c.AppContext, userIds, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(groupChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(groupChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type == model.ChannelTypeOpen {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
err = c.App.FillInChannelProps(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
channelUnread, err := c.App.GetChannelUnread(c.AppContext, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channelUnread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
excludeFilesCount := r.URL.Query().Get("exclude_files_count")
excludeFilesCountBool, _ := strconv.ParseBool(excludeFilesCount)
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
memberCount, err := c.App.GetChannelMemberCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
guestCount, err := c.App.GetChannelGuestCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
pinnedPostCount, err := c.App.GetChannelPinnedPostCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
filesCount := int64(-1)
if !excludeFilesCountBool {
filesCount, err = c.App.GetChannelFileCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
}
stats := model.ChannelStats{
ChannelId: c.Params.ChannelId,
MemberCount: memberCount,
GuestCount: guestCount,
PinnedPostCount: pinnedPostCount,
FilesCount: filesCount,
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
posts, err := c.App.GetPinnedPosts(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
if c.HandleEtag(posts.Etag(), "Get Pinned Posts", w, r) {
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, posts)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, clientPostList.Etag())
if err := clientPostList.EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
permissions := []*model.Permission{
model.PermissionSysconsoleReadUserManagementGroups,
model.PermissionSysconsoleReadUserManagementChannels,
}
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), permissions) {
c.SetPermissionError(permissions...)
return
}
// Only system managers may use the ExcludePolicyConstrained parameter
if c.Params.ExcludePolicyConstrained && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: c.Params.NotAssociatedToGroup,
ExcludeDefaultChannels: c.Params.ExcludeDefaultChannels,
IncludeDeleted: c.Params.IncludeDeleted,
ExcludePolicyConstrained: c.Params.ExcludePolicyConstrained,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
}
channels, err := c.App.GetAllChannels(c.AppContext, c.Params.Page, c.Params.PerPage, opts)
if err != nil {
c.Err = err
return
}
if c.Params.IncludeTotalCount {
totalCount, err := c.App.GetAllChannelsCount(c.AppContext, opts)
if err != nil {
c.Err = err
return
}
cwc := &model.ChannelsWithCount{
Channels: channels,
TotalCount: totalCount,
}
if err := json.NewEncoder(w).Encode(cwc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
c.SetPermissionError(model.PermissionListTeamChannels)
return
}
channels, err := c.App.GetPublicChannelsForTeam(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getDeletedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
channels, err := c.App.GetDeletedChannels(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPrivateChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
channels, err := c.App.GetPrivateChannelsForTeam(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
channelIds := model.ArrayFromJSON(r.Body)
if len(channelIds) == 0 {
c.SetInvalidParam("channel_ids")
return
}
for _, cid := range channelIds {
if !model.IsValidId(cid) {
c.SetInvalidParam("channel_id")
return
}
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
channels, err := c.App.GetPublicChannelsByIdsForTeam(c.AppContext, c.Params.TeamId, channelIds)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelsForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
query := r.URL.Query()
lastDeleteAt, nErr := strconv.Atoi(query.Get("last_delete_at"))
if nErr != nil {
lastDeleteAt = 0
}
if lastDeleteAt < 0 {
c.SetInvalidURLParam("last_delete_at")
return
}
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, c.Params.TeamId, c.Params.UserId, &model.ChannelSearchOpts{
IncludeDeleted: c.Params.IncludeDeleted,
LastDeleteAt: lastDeleteAt,
})
if err != nil {
c.Err = err
return
}
if c.HandleEtag(channels.Etag(), "Get Channels", w, r) {
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, channels.Etag())
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
query := r.URL.Query()
lastDeleteAt, nErr := strconv.Atoi(query.Get("last_delete_at"))
if nErr != nil {
lastDeleteAt = 0
}
if lastDeleteAt < 0 {
c.SetInvalidURLParam("last_delete_at")
return
}
pageSize := 100
fromChannelID := ""
// We have to write `[` and `]` separately because we want to stream the response.
// The internal API is paginated, but the client always needs to get the full data.
// Therefore, to avoid forcing the client to go through all the pages,
// we stream the full data from server side itself.
//
// Note that this means if an error occurs in mid-stream, the response won't be
// fully JSON.
w.Write([]byte(`[`))
enc := json.NewEncoder(w)
for {
channels, err := c.App.GetChannelsForUser(c.AppContext, c.Params.UserId, c.Params.IncludeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
// If the page size was a perfect multiple of the total number of results,
// then the last query will always return zero results.
if fromChannelID != "" && err.Id == "app.channel.get_channels.not_found.app_error" {
break
}
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
// intermediary comma between sets
if fromChannelID != "" {
w.Write([]byte(`,`))
}
for i, ch := range channels {
if err := enc.Encode(ch); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
if i < len(channels)-1 {
w.Write([]byte(`,`))
}
}
if len(channels) < pageSize {
break
}
fromChannelID = channels[len(channels)-1].Id
}
w.Write([]byte(`]`))
}
func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
c.SetPermissionError(model.PermissionListTeamChannels)
return
}
name := r.URL.Query().Get("name")
channels, err := c.App.AutocompleteChannelsForTeam(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, name)
if err != nil {
c.Err = err
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteChannelsForTeamForSearch(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
name := r.URL.Query().Get("name")
channels, err := c.App.AutocompleteChannelsForSearch(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, name)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
var channels model.ChannelList
var appErr *model.AppError
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
channels, appErr = c.App.SearchChannels(c.AppContext, c.Params.TeamId, props.Term)
} else {
// If the user is not a team member, return a 404
if _, appErr = c.App.GetTeamMember(c.Params.TeamId, c.AppContext.Session().UserId); appErr != nil {
c.Err = appErr
return
}
channels, appErr = c.App.SearchChannelsForUser(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, props.Term)
}
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchArchivedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
var channels model.ChannelList
var appErr *model.AppError
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
channels, appErr = c.App.SearchArchivedChannels(c.AppContext, c.Params.TeamId, props.Term, c.AppContext.Session().UserId)
} else {
// If the user is not a team member, return a 404
if _, appErr = c.App.GetTeamMember(c.Params.TeamId, c.AppContext.Session().UserId); appErr != nil {
c.Err = appErr
return
}
channels, appErr = c.App.SearchArchivedChannels(c.AppContext, c.Params.TeamId, props.Term, c.AppContext.Session().UserId)
}
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
fromSysConsole := true
if val := r.URL.Query().Get("system_console"); val != "" {
fromSysConsole, err = strconv.ParseBool(val)
if err != nil {
c.SetInvalidParam("system_console")
return
}
}
if !fromSysConsole {
// If the request is not coming from system_console, only show the user level channels
// from all teams.
channels, err := c.App.AutocompleteChannels(c.AppContext, c.AppContext.Session().UserId, props.Term)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
// Only system managers may use the ExcludePolicyConstrained field
if props.ExcludePolicyConstrained && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
includeDeleted = includeDeleted || props.IncludeDeleted
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: props.NotAssociatedToGroup,
ExcludeDefaultChannels: props.ExcludeDefaultChannels,
TeamIds: props.TeamIds,
GroupConstrained: props.GroupConstrained,
ExcludeGroupConstrained: props.ExcludeGroupConstrained,
ExcludePolicyConstrained: props.ExcludePolicyConstrained,
IncludeSearchById: props.IncludeSearchById,
Public: props.Public,
Private: props.Private,
IncludeDeleted: includeDeleted,
Deleted: props.Deleted,
Page: props.Page,
PerPage: props.PerPage,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
}
channels, totalCount, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if props.Page != nil && props.PerPage != nil {
data := model.ChannelsWithCount{Channels: channels, TotalCount: totalCount}
if err := json.NewEncoder(w).Encode(data); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteChannel", audit.Fail)
audit.AddEventParameter(auditRec, "id", c.Params.ChannelId)
auditRec.AddEventPriorState(channel)
defer c.LogAuditRec(auditRec)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("deleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePublicChannel) {
c.SetPermissionError(model.PermissionDeletePublicChannel)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePrivateChannel) {
c.SetPermissionError(model.PermissionDeletePrivateChannel)
return
}
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPIChannelDeletion {
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
} else {
err = model.NewAppError("deleteChannel", "api.user.delete_channel.not_enabled.app_error", nil, "channelId="+c.Params.ChannelId, http.StatusUnauthorized)
}
} else {
err = c.App.DeleteChannel(c.AppContext, channel, c.AppContext.Session().UserId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name)
ReturnStatusOK(w)
}
func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireChannelName()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
channel, appErr := c.App.GetChannelByName(c.AppContext, c.Params.ChannelName, c.Params.TeamId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if channel.Type == model.ChannelTypeOpen {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.Err = model.NewAppError("getChannelByName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
return
}
}
appErr = c.App.FillInChannelProps(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelByNameForTeamName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName().RequireChannelName()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
channel, appErr := c.App.GetChannelByNameForTeamName(c.AppContext, c.Params.ChannelName, c.Params.TeamName, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
teamOk := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)
channelOk := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel)
if channel.Type == model.ChannelTypeOpen {
if !teamOk && !channelOk {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else if !channelOk {
c.Err = model.NewAppError("getChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
return
}
appErr = c.App.FillInChannelProps(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
members, err := c.App.GetChannelMembersPage(c.AppContext, c.Params.ChannelId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembersTimezones(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
membersTimezones, err := c.App.GetChannelMembersTimezones(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.ArrayToJSON(membersTimezones)))
}
func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
members, err := c.App.GetChannelMembersByIds(c.AppContext, c.Params.ChannelId, userIds)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, err := c.App.GetChannelMember(ctx, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(member); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembersForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
members, err := c.App.GetChannelMembersForUser(c.AppContext, c.Params.TeamId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var view model.ChannelView
if jsonErr := json.NewDecoder(r.Body).Decode(&view); jsonErr != nil {
c.SetInvalidParamWithErr("channel_view", jsonErr)
return
}
// Validate view struct
// Check IDs are valid or blank. Blank IDs are used to denote focus loss or initial channel view.
if view.ChannelId != "" && !model.IsValidId(view.ChannelId) {
c.SetInvalidParam("channel_view.channel_id")
return
}
if view.PrevChannelId != "" && !model.IsValidId(view.PrevChannelId) {
c.SetInvalidParam("channel_view.prev_channel_id")
return
}
times, err := c.App.ViewChannel(c.AppContext, &view, c.Params.UserId, c.AppContext.Session().Id, view.CollapsedThreadsSupported)
if err != nil {
c.Err = err
return
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
c.ExtendSessionExpiryIfNeeded(w, r)
// Returning {"status": "OK", ...} for backwards compatibility
resp := &model.ChannelViewResponse{
Status: "OK",
LastViewedAtTimes: times,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !(model.IsValidUserRoles(newRoles)) {
c.SetInvalidParam("roles")
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "props", props)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
c.SetPermissionError(model.PermissionManageChannelRoles)
return
}
if _, err := c.App.UpdateChannelMemberRoles(c.AppContext, c.Params.ChannelId, c.Params.UserId, newRoles); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateChannelMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
var schemeRoles model.SchemeRoles
if jsonErr := json.NewDecoder(r.Body).Decode(&schemeRoles); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_roles", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberSchemeRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameterAuditable(auditRec, "roles", &schemeRoles)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
c.SetPermissionError(model.PermissionManageChannelRoles)
return
}
if _, err := c.App.UpdateChannelMemberSchemeRoles(c.AppContext, c.Params.ChannelId, c.Params.UserId, schemeRoles.SchemeGuest, schemeRoles.SchemeUser, schemeRoles.SchemeAdmin); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateChannelMemberNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
if props == nil {
c.SetInvalidParam("notify_props")
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberNotifyProps", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "props", props)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
_, err := c.App.UpdateChannelMemberNotifyProps(c.AppContext, props, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
userId, ok := props["user_id"].(string)
if !ok || !model.IsValidId(userId) {
c.SetInvalidParam("user_id")
return
}
auditRec := c.MakeAuditRecord("addChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", userId)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
member := &model.ChannelMember{
ChannelId: c.Params.ChannelId,
UserId: userId,
}
postRootId, ok := props["post_root_id"].(string)
if ok && postRootId != "" && !model.IsValidId(postRootId) {
c.SetInvalidParam("post_root_id")
return
}
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
if ok && len(postRootId) == 26 {
rootPost, err := c.App.GetSinglePost(postRootId, false)
if err != nil {
c.Err = err
return
}
if rootPost.ChannelId != member.ChannelId {
c.SetInvalidParam("post_root_id")
return
}
}
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
return
}
isNewMembership := false
if _, err = c.App.GetChannelMember(c.AppContext, member.ChannelId, member.UserId); err != nil {
if err.Id == app.MissingChannelMemberError {
isNewMembership = true
} else {
c.Err = err
return
}
}
isSelfAdd := member.UserId == c.AppContext.Session().UserId
if channel.Type == model.ChannelTypeOpen {
if isSelfAdd && isNewMembership {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionJoinPublicChannels) {
c.SetPermissionError(model.PermissionJoinPublicChannels)
return
}
} else if isSelfAdd && !isNewMembership {
// nothing to do, since already in the channel
} else if !isSelfAdd {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
return
}
}
}
if channel.Type == model.ChannelTypePrivate {
if isSelfAdd && isNewMembership {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
} else if isSelfAdd && !isNewMembership {
// nothing to do, since already in the channel
} else if !isSelfAdd {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
}
}
if channel.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
UserRequestorID: c.AppContext.Session().UserId,
PostRootID: postRootId,
})
if err != nil {
c.Err = err
return
}
if postRootId != "" {
err := c.App.UpdateThreadFollowForUserFromChannelAdd(c.AppContext, cm.UserId, channel.TeamId, postRootId)
if err != nil {
c.Err = err
return
}
}
auditRec.Success()
auditRec.AddEventResultState(cm)
auditRec.AddEventObjectType("channel_member")
auditRec.AddMeta("add_user_id", cm.UserId)
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(cm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() && (c.Params.UserId != c.AppContext.Session().UserId) && !user.IsBot {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
if c.Params.UserId != c.AppContext.Session().UserId {
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
}
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, c.AppContext.Session().UserId, channel); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
ReturnStatusOK(w)
}
func updateChannelScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateChannelScheme", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
var p model.SchemeIDPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil || p.SchemeID == nil || !model.IsValidId(*p.SchemeID) {
c.SetInvalidParamWithErr("scheme_id", jsonErr)
return
}
schemeID := p.SchemeID
audit.AddEventParameter(auditRec, "scheme_id", *schemeID)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.license.error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
scheme, err := c.App.GetScheme(*schemeID)
if err != nil {
c.Err = err
return
}
if scheme.Scope != model.SchemeScopeChannel {
c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(channel)
channel.SchemeId = &scheme.Id
updatedChannel, err := c.App.UpdateChannelScheme(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
ReturnStatusOK(w)
}
func channelMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
groupIDsParam := groupIDsQueryParamRegex.ReplaceAllString(c.Params.GroupIDs, "")
if len(groupIDsParam) < 26 {
c.SetInvalidParam("group_ids")
return
}
groupIDs := []string{}
for _, gid := range strings.Split(c.Params.GroupIDs, ",") {
if !model.IsValidId(gid) {
c.SetInvalidParam("group_ids")
return
}
groupIDs = append(groupIDs, gid)
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
users, totalCount, appErr := c.App.ChannelMembersMinusGroupMembers(
c.Params.ChannelId,
groupIDs,
c.Params.Page,
c.Params.PerPage,
)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(&model.UsersWithGroupsAndCount{
Users: users,
Count: totalCount,
})
if err != nil {
c.Err = model.NewAppError("Api4.channelMembersMinusGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func channelMemberCountsByGroup(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.channelMemberCountsByGroup", "api.channel.channel_member_counts_by_group.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
channelMemberCounts, appErr := c.App.GetMemberCountsByGroup(app.WithMaster(context.Background()), c.Params.ChannelId, includeTimezones)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channelMemberCounts)
if err != nil {
c.Err = model.NewAppError("Api4.channelMemberCountsByGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.GetChannelModerations", "api.channel.get_channel_moderations.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
channelModerations, appErr := c.App.GetChannelModerationsForChannel(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channelModerations)
if err != nil {
c.Err = model.NewAppError("Api4.getChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func patchChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.channel.patch_channel_moderations.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("patchChannelModerations", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementChannels)
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "channel", channel)
var channelModerationsPatch []*model.ChannelModerationPatch
err := json.NewDecoder(r.Body).Decode(&channelModerationsPatch)
if err != nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
channelModerations, appErr := c.App.PatchChannelModerationsForChannel(c.AppContext, channel, channelModerationsPatch)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditableArray(auditRec, "channel_moderations_patch", channelModerationsPatch)
b, err := json.Marshal(channelModerations)
if err != nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func moveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
props := model.StringInterfaceFromJSON(r.Body)
teamId, ok := props["team_id"].(string)
if !ok {
c.SetInvalidParam("team_id")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
team, err := c.App.GetTeam(teamId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("moveChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "team_id", teamId)
audit.AddEventParameter(auditRec, "force", force)
auditRec.AddEventPriorState(channel)
// TODO check and verify if the below three things are parameters or prior state if any
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", team.Id)
auditRec.AddMeta("team_name", team.Name)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if force {
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, user, channel, team)
if err != nil {
c.Err = err
return
}
}
err = c.App.MoveChannel(c.AppContext, team, channel, user)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("channel=" + channel.Name)
c.LogAudit("team=" + team.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryCreateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryCreateRequest)
if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryCreateRequest); appErr != nil {
c.Err = appErr
return
}
category, appErr := c.App.CreateSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, &categoryCreateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(category)
if err != nil {
c.Err = model.NewAppError("createCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
order, appErr := c.App.GetSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(order)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
categoryOrder := model.ArrayFromJSON(r.Body)
for _, categoryId := range categoryOrder {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
c.SetInvalidParam("category")
return
}
}
err := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.Write([]byte(model.ArrayToJSON(categoryOrder)))
}
func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategory(c.AppContext, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoriesUpdateRequest []*model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoriesUpdateRequest)
if err != nil {
c.SetInvalidParamWithErr("category", err)
return
}
for _, category := range categoriesUpdateRequest {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, category.Id) {
c.SetInvalidParam("category")
return
}
}
if appErr := validateSidebarCategories(c, c.Params.TeamId, c.Params.UserId, categoriesUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("updateCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoriesJSON)
}
func validateSidebarCategory(c *Context, teamId, userId string, category *model.SidebarCategoryWithChannels) *model.AppError {
channels, appErr := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if appErr != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, "", http.StatusBadRequest).Wrap(appErr)
}
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
return nil
}
func validateSidebarCategories(c *Context, teamId, userId string, categories []*model.SidebarCategoryWithChannels) *model.AppError {
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if err != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest)
}
for _, category := range categories {
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
}
return nil
}
func validateSidebarCategoryChannels(c *Context, userId string, channelIds []string, channels model.ChannelList) []string {
var filtered []string
for _, channelId := range channelIds {
found := false
for _, channel := range channels {
if channel.Id == channelId {
found = true
break
}
}
if found {
filtered = append(filtered, channelId)
} else {
c.Logger.Info("Stopping user from adding channel to their sidebar when they are not a member", mlog.String("user_id", userId), mlog.String("channel_id", channelId))
}
}
return filtered
}
func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryUpdateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryUpdateRequest)
if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categoryUpdateRequest.Id = c.Params.CategoryId
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{&categoryUpdateRequest})
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(categories[0])
if err != nil {
c.Err = model.NewAppError("updateCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
appErr := c.App.DeleteSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitChannelLocal() {
api.BaseRoutes.Channels.Handle("", api.APILocal(getAllChannels)).Methods("GET")
api.BaseRoutes.Channels.Handle("", api.APILocal(localCreateChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("", api.APILocal(getChannel)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.APILocal(localDeleteChannel)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("/patch", api.APILocal(localPatchChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/move", api.APILocal(localMoveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/privacy", api.APILocal(localUpdateChannelPrivacy)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/restore", api.APILocal(localRestoreChannel)).Methods("POST")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(localRemoveChannelMember)).Methods("DELETE")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(getChannelMember)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(localAddChannelMember)).Methods("POST")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(getChannelMembers)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("", api.APILocal(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APILocal(getDeletedChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APILocal(getPrivateChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APILocal(getChannelByNameForTeamName)).Methods("GET")
}
func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
auditRec := c.MakeAuditRecord("localCreateChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
sc, appErr := c.App.CreateChannel(c.AppContext, channel, false)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
c.SetInvalidParam("privacy")
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localUpdateChannelPrivacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "privacy", privacy)
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
return
}
channel.Type = model.ChannelType(privacy)
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + updatedChannel.Name)
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRestoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localRestoreChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
channel, err = c.App.RestoreChannel(c.AppContext, channel, "")
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("localAddChannelMember", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
props := model.StringInterfaceFromJSON(r.Body)
userId, ok := props["user_id"].(string)
if !ok || !model.IsValidId(userId) {
c.SetInvalidParam("user_id")
return
}
audit.AddEventParameter(auditRec, "user_id", userId)
member := &model.ChannelMember{
ChannelId: c.Params.ChannelId,
UserId: userId,
}
postRootId, ok := props["post_root_id"].(string)
if ok && postRootId != "" && !model.IsValidId(postRootId) {
c.SetInvalidParam("post_root_id")
return
}
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
if ok && len(postRootId) == 26 {
rootPost, err := c.App.GetSinglePost(postRootId, false)
if err != nil {
c.Err = err
return
}
if rootPost.ChannelId != member.ChannelId {
c.SetInvalidParam("post_root_id")
return
}
}
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "channel", channel)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
PostRootID: postRootId,
})
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("add_user_id", cm.UserId)
auditRec.AddEventResultState(cm)
auditRec.AddEventObjectType("channel_member")
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(cm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() && !user.IsBot {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("localRemoveChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "remove_user_id", c.Params.UserId)
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, "", channel); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
ReturnStatusOK(w)
}
func localPatchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
channel := originalOldChannel.DeepCopy()
auditRec := c.MakeAuditRecord("localPatchChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel_patch", patch)
channel.Patch(patch)
rchannel, appErr := c.App.UpdateChannel(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("")
auditRec.AddEventResultState(rchannel)
auditRec.AddEventObjectType("channel")
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMoveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
props := model.StringInterfaceFromJSON(r.Body)
teamId, ok := props["team_id"].(string)
if !ok {
c.SetInvalidParam("team_id")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
team, err := c.App.GetTeam(teamId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localMoveChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", teamId)
audit.AddEventParameter(auditRec, "force", force)
// TODO do we need these?
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", team.Id)
auditRec.AddMeta("team_name", team.Name)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
return
}
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if force {
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, nil, channel, team)
if err != nil {
c.Err = err
return
}
}
err = c.App.MoveChannel(c.AppContext, team, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("channel=" + channel.Name)
c.LogAudit("team=" + team.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localDeleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localDeleteChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddEventPriorState(channel)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localDeleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if c.Params.Permanent {
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
} else {
err = c.App.DeleteChannel(c.AppContext, channel, "")
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitCloud() {
// GET /api/v4/cloud/products
api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods("GET")
// GET /api/v4/cloud/limits
api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/products/selfhosted", api.APISessionRequired(getSelfHostedProducts)).Methods("GET")
// POST /api/v4/cloud/payment
// POST /api/v4/cloud/payment/confirm
api.BaseRoutes.Cloud.Handle("/payment", api.APISessionRequired(createCustomerPayment)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/payment/confirm", api.APISessionRequired(confirmCustomerPayment)).Methods("POST")
// GET /api/v4/cloud/customer
// PUT /api/v4/cloud/customer
// PUT /api/v4/cloud/customer/address
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(getCloudCustomer)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(updateCloudCustomer)).Methods("PUT")
api.BaseRoutes.Cloud.Handle("/customer/address", api.APISessionRequired(updateCloudCustomerAddress)).Methods("PUT")
// GET /api/v4/cloud/subscription
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
// GET /api/v4/cloud/request-trial
api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT")
// GET /api/v4/cloud/validate-business-email
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/validate-workspace-business-email", api.APISessionRequired(validateWorkspaceBusinessEmail)).Methods("POST")
// POST /api/v4/cloud/webhook
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods(http.MethodDelete)
}
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
subscription, err := c.App.Cloud().GetSubscription(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// if it is an end user, return basic subscription data without sensitive information
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
subscription = &model.Subscription{
ID: subscription.ID,
ProductID: subscription.ProductID,
IsFreeTrial: subscription.IsFreeTrial,
TrialEndAt: subscription.TrialEndAt,
CustomerID: "",
AddOns: []string{},
StartAt: 0,
EndAt: 0,
CreateAt: 0,
Seats: 0,
Status: "",
DNS: "",
LastInvoice: &model.Invoice{},
DelinquentSince: subscription.DelinquentSince,
}
}
json, err := json.Marshal(subscription)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.license_error", nil, "", http.StatusInternalServerError)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var subscriptionChange *model.SubscriptionChange
if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
currentSubscription, appErr := c.App.Cloud().GetSubscription(userId)
if appErr != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
changedSub, err := c.App.Cloud().ChangeSubscription(userId, currentSubscription.ID, subscriptionChange)
if err != nil {
appErr := model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
if err.Error() == "compliance-failed" {
c.Logger.Error("Compliance check failed", mlog.Err(err))
appErr.StatusCode = http.StatusUnprocessableEntity
}
c.Err = appErr
return
}
if subscriptionChange.Feedback != nil {
c.App.Srv().GetTelemetryService().SendTelemetry("downgrade_feedback", subscriptionChange.Feedback.ToMap())
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID)
if err != nil || product == nil {
c.Logger.Error("Error finding the new cloud product", mlog.Err(err))
}
if product.SKU == string(model.SkuCloudStarter) {
w.Write(json)
return
}
isYearly := product.IsYearly()
// Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them
// At this point, the upgrade is complete.
if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil {
c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr))
}
w.Write(json)
}
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
// check if the email needs to be set
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
// a new business email was provided via the request business email modal
var startTrialRequest *model.StartCloudTrialRequest
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, startTrialRequest.SubscriptionID, startTrialRequest.Email)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
defer c.App.Srv().Cloud.InvalidateCaches()
w.Write(json)
}
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var emailToValidate *model.ValidateBusinessEmailRequest
err = json.Unmarshal(bodyBytes, &emailToValidate)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = c.App.Cloud().ValidateBusinessEmail(user.Id, emailToValidate.Email)
if err != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(err)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
// get the cloud customer email to validate if is a valid business email
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
return
}
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)
// if the current workspace email is not a valid business email
if emailErr != nil {
// grab the current admin email and validate it
errValidatingAdminEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, user.Email)
if errValidatingAdminEmail != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, errValidatingAdminEmail.Error(), http.StatusForbidden)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
return
}
}
// if any of the emails is valid, return ok
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
products, err := c.App.Cloud().GetSelfHostedProducts(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
includeLegacyProducts := r.URL.Query().Get("include_legacy") == "true"
products, err := c.App.Cloud().GetCloudProducts(c.AppContext.Session().UserId, includeLegacyProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(limits)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
customer, err := c.App.Cloud().GetCloudCustomer(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
_, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if cloudErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
return
}
json, jsonErr := json.Marshal(status)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
w.Write(json)
}
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var customerInfo *model.CloudCustomerInfo
if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomer(c.AppContext.Session().UserId, customerInfo)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var address *model.Address
if err = json.Unmarshal(bodyBytes, &address); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomerAddress(c.AppContext.Session().UserId, address)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("createCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
intent, err := c.App.Cloud().CreateCustomerPayment(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(intent)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(json)
}
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("confirmCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var confirmRequest *model.ConfirmPaymentMethodRequest
if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
err = c.App.Cloud().ConfirmCustomerPayment(c.AppContext.Session().UserId, confirmRequest)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
invoices, appErr := c.App.Cloud().GetInvoicesForSubscription(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireInvoiceId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
pdfData, filename, appErr := c.App.Cloud().GetInvoicePDF(c.AppContext.Session().UserId, c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}
func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
defer r.Body.Close()
var event *model.CWSWebhookPayload
if err = json.Unmarshal(bodyBytes, &event); err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
switch event.Event {
case model.EventTypeFailedPayment:
if nErr := c.App.SendPaymentFailedEmail(event.FailedPayment); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeFailedPaymentNoCard:
if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendUpgradeConfirmationEmail:
// isYearly determines whether to send the yearly or monthly Upgrade email
isYearly := false
if event.Subscription != nil && event.CloudWorkspaceOwner != nil {
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
// Get the current cloud product to determine whether it's a monthly or yearly product
product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
isYearly = product.IsYearly()
}
if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendAdminWelcomeEmail:
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
teams, appErr := c.App.GetAllTeams()
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
team := teams[0]
subscription, err := c.App.Cloud().GetSubscription(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
return
}
case model.EventTypeTriggerDelinquencyEmail:
var emailToTrigger model.DelinquencyEmail
if event.DelinquencyEmail != nil {
emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
} else {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
return
}
if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
c.Err = nErr
return
}
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
return
}
ReturnStatusOK(w)
}
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
if err := cloud.CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
var deleteRequest *model.WorkspaceDeletionRequest
if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Cloud().SelfServeDeleteWorkspace(c.AppContext.Session().UserId, deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.server.cws.delete_workspace.app_error", nil, "CWS Server failed to delete workspace.", http.StatusInternalServerError)
return
}
c.App.Srv().GetTelemetryService().SendTelemetry("delete_workspace_feedback", deleteRequest.Feedback.ToMap())
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitCluster() {
api.BaseRoutes.Cluster.Handle("/status", api.APISessionRequired(getClusterStatus)).Methods("GET")
}
func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadEnvironmentHighAvailability) {
c.SetPermissionError(model.PermissionSysconsoleReadEnvironmentHighAvailability)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("getClusterStatus", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
infos := c.App.GetClusterStatus()
js, err := json.Marshal(infos)
if err != nil {
c.Err = model.NewAppError("getClusterStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommand() {
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(createCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(listCommands)).Methods("GET")
api.BaseRoutes.Commands.Handle("/execute", api.APISessionRequired(executeCommand)).Methods("POST")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APISessionRequired(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(deleteCommand)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.APISessionRequired(listAutocompleteCommands)).Methods("GET")
api.BaseRoutes.Team.Handle("/commands/autocomplete_suggestions", api.APISessionRequired(listCommandAutocompleteSuggestions)).Methods("GET")
api.BaseRoutes.Command.Handle("/regen_token", api.APISessionRequired(regenCommandToken)).Methods("PUT")
}
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd.CreatorId = c.AppContext.Session().UserId
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil || cmd.Id != c.Params.CommandId {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
oldCmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(oldCmd)
if cmd.TeamId != oldCmd.TeamId {
c.Err = model.NewAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != oldCmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.UpdateCommand(oldCmd, &cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmr model.CommandMoveRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&cmr); jsonErr != nil {
c.SetInvalidParamWithErr("team_id", jsonErr)
return
}
auditRec := c.MakeAuditRecord("moveCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_move_request", cmr.TeamId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
newTeam, appErr := c.App.GetTeam(cmr.TeamId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "team", newTeam)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd, appErr := c.App.GetCommand(c.Params.CommandId)
if appErr != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if appErr = c.App.MoveCommand(newTeam, cmd); appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(cmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
err = c.App.DeleteCommand(cmd.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
customOnly, _ := strconv.ParseBool(r.URL.Query().Get("custom_only"))
teamId := r.URL.Query().Get("team_id")
if teamId == "" {
c.SetInvalidParam("team_id")
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
var commands []*model.Command
var err *model.AppError
if customOnly {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
commands, err = c.App.ListTeamCommands(teamId)
if err != nil {
c.Err = err
return
}
} else {
//User with no permission should see only system commands
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
} else {
commands, err = c.App.ListAllCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
}
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
// check for permissions to view this command; must have perms to view team and
// PERMISSION_MANAGE_SLASH_COMMANDS for the team the command belongs to.
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionViewTeam) {
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
// again, return not_found to ensure id existence does not leak.
c.SetCommandNotFoundError()
return
}
if err := json.NewEncoder(w).Encode(cmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var commandArgs model.CommandArgs
if jsonErr := json.NewDecoder(r.Body).Decode(&commandArgs); jsonErr != nil {
c.SetInvalidParamWithErr("command_args", jsonErr)
return
}
if len(commandArgs.Command) <= 1 || strings.Index(commandArgs.Command, "/") != 0 || !model.IsValidId(commandArgs.ChannelId) {
c.Err = model.NewAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("executeCommand", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup {
// if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on
// some other team can't be run against this one
commandArgs.TeamId = channel.TeamId
} else {
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
// they can't just execute slash commands against arbitrary teams
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
}
}
commandArgs.UserId = c.AppContext.Session().UserId
commandArgs.T = c.AppContext.T
commandArgs.SiteURL = c.GetSiteURLHeader()
commandArgs.Session = *c.AppContext.Session()
response, err := c.App.ExecuteCommand(c.AppContext, &commandArgs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listAutocompleteCommands(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
commands, err := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listCommandAutocompleteSuggestions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
roleId := model.SystemUserRoleId
if c.IsSystemAdmin() {
roleId = model.SystemAdminRoleId
}
query := r.URL.Query()
userInput := query.Get("user_input")
if userInput == "" {
c.SetInvalidParam("userInput")
return
}
userInput = strings.TrimPrefix(userInput, "/")
commands, appErr := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if appErr != nil {
c.Err = appErr
return
}
commandArgs := &model.CommandArgs{
ChannelId: query.Get("channel_id"),
TeamId: c.Params.TeamId,
RootId: query.Get("root_id"),
UserId: c.AppContext.Session().UserId,
T: c.AppContext.T,
Session: *c.AppContext.Session(),
SiteURL: c.GetSiteURLHeader(),
Command: userInput,
}
suggestions := c.App.GetSuggestions(c.AppContext, commandArgs, commands, roleId)
js, err := json.Marshal(suggestions)
if err != nil {
c.Err = model.NewAppError("listCommandAutocompleteSuggestions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenCommandToken", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.RegenCommandToken(cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.Success()
c.LogAudit("success")
resp := make(map[string]string)
resp["token"] = rcmd.Token
w.Write([]byte(model.MapToJSON(resp)))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommandLocal() {
api.BaseRoutes.Commands.Handle("", api.APILocal(localCreateCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APILocal(listCommands)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APILocal(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APILocal(deleteCommand)).Methods("DELETE")
}
func localCreateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("localCreateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCompliance() {
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(createComplianceReport)).Methods("POST")
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(getComplianceReports)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}", api.APISessionRequired(getComplianceReport)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadComplianceReport)).Methods("GET")
}
func createComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Compliance
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("compliance", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createComplianceReport", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "compliance", &job)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateComplianceExportJob) {
c.SetPermissionError(model.PermissionCreateComplianceExportJob)
return
}
job.UserId = c.AppContext.Session().UserId
rjob, err := c.App.SaveComplianceReport(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("compliance")
auditRec.AddMeta("compliance_id", rjob.Id)
auditRec.AddMeta("compliance_desc", rjob.Desc)
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
auditRec := c.MakeAuditRecord("getComplianceReports", audit.Fail)
defer c.LogAuditRec(auditRec)
crs, err := c.App.GetComplianceReports(c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(crs); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("getComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
audit.AddEventParameter(auditRec, "report_id", c.Params.ReportId)
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("compliance_id", job.Id)
auditRec.AddMeta("compliance_desc", job.Desc)
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("downloadComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "compliance_id", c.Params.ReportId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
}
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(job)
auditRec.AddEventObjectType("compliance")
reportBytes, err := c.App.GetComplianceFile(job)
if err != nil {
c.Err = err
return
}
auditRec.AddMeta("length", len(reportBytes))
c.LogAudit("downloaded " + job.Desc)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Length", strconv.Itoa(len(reportBytes)))
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
// attach extra headers to trigger a download on IE, Edge, and Safari
ua := uasurfer.Parse(r.UserAgent())
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
// trim off anything before the final / so we just get the file's name
w.Header().Set("Content-Type", "application/octet-stream")
}
auditRec.Success()
w.Write(reportBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var writeFilter func(c *Context, structField reflect.StructField) bool
var readFilter func(c *Context, structField reflect.StructField) bool
var permissionMap map[string]*model.Permission
type filterType string
const (
FilterTypeWrite filterType = "write"
FilterTypeRead filterType = "read"
)
func (api *API) InitConfig() {
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(getConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(updateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APISessionRequired(patchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods("GET")
}
func init() {
writeFilter = makeFilterConfigByPermission(FilterTypeWrite)
readFilter = makeFilterConfigByPermission(FilterTypeRead)
permissionMap = map[string]*model.Permission{}
for _, p := range model.AllPermissions {
permissionMap[p.Id] = p
}
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
auditRec := c.MakeAuditRecord("getConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg, err := config.Merge(&model.Config{}, c.App.GetSanitizedConfig(), &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("getConfig", "api.config.get_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if jsonErr != nil {
c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func configReload(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("configReload", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReloadConfig) {
c.SetPermissionError(model.PermissionReloadConfig)
return
}
if !c.AppContext.Session().IsUnrestricted() && *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("configReload", "api.restricted_system_admin", nil, "", http.StatusBadRequest)
return
}
if err := c.App.ReloadConfig(); err != nil {
c.Err = model.NewAppError("configReload", "api.config.reload_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("updateConfig", audit.Fail)
// audit.AddEventParameter(auditRec, "config", cfg) // TODO We can do this but do we want to?
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
cfg, err = config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// Do not allow plugin uploads to be toggled through the API
*cfg.PluginSettings.EnableUploads = *appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
// This shallow-copies the slice header. So be careful if there are concurrent
// modifications to the slice.
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
// Do not allow marketplace URL to be toggled through the API if EnableUploads are disabled.
if cfg.PluginSettings.EnableUploads != nil && !*appCfg.PluginSettings.EnableUploads {
*cfg.PluginSettings.MarketplaceURL = *appCfg.PluginSettings.MarketplaceURL
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
// Both of them cannot be nil since cfg.SetDefaults is called earlier for cfg,
// and appCfg is the existing earlier config and if it's nil, server sets a default value.
if *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
c.App.HandleMessageExportConfig(cfg, appCfg)
if appErr := cfg.IsValid(); appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
// If the config for default server locale has changed, reinitialize the server's translations.
if oldCfg.LocalizationSettings.DefaultServerLocale != newCfg.LocalizationSettings.DefaultServerLocale {
s := newCfg.LocalizationSettings
if err = i18n.InitTranslations(*s.DefaultServerLocale, *s.DefaultClientLocale); err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.translations.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
//auditRec.AddEventResultState(cfg) // TODO we can do this too but do we want to? the config object is huge
auditRec.AddEventObjectType("config")
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientConfig", "api.config.client.old_format.app_error", nil, "", http.StatusNotImplemented)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var config map[string]string
if c.AppContext.Session().UserId == "" {
config = c.App.Srv().Platform().LimitedClientConfigWithComputed()
} else {
config = c.App.Srv().Platform().ClientConfigWithComputed()
}
w.Write([]byte(model.MapToJSON(config)))
}
func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
// Only return the environment variables for the subsections which the client is
// allowed to see
envConfig := c.App.GetEnvironmentConfig(func(structField reflect.StructField) bool {
return readFilter(c, structField)
})
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Write([]byte(model.StringInterfaceToJSON(envConfig)))
}
func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("patchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && cfg.ServiceSettings.SiteURL != nil && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
}
// Do not allow plugin uploads to be toggled through the API
if cfg.PluginSettings.EnableUploads != nil && *cfg.PluginSettings.EnableUploads != *appCfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.EnableUploads"}, "", http.StatusForbidden)
return
}
// Do not allow marketplace URL to be toggled if plugin uploads are disabled.
if cfg.PluginSettings.MarketplaceURL != nil && cfg.PluginSettings.EnableUploads != nil {
// Breaking it down to 2 conditions to make it simple.
if *cfg.PluginSettings.MarketplaceURL != *appCfg.PluginSettings.MarketplaceURL && !*cfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.MarketplaceURL"}, "", http.StatusForbidden)
return
}
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
if cfg.ComplianceSettings.Directory != nil && *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, err := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func makeFilterConfigByPermission(accessType filterType) func(c *Context, structField reflect.StructField) bool {
return func(c *Context, structField reflect.StructField) bool {
if structField.Type.Kind() == reflect.Struct {
return true
}
tagPermissions := strings.Split(structField.Tag.Get("access"), ",")
// If there are no access tag values and the role has manage_system, no need to continue
// checking permissions.
if len(tagPermissions) == 0 {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
return true
}
}
// one iteration for write_restrictable value, it could be anywhere in the order of values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
// ConfigAccessTagWriteRestrictable trumps all other permissions
if tagValue == model.ConfigAccessTagWriteRestrictable || tagValue == model.ConfigAccessTagCloudRestrictable {
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin && accessType == FilterTypeWrite {
return false
}
continue
}
}
// another iteration for permissions checks of other tag values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
if tagValue == model.ConfigAccessTagWriteRestrictable {
continue
}
if tagValue == model.ConfigAccessTagCloudRestrictable {
continue
}
if tagValue == model.ConfigAccessTagAnySysConsoleRead && accessType == FilterTypeRead &&
c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
return true
}
permissionID := fmt.Sprintf("sysconsole_%s_%s", accessType, tagValue)
if permission, ok := permissionMap[permissionID]; ok {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), permission) {
return true
}
} else {
mlog.Warn("Unrecognized config permissions tag value.", mlog.String("tag_value", permissionID))
}
}
// with manage_system, default to allow, otherwise default not-allow
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitConfigLocal() {
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localGetConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localUpdateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APILocal(localPatchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APILocal(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/migrate", api.APILocal(localMigrateConfig)).Methods("POST")
}
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg := c.App.GetSanitizedConfig()
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localUpdateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
appCfg := c.App.Config()
// Do not allow plugin uploads to be toggled through the API
cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
c.App.HandleMessageExportConfig(cfg, appCfg)
appErr := cfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, diffErr := config.Diff(oldCfg, newCfg)
if diffErr != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, diffErr.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localPatchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localPatchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
appCfg := c.App.Config()
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return true
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if mergeErr != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, mergeErr.Error(), http.StatusInternalServerError)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(c.App.GetSanitizedConfig()); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMigrateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
to, ok := props["to"].(string)
if !ok {
c.SetInvalidParam("to")
return
}
auditRec := c.MakeAuditRecord("migrateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
err := config.Migrate(from, to)
if err != nil {
c.Err = model.NewAppError("migrateConfig", "api.config.migrate_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDataRetention() {
api.BaseRoutes.DataRetention.Handle("/policy", api.APISessionRequired(getGlobalPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(getPolicies)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies_count", api.APISessionRequired(getPoliciesCount)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(createPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(getPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(patchPolicy)).Methods("PATCH")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(deletePolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(getTeamsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(addTeamsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(removeTeamsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams/search", api.APISessionRequired(searchTeamsInPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(addChannelsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(removeChannelsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels/search", api.APISessionRequired(searchChannelsInPolicy)).Methods("POST")
api.BaseRoutes.User.Handle("/data_retention/team_policies", api.APISessionRequired(getTeamPoliciesForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/data_retention/channel_policies", api.APISessionRequired(getChannelPoliciesForUser)).Methods("GET")
}
func getGlobalPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required.
policy, appErr := c.App.GetGlobalRetentionPolicy()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getGlobalPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
limit := c.Params.PerPage
offset := c.Params.Page * limit
policies, appErr := c.App.GetRetentionPolicies(offset, limit)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policies)
if err != nil {
c.Err = model.NewAppError("getPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPoliciesCount(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
count, appErr := c.App.GetRetentionPoliciesCount()
if appErr != nil {
c.Err = appErr
return
}
body := struct {
TotalCount int64 `json:"total_count"`
}{count}
err := json.NewEncoder(w).Encode(body)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policy, appErr := c.App.GetRetentionPolicy(c.Params.PolicyId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func createPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var policy model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "policy", &policy)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
newPolicy, appErr := c.App.CreateRetentionPolicy(&policy)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newPolicy)
auditRec.AddEventObjectType("policy")
js, err := json.Marshal(newPolicy)
if err != nil {
c.Err = model.NewAppError("createPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func patchPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var patch model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
c.RequirePolicyId()
patch.ID = c.Params.PolicyId
auditRec := c.MakeAuditRecord("patchPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "patch", &patch)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
policy, appErr := c.App.PatchRetentionPolicy(&patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(policy)
auditRec.AddEventObjectType("retention_policy")
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("patchPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(js)
}
func deletePolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
auditRec := c.MakeAuditRecord("deletePolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.DeleteRetentionPolicy(policyId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
teams, appErr := c.App.GetTeamsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("Api4.getTeamsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
var props model.TeamSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("team_search", err)
return
}
props.PolicyID = model.NewString(c.Params.PolicyId)
props.IncludePolicyID = model.NewBool(true)
teams, _, appErr := c.App.SearchAllTeams(&props)
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("searchTeamsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addTeamsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeTeamsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getChannelsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
channels, appErr := c.App.GetChannelsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channels)
if err != nil {
c.Err = model.NewAppError("Api4.getChannelsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts := model.ChannelSearchOpts{
PolicyID: c.Params.PolicyId,
IncludePolicyID: true,
Deleted: props.Deleted,
IncludeDeleted: props.IncludeDeleted,
Public: props.Public,
Private: props.Private,
TeamIds: props.TeamIds,
}
channels, _, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
channelsJSON, jsonErr := json.Marshal(channels)
if jsonErr != nil {
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(channelsJSON)
}
func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addChannelsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeChannelsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetTeamPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getTeamPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}
func getChannelPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetChannelPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getChannelPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDrafts() {
api.BaseRoutes.Drafts.Handle("", api.APISessionRequired(upsertDraft)).Methods("POST")
api.BaseRoutes.TeamForUser.Handle("/drafts", api.APISessionRequired(getDrafts)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/drafts/{thread_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteDraft)).Methods("DELETE")
api.BaseRoutes.ChannelForUser.Handle("/drafts", api.APISessionRequired(deleteDraft)).Methods("DELETE")
}
func upsertDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("upsertDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
var draft model.Draft
if jsonErr := json.NewDecoder(r.Body).Decode(&draft); jsonErr != nil {
c.SetInvalidParam("draft")
return
}
draft.DeleteAt = 0
draft.UserId = c.AppContext.Session().UserId
connectionID := r.Header.Get(model.ConnectionId)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, draft.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
dt, err := c.App.UpsertDraft(c.AppContext, &draft, connectionID)
if err != nil {
c.Err = err
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(dt); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getDrafts(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("getDrafts", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
hasPermission := false
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
hasPermission = true
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
drafts, err := c.App.GetDraftsForUser(c.AppContext.Session().UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(drafts); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("deleteDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
rootID := ""
connectionID := r.Header.Get(model.ConnectionId)
if c.Params.ThreadId != "" {
rootID = c.Params.ThreadId
}
userID := c.AppContext.Session().UserId
channelID := c.Params.ChannelId
draft, err := c.App.GetDraft(userID, channelID, rootID)
if err != nil {
switch {
case err.StatusCode == http.StatusNotFound:
// If the draft doesn't exist in the server, we don't need to delete.
ReturnStatusOK(w)
default:
c.Err = err
}
return
}
if c.AppContext.Session().UserId != draft.UserId {
c.SetPermissionError(model.PermissionDeletePost)
return
}
if _, err := c.App.DeleteDraft(userID, channelID, rootID, connectionID); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitElasticsearch() {
api.BaseRoutes.Elasticsearch.Handle("/test", api.APISessionRequired(testElasticsearch)).Methods("POST")
api.BaseRoutes.Elasticsearch.Handle("/purge_indexes", api.APISessionRequired(purgeElasticsearchIndexes)).Methods("POST")
}
func testElasticsearch(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding config.", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
// we set BulkIndexingTimeWindowSeconds to a random value to avoid failing on the nil check
// TODO: remove this hack once we remove BulkIndexingTimeWindowSeconds from the config.
if cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds == nil {
cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds = model.NewInt(0)
}
if checkHasNilFields(&cfg.ElasticsearchSettings) {
c.Err = model.NewAppError("testElasticsearch", "api.elasticsearch.test_elasticsearch_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
// PERMISSION_TEST_ELASTICSEARCH is an ancillary permission of PERMISSION_SYSCONSOLE_WRITE_ENVIRONMENT_ELASTICSEARCH,
// which should prevent read-only managers from password sniffing
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestElasticsearch) {
c.SetPermissionError(model.PermissionTestElasticsearch)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testElasticsearch", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.TestElasticsearch(cfg); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func purgeElasticsearchIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeElasticsearchIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeElasticsearchIndexes) {
c.SetPermissionError(model.PermissionPurgeElasticsearchIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeElasticsearchIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeElasticsearchIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EmojiMaxAutocompleteItems = 100
)
func (api *API) InitEmoji() {
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods("GET")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(deleteEmoji)).Methods("DELETE")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(getEmoji)).Methods("GET")
api.BaseRoutes.EmojiByName.Handle("", api.APISessionRequired(getEmojiByName)).Methods("GET")
api.BaseRoutes.Emoji.Handle("/image", api.APISessionRequiredTrustRequester(getEmojiImage)).Methods("GET")
}
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("createEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > app.MaxEmojiFileSize {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
// Allow any user with CREATE_EMOJIS permission at Team level to create emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionCreateEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreateEmojis)
return
}
}
m := r.MultipartForm
props := m.Value
if len(props["emoji"]) == 0 {
c.SetInvalidParam("emoji")
return
}
var emoji model.Emoji
if jsonErr := json.Unmarshal([]byte(props["emoji"][0]), &emoji); jsonErr != nil {
c.SetInvalidParam("emoji")
return
}
auditRec.AddEventResultState(&emoji)
auditRec.AddEventObjectType("emoji")
newEmoji, err := c.App.CreateEmoji(c.AppContext, c.AppContext.Session().UserId, &emoji, m)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(newEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiList(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
sort := r.URL.Query().Get("sort")
if sort != "" && sort != model.EmojiSortByName {
c.SetInvalidURLParam("sort")
return
}
listEmoji, err := c.App.GetEmojiList(c.AppContext, c.Params.Page, c.Params.PerPage, sort)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(listEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
audit.AddEventParameter(auditRec, "emoji_id", c.Params.EmojiId)
c.Err = err
return
}
auditRec.AddEventPriorState(emoji)
auditRec.AddEventObjectType("emoji")
// Allow any user with DELETE_EMOJIS permission at Team level to delete emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteEmojis)
return
}
}
if c.AppContext.Session().UserId != emoji.CreatorId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteOthersEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteOthersEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteOthersEmojis)
return
}
}
}
err = c.App.DeleteEmoji(c.AppContext, emoji)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiName()
if c.Err != nil {
return
}
emoji, err := c.App.GetEmojiByName(c.AppContext, c.Params.EmojiName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
image, imageType, err := c.App.GetEmojiImage(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "image/"+imageType)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Write(image)
}
func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
var emojiSearch model.EmojiSearch
if jsonErr := json.NewDecoder(r.Body).Decode(&emojiSearch); jsonErr != nil {
c.SetInvalidParamWithErr("term", jsonErr)
return
}
if emojiSearch.Term == "" {
c.SetInvalidParam("term")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, emojiSearch.Term, emojiSearch.PrefixOnly, web.PerPageMaximum)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
c.SetInvalidURLParam("name")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, name, true, EmojiMaxAutocompleteItems)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitExport() {
api.BaseRoutes.Exports.Handle("", api.APISessionRequired(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(downloadExport)).Methods("GET")
}
func listExports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
exports, appErr := c.App.ListExports()
if appErr != nil {
c.Err = appErr
return
}
data, err := json.Marshal(exports)
if err != nil {
c.Err = model.NewAppError("listImports", "app.export.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(data)
}
func deleteExport(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteExport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "export_name", c.Params.ExportName)
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.DeleteExport(c.Params.ExportName); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func downloadExport(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
filePath := filepath.Join(*c.App.Config().ExportSettings.Directory, c.Params.ExportName)
if ok, err := c.App.FileExists(filePath); err != nil {
c.Err = err
return
} else if !ok {
c.Err = model.NewAppError("downloadExport", "api.export.export_not_found.app_error", nil, "", http.StatusNotFound)
return
}
file, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
return
}
defer file.Close()
w.Header().Set("Content-Type", "application/zip")
http.ServeContent(w, r, c.Params.ExportName, time.Time{}, file)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitExportLocal() {
api.BaseRoutes.Exports.Handle("", api.APILocal(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APILocal(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APILocal(downloadExport)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"crypto/subtle"
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
const (
FileTeamId = "noteam"
PreviewImageType = "image/jpeg"
ThumbnailImageType = "image/jpeg"
)
const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
func (api *API) InitFile() {
api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST")
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET")
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET")
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST")
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET")
}
func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) {
v := req.Header.Get("Content-Type")
if v == "" {
return "", http.ErrNotMultipart
}
d, params, err := mime.ParseMediaType(v)
if err != nil || d != "multipart/form-data" {
return "", http.ErrNotMultipart
}
boundary, ok := params["boundary"]
if !ok {
return "", http.ErrMissingBoundary
}
return boundary, nil
}
func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) {
boundary, err := parseMultipartRequestHeader(req)
if err != nil {
return nil, err
}
if stream != nil {
return multipart.NewReader(stream, boundary), nil
}
return multipart.NewReader(req.Body, boundary), nil
}
func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadFileStream",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusForbidden)
return
}
// Parse the post as a regular form (in practice, use the URL values
// since we never expect a real application/x-www-form-urlencoded
// form).
if r.Form == nil {
err := r.ParseForm()
if err != nil {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return
}
}
if r.ContentLength == 0 {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, "Content-Length should not be 0", http.StatusBadRequest)
return
}
timestamp := time.Now()
var fileUploadResponse *model.FileUploadResponse
_, err := parseMultipartRequestHeader(r)
switch err {
case nil:
fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp)
case http.ErrNotMultipart:
fileUploadResponse = uploadFileSimple(c, r, timestamp)
default:
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
}
if c.Err != nil {
return
}
// Write the response values to the output upon return
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(fileUploadResponse); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// uploadFileSimple uploads a file from a simple POST with the file in the request body
func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse {
// Simple POST with the file in the body and all metadata in the args.
c.RequireChannelId()
c.RequireFilename()
if c.Err != nil {
return nil
}
auditRec := c.MakeAuditRecord("uploadFileSimple", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
clientId := r.Form.Get("client_id")
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(r.ContentLength),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
fileUploadResponse := &model.FileUploadResponse{
FileInfos: []*model.FileInfo{info},
}
if clientId != "" {
fileUploadResponse.ClientIds = []string{clientId}
}
auditRec.Success()
return fileUploadResponse
}
// uploadFileMultipart parses and uploads file(s) from a mime/multipart
// request. It pre-buffers up to the first part which is either the (a)
// `channel_id` value, or (b) a file. Then in case of (a) it re-processes the
// entire message recursively calling itself in stream mode. In case of (b) it
// calls to uploadFileMultipartLegacy for legacy support
func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse {
expectClientIds := true
var clientIds []string
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
var buf *bytes.Buffer
var mr *multipart.Reader
var err error
if asStream == nil {
// We need to buffer until we get the channel_id, or the first file.
buf = &bytes.Buffer{}
mr, err = multipartReader(r, io.TeeReader(r.Body, buf))
} else {
mr, err = multipartReader(r, asStream)
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
nFiles := 0
NextPart:
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
// Parse any form fields in the multipart.
formname := part.FormName()
if formname == "" {
continue
}
filename := part.FileName()
if filename == "" {
var b bytes.Buffer
_, err = io.CopyN(&b, part, maxMultipartFormDataBytes)
if err != nil && err != io.EOF {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_form_value.app_error",
map[string]any{"Formname": formname},
err.Error(), http.StatusBadRequest)
return nil
}
v := b.String()
switch formname {
case "channel_id":
if c.Params.ChannelId != "" && c.Params.ChannelId != v {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.multiple_channel_ids.app_error",
nil, "", http.StatusBadRequest)
return nil
}
if v != "" {
c.Params.ChannelId = v
}
// Got channel_id, re-process the entire post
// in the streaming mode.
if asStream == nil {
return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp)
}
case "client_ids":
if !expectClientIds {
c.SetInvalidParam("client_ids")
return nil
}
clientIds = append(clientIds, v)
default:
c.SetInvalidParam(formname)
return nil
}
continue NextPart
}
// A file part.
if c.Params.ChannelId == "" && asStream == nil {
// Got file before channel_id, fall back to legacy buffered mode
mr, err = multipartReader(r, io.MultiReader(buf, r.Body))
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
return uploadFileMultipartLegacy(c, mr, timestamp)
}
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// If there's no clientIds when the first file comes, expect
// none later.
if nFiles == 0 && len(clientIds) == 0 {
expectClientIds = false
}
// Must have a exactly one client ID for each file.
clientId := ""
if expectClientIds {
if nFiles >= len(clientIds) {
c.SetInvalidParam("client_ids")
return nil
}
clientId = clientIds[nFiles]
}
auditRec := c.MakeAuditRecord("uploadFileMultipart", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
// add to the response
resp.FileInfos = append(resp.FileInfos, info)
if expectClientIds {
resp.ClientIds = append(resp.ClientIds, clientId)
}
nFiles++
}
// Verify that the number of ClientIds matched the number of files.
if expectClientIds && len(clientIds) != nFiles {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": nFiles},
"", http.StatusBadRequest)
return nil
}
return &resp
}
// uploadFileMultipartLegacy reads, buffers, and then uploads the message,
// borrowing from http.ParseMultipartForm. If successful it returns a
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
timestamp time.Time) *model.FileUploadResponse {
// Parse the entire form.
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusInternalServerError)
return nil
}
// get and validate the channel Id, permission to upload there.
if len(form.Value["channel_id"]) == 0 {
c.SetInvalidParam("channel_id")
return nil
}
channelId := form.Value["channel_id"][0]
c.Params.ChannelId = channelId
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// Check that we have either no client IDs, or one per file.
clientIds := form.Value["client_ids"]
fileHeaders := form.File["files"]
if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) {
c.Err = model.NewAppError("uploadFilesMultipartBuffered",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)},
"", http.StatusBadRequest)
return nil
}
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
for i, fileHeader := range fileHeaders {
f, err := fileHeader.Open()
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
clientId := ""
if len(clientIds) > 0 {
clientId = clientIds[i]
}
auditRec := c.MakeAuditRecord("uploadFileMultipartLegacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", channelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
f.Close()
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
resp.FileInfos = append(resp.FileInfos, info)
if clientId != "" {
resp.ClientIds = append(resp.ClientIds, clientId)
}
}
return &resp
}
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
auditRec := c.MakeAuditRecord("getFile", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "force_download", forceDownload)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
auditRec.Success()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.ThumbnailPath == "" {
c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.ThumbnailPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("getFileLink", audit.Fail)
defer c.LogAuditRec(auditRec)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PostId == "" {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
resp := make(map[string]string)
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
resp["link"] = link
auditRec.Success()
w.Write([]byte(model.MapToJSON(resp)))
}
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PreviewPath == "" {
c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.PreviewPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
w.Header().Set("Cache-Control", "max-age=2592000, private")
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
hash := r.URL.Query().Get("h")
if hash == "" {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
}
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
searchFiles(c, w, r, c.Params.TeamId)
}
func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Config().FeatureFlags.CommandPalette {
searchFiles(c, w, r, "")
}
}
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
var params model.SearchParameter
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
if jsonErr != nil {
c.Err = model.NewAppError("searchFiles", "api.post.search_files.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if params.Terms == nil || *params.Terms == "" {
c.SetInvalidParam("terms")
return
}
terms := *params.Terms
timeZoneOffset := 0
if params.TimeZoneOffset != nil {
timeZoneOffset = *params.TimeZoneOffset
}
isOrSearch := false
if params.IsOrSearch != nil {
isOrSearch = *params.IsOrSearch
}
page := 0
if params.Page != nil {
page = *params.Page
}
perPage := 60
if params.PerPage != nil {
perPage = *params.PerPage
}
includeDeletedChannels := false
if params.IncludeDeletedChannels != nil {
includeDeletedChannels = *params.IncludeDeletedChannels
}
modifier := ""
if params.Modifier != nil {
modifier = *params.Modifier
}
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
c.SetInvalidParam("modifier")
return
}
startTime := time.Now()
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
metrics := c.App.Metrics()
if metrics != nil {
metrics.IncrementFilesSearchCounter()
metrics.ObserveFilesSearchDuration(elapsedTime)
}
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(results); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
// File is inaccessible due to cloud plan's limit.
if appErr.Id == "app.file.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessibleFileTime, "1")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/graph-gophers/dataloader/v6"
graphql "github.com/graph-gophers/graphql-go"
gqlerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type graphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]any `json:"variables"`
}
// Unique type to hold our context.
type ctxKey int
const (
webCtx ctxKey = 0
rolesLoaderCtx ctxKey = 1
channelsLoaderCtx ctxKey = 2
teamsLoaderCtx ctxKey = 3
usersLoaderCtx ctxKey = 4
)
const loaderBatchCapacity = web.PerPageMaximum
//go:embed schema.graphqls
var schemaRaw string
func (api *API) InitGraphQL() error {
// Guard with a feature flag.
if !api.srv.Config().FeatureFlags.GraphQL {
return nil
}
var err error
opts := []graphql.SchemaOpt{
graphql.UseFieldResolvers(),
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log())),
graphql.MaxParallelism(loaderBatchCapacity), // This is dangerous if the query
// uses any non-dataloader backed object. So we need to be a bit careful here.
}
if isProd() {
opts = append(opts,
// MaxDepth cannot be moved as a general param
// because otherwise introspection also doesn't work
// with just a depth of 4.
graphql.MaxDepth(4),
graphql.DisableIntrospection(),
)
}
api.schema, err = graphql.ParseSchema(schemaRaw, &resolver{}, opts...)
if err != nil {
return err
}
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APIHandlerTrustRequester(graphiQL)).Methods("GET")
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APISessionRequired(api.graphQL)).Methods("POST")
return nil
}
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
var response *graphql.Response
defer func() {
if response != nil {
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
}()
// Limit bodies to 100KiB.
// We need to enforce a lower limit than the file upload size,
// to prevent the library doing unnecessary parsing.
r.Body = http.MaxBytesReader(w, r.Body, 102400)
var params graphQLInput
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
err2 := gqlerrors.Errorf("invalid request body: %v", err)
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
if isProd() && params.OperationName == "" {
err2 := gqlerrors.Errorf("operation name not passed")
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
c.GraphQLOperationName = params.OperationName
// Populate the context with required info.
reqCtx := r.Context()
reqCtx = context.WithValue(reqCtx, webCtx, c)
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
channelsLoader := dataloader.NewBatchedLoader(graphQLChannelsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, channelsLoaderCtx, channelsLoader)
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
response = api.schema.Exec(reqCtx,
params.Query,
params.OperationName,
params.Variables)
if len(response.Errors) > 0 {
logFunc := mlog.Error
for _, gqlErr := range response.Errors {
if gqlErr.Err != nil {
if appErr, ok := gqlErr.Err.(*model.AppError); ok && appErr.StatusCode < http.StatusInternalServerError {
logFunc = mlog.Debug
break
}
}
}
logFunc("Error executing request", mlog.String("operation", params.OperationName),
mlog.Array("errors", response.Errors))
}
}
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write(graphiqlPage)
}
var graphiqlPage = []byte(`
<!DOCTYPE html>
<html>
<head>
<title>GraphiQL editor | Mattermost</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/api/v5/graphql", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
// isProd is a helper function to apply prod-specific graphQL validations.
func isProd() bool {
return model.BuildNumber != "dev"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// graphQLClient is an internal test client to run the tests.
// When the API matures, we will expose it to the model package.
type graphQLClient struct {
URL string // The location of the server, for example "http://localhost:8065"
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
httpClient *http.Client // The http client
authToken string
authType string
httpHeader map[string]string // Headers to be copied over for each request
}
func newGraphQLClient(url string) *graphQLClient {
url = strings.TrimRight(url, "/")
return &graphQLClient{url, url + model.APIURLSuffix, &http.Client{}, "", "", map[string]string{}}
}
func (c *graphQLClient) login(loginId string, password string) (*model.User, *model.Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
r, err := c.doAPIRequest(http.MethodPost, c.APIURL+"/users/login", strings.NewReader(model.MapToJSON(m)), map[string]string{model.HeaderEtagClient: ""})
if err != nil {
return nil, model.BuildResponse(r), err
}
defer closeBody(r)
c.authToken = r.Header.Get(model.HeaderToken)
c.authType = model.HeaderBearer
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
return nil, nil, model.NewAppError("login", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &user, model.BuildResponse(r), nil
}
func (c *graphQLClient) doAPIRequest(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) {
rq, err := c.prepareRequest(method, url, data, headers)
if err != nil {
return nil, err
}
rp, err := c.httpClient.Do(rq)
if err != nil {
return rp, err
}
return rp, nil
}
func (c *graphQLClient) prepareRequest(method, url string, data io.Reader, headers map[string]string) (*http.Request, error) {
rq, err := http.NewRequest(method, url, data)
if err != nil {
return nil, err
}
for k, v := range headers {
rq.Header.Set(k, v)
}
if c.authToken != "" {
rq.Header.Set(model.HeaderAuth, c.authType+" "+c.authToken)
}
if c.httpHeader != nil && len(c.httpHeader) > 0 {
for k, v := range c.httpHeader {
rq.Header.Set(k, v)
}
}
return rq, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitGroup() {
// GET /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET")
// POST /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods("POST")
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroup)).Methods("GET")
// PUT /api/v4/groups/:group_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroup)).Methods("PUT")
// POST /api/v4/groups/:group_id/teams/:team_id/link
// POST /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(linkGroupSyncable)).Methods("POST")
// DELETE /api/v4/groups/:group_id/teams/:team_id/link
// DELETE /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE")
// GET /api/v4/groups/:group_id/teams/:team_id
// GET /api/v4/groups/:group_id/channels/:channel_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroupSyncable)).Methods("GET")
// GET /api/v4/groups/:group_id/teams
// GET /api/v4/groups/:group_id/channels
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}",
api.APISessionRequired(getGroupSyncables)).Methods("GET")
// PUT /api/v4/groups/:group_id/teams/:team_id/patch
// PUT /api/v4/groups/:group_id/channels/:channel_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroupSyncable)).Methods("PUT")
// GET /api/v4/groups/:group_id/stats
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
api.APISessionRequired(getGroupStats)).Methods("GET")
// GET /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(getGroupMembers)).Methods("GET")
// GET /api/v4/users/:user_id/groups
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByUserId)).Methods("GET")
// GET /api/v4/channels/:channel_id/groups
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByChannel)).Methods("GET")
// GET /api/v4/teams/:team_id/groups
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByTeam)).Methods("GET")
// GET /api/v4/teams/:team_id/groups_by_channels
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels",
api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET")
// DELETE /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(deleteGroup)).Methods("DELETE")
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/restore",
api.APISessionRequired(restoreGroup)).Methods("POST")
// POST /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(addGroupMembers)).Methods("POST")
// DELETE /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(deleteGroupMembers)).Methods("DELETE")
}
func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{
IncludeMemberCount: c.Params.IncludeMemberCount,
}, restrictions)
if appErr != nil {
c.Err = appErr
return
}
if group.Source == model.GroupSourceLdap {
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.getGroup"
c.Err = appErr
return
}
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.getGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
var group *model.GroupWithUserIds
if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
c.SetInvalidParamWithErr("group", err)
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("createGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.createGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateCustomGroup) {
c.SetPermissionError(model.PermissionCreateCustomGroup)
return
}
if !group.AllowReference {
c.Err = model.NewAppError("createGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusBadRequest)
return
}
if group.GetRemoteId() != "" {
c.Err = model.NewAppError("createGroup", "api.custom_groups.no_remote_id", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "group", group)
newGroup, appErr := c.App.CreateGroupWithUserIds(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newGroup)
auditRec.AddEventObjectType("group")
js, err := json.Marshal(newGroup)
if err != nil {
c.Err = model.NewAppError("createGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, group.Source)
if appErr != nil {
appErr.Where = "Api4.patchGroup"
c.Err = appErr
return
}
var requiredPermission *model.Permission
if group.Source == model.GroupSourceCustom {
requiredPermission = model.PermissionEditCustomGroup
} else {
requiredPermission = model.PermissionSysconsoleWriteUserManagementGroups
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, requiredPermission) {
c.SetPermissionError(requiredPermission)
return
}
var groupPatch model.GroupPatch
if err := json.NewDecoder(r.Body).Decode(&groupPatch); err != nil {
c.SetInvalidParamWithErr("group", err)
return
}
if group.Source == model.GroupSourceCustom && groupPatch.AllowReference != nil && !*groupPatch.AllowReference {
c.Err = model.NewAppError("Api4.patchGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("patchGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "group", group)
if groupPatch.AllowReference != nil && *groupPatch.AllowReference {
if groupPatch.Name == nil {
tmp := strings.ReplaceAll(strings.ToLower(group.DisplayName), " ", "-")
groupPatch.Name = &tmp
} else {
if *groupPatch.Name == model.UserNotifyAll || *groupPatch.Name == model.ChannelMentionsNotifyProp || *groupPatch.Name == model.UserNotifyHere {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_reserved_name_error", nil, "", http.StatusBadRequest)
return
}
//check if a user already has this group name
user, _ := c.App.GetUserByUsername(*groupPatch.Name)
if user != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_user_name_error", nil, "", http.StatusBadRequest)
return
}
//check if a mentionable group already has this name
searchOpts := model.GroupSearchOpts{
FilterAllowReference: true,
}
existingGroup, _ := c.App.GetGroupByName(*groupPatch.Name, searchOpts)
if existingGroup != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_group_name_error", nil, "", http.StatusBadRequest)
return
}
}
}
group.Patch(&groupPatch)
group, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(group)
auditRec.AddEventObjectType("group")
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceLdap {
c.Err = model.NewAppError("Api4.linkGroupSyncable", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("linkGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "syncable_id", syncableID)
audit.AddEventParameter(auditRec, "syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group%s", syncableType), err)
return
}
audit.AddEventParameterAuditable(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr = verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable := &model.GroupSyncable{
GroupId: c.Params.GroupId,
SyncableId: syncableID,
Type: syncableType,
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
w.WriteHeader(http.StatusCreated)
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupSyncables, appErr := c.App.GetGroupSyncables(c.Params.GroupId, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncables)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
auditRec := c.MakeAuditRecord("patchGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "old_syncable_id", syncableID)
audit.AddEventParameter(auditRec, "old_syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group[%s]Patch", syncableType), err)
return
}
audit.AddEventParameterAuditable(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.ldap_groups.license_error", nil, "",
http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpdateGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
auditRec := c.MakeAuditRecord("unlinkGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "syncable_id", syncableID)
audit.AddEventParameter(auditRec, "syncable_type", string(syncableType))
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
_, appErr = c.App.DeleteGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
auditRec.Success()
ReturnStatusOK(w)
}
func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType, syncableID string) *model.AppError {
switch syncableType {
case model.GroupSyncableTypeTeam:
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionManageTeam) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageTeam})
}
case model.GroupSyncableTypeChannel:
channel, err := c.App.GetChannel(c.AppContext, syncableID)
if err != nil {
return err
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, permission) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
}
return nil
}
func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, group.Source)
if appErr != nil {
appErr.Where = "Api4.getGroupMembers"
c.Err = appErr
return
}
if group.Source == model.GroupSourceLdap && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
members, count, appErr := c.App.GetGroupMemberUsersPage(c.Params.GroupId, c.Params.Page, c.Params.PerPage, restrictions)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
Members []*model.User `json:"members"`
Count int `json:"total_member_count"`
}{
Members: members,
Count: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupStats", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupID := c.Params.GroupId
count, appErr := c.App.GetGroupMemberCount(groupID, nil)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(model.GroupStats{
GroupID: groupID,
TotalMemberCount: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupStats", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
groups, appErr := c.App.GetGroupsByUserId(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groups)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}
func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
return nil, appErr
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionReadPrivateChannelGroups
} else {
permission = model.PermissionReadPublicChannelGroups
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
return nil, c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groupsAssociatedByChannelID, appErr := c.App.GetGroupsAssociatedToChannelsByTeam(c.Params.TeamId, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
GroupsAssociatedToChannels map[string][]*model.GroupWithSchemeAdmin `json:"groups"`
}{
GroupsAssociatedToChannels: groupsAssociatedByChannelID,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
var teamID, NotAssociatedToChannelID, ChannelIDForMemberCount string
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
source := c.Params.GroupSource
if id := c.Params.NotAssociatedToTeam; model.IsValidId(id) {
teamID = id
}
if id := c.Params.NotAssociatedToChannel; model.IsValidId(id) {
NotAssociatedToChannelID = id
}
if id := c.Params.IncludeChannelMemberCount; model.IsValidId(id) {
ChannelIDForMemberCount = id
}
// If they specify the group_source as custom when the feature is disabled, throw an error
if appErr := licensedAndConfiguredForGroupBySource(c.App, source); appErr != nil {
appErr.Where = "Api4.getGroups"
c.Err = appErr
return
}
// If they don't specify a source and custom groups are disabled, ensure they only get ldap groups in the response
if !*c.App.Config().ServiceSettings.EnableCustomGroups {
source = model.GroupSourceLdap
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
Source: source,
FilterHasMember: c.Params.FilterHasMember,
IncludeTimezones: includeTimezones,
}
if teamID != "" {
_, appErr := c.App.GetTeam(teamID)
if appErr != nil {
c.Err = appErr
return
}
opts.NotAssociatedToTeam = teamID
}
if NotAssociatedToChannelID != "" {
channel, appErr := c.App.GetChannel(c.AppContext, NotAssociatedToChannelID)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission) {
c.SetPermissionError(permission)
return
}
opts.NotAssociatedToChannel = NotAssociatedToChannelID
}
if ChannelIDForMemberCount != "" {
channel, appErr := c.App.GetChannel(c.AppContext, ChannelIDForMemberCount)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission) {
c.SetPermissionError(permission)
return
}
opts.IncludeChannelMemberCount = ChannelIDForMemberCount
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, err := strconv.ParseInt(sinceString, 10, 64)
if err != nil {
c.SetInvalidParamWithErr("since", err)
return
}
opts.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
var (
groups = []*model.Group{}
canSee bool = true
)
if opts.FilterHasMember != "" {
canSee, appErr = c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, opts.FilterHasMember)
if appErr != nil {
c.Err = appErr
return
}
}
if canSee {
groups, appErr = c.App.GetGroups(c.Params.Page, c.Params.PerPage, opts, restrictions)
if appErr != nil {
c.Err = appErr
return
}
}
var (
b []byte
err error
)
if c.Params.IncludeTotalCount {
totalCount, cerr := c.App.Srv().Store().Group().GroupCount()
if cerr != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.custom_groups.count_err", nil, "", http.StatusInternalServerError).Wrap(cerr)
return
}
gwc := &model.GroupsWithCount{
Groups: groups,
TotalCount: totalCount,
}
b, err = json.Marshal(gwc)
} else {
b, err = json.Marshal(groups)
}
if err != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, nil)
if err != nil {
c.Err = err
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil {
lcErr.Where = "Api4.deleteGroup"
c.Err = lcErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionDeleteCustomGroup) {
c.SetPermissionError(model.PermissionDeleteCustomGroup)
return
}
auditRec := c.MakeAuditRecord("deleteGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
_, err = c.App.DeleteGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, nil)
if err != nil {
c.Err = err
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.restoreGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented)
return
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil {
lcErr.Where = "Api4.restoreGroup"
c.Err = lcErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionRestoreCustomGroup) {
c.SetPermissionError(model.PermissionRestoreCustomGroup)
return
}
auditRec := c.MakeAuditRecord("restoreGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
_, err = c.App.RestoreGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.deleteGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var newMembers *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&newMembers); err != nil {
c.SetInvalidParamWithErr("addGroupMembers", err)
return
}
auditRec := c.MakeAuditRecord("addGroupMembers", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "addGroupMembers_userids", newMembers.UserIds)
members, appErr := c.App.UpsertGroupMembers(c.Params.GroupId, newMembers.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.deleteGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var deleteBody *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&deleteBody); err != nil {
c.SetInvalidParamWithErr("deleteGroupMembers", err)
return
}
auditRec := c.MakeAuditRecord("deleteGroupMembers", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "deleteGroupMembers_userids", deleteBody.UserIds)
members, appErr := c.App.DeleteGroupMembers(c.Params.GroupId, deleteBody.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
// licensedAndConfiguredForGroupBySource returns an app error if not properly license or configured for the given group type. The returned app error
// will have a blank 'Where' field, which should be subsequently set by the caller, for example:
//
// err := licensedAndConfiguredForGroupBySource(c.App, group.Source)
// err.Where = "Api4.getGroup"
func licensedAndConfiguredForGroupBySource(app app.AppIface, source model.GroupSource) *model.AppError {
lic := app.Srv().License()
if lic == nil {
return model.NewAppError("", "api.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceLdap && !*lic.Features.LDAPGroups {
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceCustom && lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusBadRequest)
}
if source == model.GroupSourceCustom && !*app.Config().ServiceSettings.EnableCustomGroups {
return model.NewAppError("", "api.custom_groups.feature_disabled", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
)
func (api *API) InitGroupLocal() {
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET")
}
func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/gziphandler"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Context = web.Context
type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
// granted.
func (api *API) APIHandler(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
// be granted.
func (api *API) APISessionRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: false,
RequireRemoteClusterToken: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed,
// if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
// authentication must be waived.
func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
// websocket.
func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
// are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: true,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
// responding with HTTP 503 (Service Unavailable).
func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
DisableWhenBusy: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APILocal provides a handler for API endpoints to be used in local
// mode, this is, through a UNIX socket and without an authenticated
// session, but with one that has no user set and no permission
// restrictions
func (api *API) APILocal(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
func requireLicense(c *Context) *model.AppError {
if c.App.Channels().License() == nil {
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func minimumProfessionalLicense(c *Context) *model.AppError {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return err
}
return nil
}
func rejectGuests(c *Context) *model.AppError {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/url"
"strconv"
"github.com/pkg/errors"
)
func parseInt(u *url.URL, name string, defaultValue int) (int, error) {
valueStr := u.Query().Get(name)
if valueStr == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return 0, errors.Wrapf(err, "failed to parse %s as integer", name)
}
return value, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
// Endpoints for cloud installations should not go in this file.
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET")
// POST /api/v4/hosted_customer/bootstrap
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
// POST /api/v4/hosted_customer/customer
api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST")
// POST /api/v4/hosted_customer/confirm
api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST")
// GET /api/v4/hosted_customer/invoices
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
}
func ensureSelfHostedAdmin(c *Context, where string) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
license := c.App.Channels().License()
if license.IsCloud() {
c.Err = model.NewAppError(where, "api.cloud.license_error", nil, "Cloud installations do not use this endpoint", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
}
func checkSelfHostedPurchaseEnabled(c *Context) bool {
config := c.App.Config()
if config == nil {
return false
}
enabled := config.ServiceSettings.SelfHostedPurchase
return enabled != nil && *enabled
}
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedBootstrap"
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
reset := r.URL.Query().Get("reset") == "true"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email, Reset: reset})
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
json, err := json.Marshal(signupProgress)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}
func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedCustomer"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var form *model.SelfHostedCustomerForm
if err = json.Unmarshal(bodyBytes, &form); err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customerResponse)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedConfirm"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var confirm model.SelfHostedConfirmPaymentMethodRequest
err = json.Unmarshal(bodyBytes, &confirm)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email)
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
if err.Error() == fmt.Sprintf("%d", http.StatusUnprocessableEntity) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
license, err := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
// dealing with an AppError
if !(reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil()) {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
License: utils.GetClientLicense(license),
Progress: confirmResponse.Progress,
})
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
go func() {
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
if err != nil {
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
}
}()
_, _ = w.Write(clientResponse)
}
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.handleSignupAvailable"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil {
if err.Error() == "upstream_off" {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusServiceUnavailable)
} else {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
return
}
systemValue, err := c.App.Srv().Store().System().GetByName(model.SystemHostedPurchaseNeedsScreening)
if err == nil && systemValue != nil {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusTooEarly)
return
}
ReturnStatusOK(w)
}
func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoices"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
invoices, err := c.App.Cloud().GetSelfHostedInvoices()
if err != nil {
if err.Error() == "404" {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotFound).Wrap(errors.New("invoices for license not found"))
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoicePDF"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"net/url"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitImage() {
api.BaseRoutes.Image.Handle("", api.APISessionRequiredTrustRequester(getImage)).Methods("GET")
}
func getImage(c *Context, w http.ResponseWriter, r *http.Request) {
actualURL := r.URL.Query().Get("url")
parsedURL, err := url.Parse(actualURL)
if err != nil {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, err.Error(), http.StatusBadRequest)
return
} else if parsedURL.Opaque != "" {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, "", http.StatusBadRequest)
return
}
siteURL, err := url.Parse(*c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = model.NewAppError("getImage", "model.config.is_valid.site_url.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if parsedURL.Scheme == "" {
parsedURL.Scheme = siteURL.Scheme
}
if parsedURL.Host == "" {
parsedURL.Host = siteURL.Host
}
// in case image proxy is enabled and we are fetching a remote image (NOT static or served by plugins), pass request to proxy
if *c.App.Config().ImageProxySettings.Enable && parsedURL.Host != siteURL.Host {
c.App.ImageProxy().GetImage(w, r, parsedURL.String())
} else {
http.Redirect(w, r, parsedURL.String(), http.StatusFound)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitImport() {
api.BaseRoutes.Imports.Handle("", api.APISessionRequired(listImports)).Methods("GET")
}
func listImports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
imports, appErr := c.App.ListImports()
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(imports); err != nil {
c.Logger.Warn("Error writing imports", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitImportLocal() {
api.BaseRoutes.Imports.Handle("", api.APILocal(listImports)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
// Inactive channels
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
}
// Top Reactions
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, nil, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, &c.AppContext.Session().UserId, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Threads
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topDMs); err != nil {
c.Err = model.NewAppError("getTopDMsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// top inactive channels
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
if len(topChannelList.Items) == 0 {
return nil, nil
}
var postCountsByDay []*model.DurationPostCount
channelIDs := topChannelList.ChannelIDs()
var grouping model.PostCountGrouping
if timeRange == model.TimeRangeToday {
grouping = model.PostsByHour
} else {
grouping = model.PostsByDay
}
postCountsByDay, err := c.App.PostCountsByDuration(c.AppContext, channelIDs, startTime.UnixMilli(), userID, grouping, location)
if err != nil {
return nil, err
}
return model.ToDailyPostCountViewModel(postCountsByDay, startTime, model.TimeRangeToNumberDays(timeRange), channelIDs), nil
}
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
ntms.TotalCount = count
if err := json.NewEncoder(w).Encode(ntms); err != nil {
c.Err = model.NewAppError("getNewTeamembersForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitAction() {
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.APISessionRequired(doPostAction)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/open", api.APIHandler(openDialog)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/submit", api.APISessionRequired(submitDialog)).Methods("POST")
}
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var actionRequest model.DoPostActionRequest
err := json.NewDecoder(r.Body).Decode(&actionRequest)
if err != nil {
c.Logger.Warn("Error decoding the action request", mlog.Err(err))
}
var cookie *model.PostActionCookie
if actionRequest.Cookie != "" {
cookie = &model.PostActionCookie{}
cookieStr := ""
cookieStr, err = model.DecryptPostActionCookie(actionRequest.Cookie, c.App.PostActionCookieSecret())
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = json.Unmarshal([]byte(cookieStr), &cookie)
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), cookie.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
var appErr *model.AppError
resp := &model.PostActionAPIResponse{Status: "OK"}
resp.TriggerId, appErr = c.App.DoPostActionWithCookie(c.AppContext, c.Params.PostId, c.Params.ActionId, c.AppContext.Session().UserId,
actionRequest.SelectedOption, cookie)
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func openDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var dialog model.OpenDialogRequest
err := json.NewDecoder(r.Body).Decode(&dialog)
if err != nil {
c.SetInvalidParamWithErr("dialog", err)
return
}
if dialog.URL == "" {
c.SetInvalidParam("url")
return
}
if appErr := c.App.OpenInteractiveDialog(dialog); appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var submit model.SubmitDialogRequest
jsonErr := json.NewDecoder(r.Body).Decode(&submit)
if jsonErr != nil {
c.SetInvalidParamWithErr("dialog", jsonErr)
return
}
if submit.URL == "" {
c.SetInvalidParam("url")
return
}
submit.UserId = c.AppContext.Session().UserId
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), submit.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), submit.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
resp, err := c.App.SubmitInteractiveDialog(c.AppContext, submit)
if err != nil {
c.Err = err
return
}
b, _ := json.Marshal(resp)
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitJob() {
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APISessionRequired(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APISessionRequired(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APISessionRequired(getJobsByType)).Methods("GET")
}
func getJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), job.Type)
if permissionRequired == nil {
c.Err = model.NewAppError("getJob", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadJob(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
const FilePath = "export"
const FileMime = "application/zip"
c.RequireJobId()
if c.Err != nil {
return
}
if !*config.MessageExportSettings.DownloadExportResults {
c.Err = model.NewAppError("downloadExportResultsNotEnabled", "app.job.download_export_results_not_enabled", nil, "", http.StatusNotImplemented)
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
// Currently, this endpoint only supports downloading the compliance report.
// If you need to download another job type, you will need to alter this section of the code to accommodate it.
if job.Type == model.JobTypeMessageExport && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
} else if job.Type != model.JobTypeMessageExport {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
isDownloadable, _ := strconv.ParseBool(job.Data["is_downloadable"])
if !isDownloadable {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job", nil, "", http.StatusBadRequest)
return
}
fileName := job.Id + ".zip"
filePath := filepath.Join(FilePath, fileName)
fileReader, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
// already sets that for us
web.WriteFileResponse(fileName, FileMime, 0, time.Unix(0, job.LastActivityAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, true, w, r)
}
func createJob(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Job
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("job", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "job", &job)
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), &job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCreateJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
rjob, err := c.App.CreateJob(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("job")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getJobs(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
var validJobTypes []string
for _, jobType := range model.AllJobTypes {
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), jobType)
if permissionRequired == nil {
mlog.Warn("The job types of a job you are trying to retrieve does not contain permissions", mlog.String("jobType", jobType))
continue
}
if hasPermission {
validJobTypes = append(validJobTypes, jobType)
}
}
if len(validJobTypes) == 0 {
c.SetPermissionError()
return
}
jobs, appErr := c.App.GetJobsByTypesPage(validJobTypes, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobs", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getJobsByType(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobType()
if c.Err != nil {
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), c.Params.JobType)
if permissionRequired == nil {
c.Err = model.NewAppError("getJobsByType", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
jobs, appErr := c.App.GetJobsByTypePage(c.Params.JobType, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobsByType", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("cancelJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "job_id", c.Params.JobId)
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(job)
auditRec.AddEventObjectType("job")
// if permission to create, permission to cancel, same permission
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCancelJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := c.App.CancelJob(c.Params.JobId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitJobLocal() {
api.BaseRoutes.Jobs.Handle("", api.APILocal(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APILocal(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APILocal(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APILocal(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APILocal(getJobsByType)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type mixedUnlinkedGroup struct {
Id *string `json:"mattermost_group_id"`
DisplayName string `json:"name"`
RemoteId string `json:"primary_key"`
HasSyncables *bool `json:"has_syncables"`
}
func (api *API) InitLdap() {
api.BaseRoutes.LDAP.Handle("/sync", api.APISessionRequired(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APISessionRequired(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIdLdap)).Methods("POST")
// GET /api/v4/ldap/groups?page=0&per_page=1000
api.BaseRoutes.LDAP.Handle("/groups", api.APISessionRequired(getLdapGroups)).Methods("GET")
// POST /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(linkLdapGroup)).Methods("POST")
// DELETE /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/users/{user_id}/group_sync_memberships", api.APISessionRequired(addUserToGroupSyncables)).Methods("POST")
}
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
type LdapSyncOptions struct {
IncludeRemovedMembers bool `json:"include_removed_members"`
}
var opts LdapSyncOptions
err := json.NewDecoder(r.Body).Decode(&opts)
if err != nil {
c.Logger.Warn("Error decoding LDAP sync options", mlog.Err(err))
}
auditRec := c.MakeAuditRecord("syncLdap", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateLdapSyncJob) {
c.SetPermissionError(model.PermissionCreateLdapSyncJob)
return
}
c.App.SyncLdap(opts.IncludeRemovedMembers)
auditRec.Success()
ReturnStatusOK(w)
}
func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
c.SetPermissionError(model.PermissionTestLdap)
return
}
if err := c.App.TestLdap(); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
opts := model.LdapGroupSearchOpts{
Q: c.Params.Q,
}
if c.Params.IsLinked != nil {
opts.IsLinked = c.Params.IsLinked
}
if c.Params.IsConfigured != nil {
opts.IsConfigured = c.Params.IsConfigured
}
groups, total, appErr := c.App.GetAllLdapGroupsPage(c.Params.Page, c.Params.PerPage, opts)
if appErr != nil {
c.Err = appErr
return
}
mugs := []*mixedUnlinkedGroup{}
for _, group := range groups {
mug := &mixedUnlinkedGroup{
DisplayName: group.DisplayName,
RemoteId: group.GetRemoteId(),
}
if len(group.Id) == 26 {
mug.Id = &group.Id
mug.HasSyncables = &group.HasSyncables
}
mugs = append(mugs, mug)
}
b, err := json.Marshal(struct {
Count int `json:"count"`
Groups []*mixedUnlinkedGroup `json:"groups"`
}{Count: total, Groups: mugs})
if err != nil {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
auditRec := c.MakeAuditRecord("linkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
ldapGroup, appErr := c.App.GetLdapGroup(c.Params.RemoteId)
if appErr != nil {
c.Err = appErr
return
}
if ldapGroup == nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
return
}
group, appErr := c.App.GetGroupByRemoteID(ldapGroup.GetRemoteId(), model.GroupSourceLdap)
if appErr != nil && appErr.Id != "app.group.no_rows" {
c.Err = appErr
return
}
if group != nil {
audit.AddEventParameterAuditable(auditRec, "group", group)
}
var status int
var newOrUpdatedGroup *model.Group
// Truncate display name if necessary
var displayName string
if len(ldapGroup.DisplayName) > model.GroupDisplayNameMaxLength {
displayName = ldapGroup.DisplayName[:model.GroupDisplayNameMaxLength]
} else {
displayName = ldapGroup.DisplayName
}
// Group has been previously linked
if group != nil {
if group.DeleteAt == 0 {
newOrUpdatedGroup = group
} else {
group.DeleteAt = 0
group.DisplayName = displayName
group.RemoteId = ldapGroup.RemoteId
newOrUpdatedGroup, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
}
status = http.StatusOK
} else {
// Group has never been linked
//
// For group mentions implementation, the Name column will no longer be set by default.
// Instead it will be set and saved in the web app when Group Mentions is enabled.
newGroup := &model.Group{
DisplayName: displayName,
RemoteId: ldapGroup.RemoteId,
Source: model.GroupSourceLdap,
}
newOrUpdatedGroup, appErr = c.App.CreateGroup(newGroup)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
status = http.StatusCreated
}
b, err := json.Marshal(newOrUpdatedGroup)
if err != nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(status)
w.Write(b)
}
func unlinkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("unlinkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
group, err := c.App.GetGroupByRemoteID(c.Params.RemoteId, model.GroupSourceLdap)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(group)
auditRec.AddEventObjectType("group")
if group.DeleteAt == 0 {
deletedGroup, err := c.App.DeleteGroup(group.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(deletedGroup)
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateIdLdap(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
toAttribute, ok := props["toAttribute"].(string)
if !ok || toAttribute == "" {
c.SetInvalidParam("toAttribute")
return
}
auditRec := c.MakeAuditRecord("idMigrateLdap", audit.Fail)
audit.AddEventParameter(auditRec, "to_attribute", toAttribute)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.MigrateIdLDAP(toAttribute); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseLdapCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.parseform.app_error", nil, err.Error(), http.StatusBadRequest)
}
m := r.MultipartForm
fileArray, ok := m.File["certificate"]
if !ok {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
}
if len(fileArray) <= 0 {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
}
return fileArray[0], nil
}
func addLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPublicCert) {
c.SetPermissionError(model.PermissionAddLdapPublicCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPublicCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPrivateCert) {
c.SetPermissionError(model.PermissionAddLdapPrivateCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPrivateCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPublicCert) {
c.SetPermissionError(model.PermissionRemoveLdapPublicCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPublicCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPrivateCert) {
c.SetPermissionError(model.PermissionRemoveLdapPrivateCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPrivateCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// addUserToGroupSyncables creates memberships—for the given user—to all of their group syncables (i.e. channels or teams).
// For each group the user is a member of, for each channel and/or team that group is associated with, the user will be added.
func addUserToGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if user.AuthService != model.UserAuthServiceLdap {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.user.add_user_to_group_syncables.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addUserToGroupSyncables", audit.Fail)
defer c.LogAuditRec(auditRec)
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: true, ScopedUserID: &user.Id}
err := c.App.CreateDefaultMemberships(c.AppContext, params)
if err != nil {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.admin.syncables_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitLdapLocal() {
api.BaseRoutes.LDAP.Handle("/migrateid", api.APILocal(migrateIdLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/sync", api.APILocal(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APILocal(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/groups", api.APILocal(getLdapGroups)).Methods("GET")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(removeLdapPrivateCertificate)).Methods("DELETE")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitLicense() {
api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET")
}
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = c.App.Srv().ClientLicense()
} else {
clientLicense = c.App.Srv().GetSanitizedClientLicense()
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("addLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("addLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
licenseBytes := buf.Bytes()
license, appErr := utils.LicenseValidator.LicenseFromBytes(licenseBytes)
if appErr != nil {
c.Err = appErr
return
}
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
lm := c.App.Srv().Platform().LicenseManager()
if lm == nil {
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
return
}
canStartTrialLicense, err := lm.CanStartTrial()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("addLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
}
license, appErr = c.App.Srv().SaveLicense(licenseBytes)
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
if c.App.Channels().License().IsCloud() {
// If cloud, invalidate the caches when a new license is loaded
defer c.App.Srv().Cloud.HandleLicenseChange()
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("removeLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("removeLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestTrialLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
if err != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.error", nil, err.Error(), http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
var trialRequest struct {
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
}
b, readErr := io.ReadAll(r.Body)
if readErr != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
return
}
json.Unmarshal(b, &trialRequest)
if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestRenewalLink", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestRenewalLink", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
renewalLink, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
if c.App.Cloud() == nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
// check if it is possible to renew license on the portal with generated token
status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if e != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusInternalServerError)
return
}
if !status.IsRenewable {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest)
return
}
auditRec.Success()
c.LogAudit("success")
_, werr := w.Write([]byte(fmt.Sprintf(`{"renewal_link": "%s"}`, renewalLink)))
if werr != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.app_error", nil, werr.Error(), http.StatusForbidden)
return
}
}
func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("getPrevTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
license, err := c.App.Srv().Platform().LicenseManager().GetPrevTrial()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = utils.GetClientLicense(license)
} else {
clientLicense = utils.GetSanitizedClientLicense(utils.GetClientLicense(license))
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
return
}
// If a true up review has already been submitted for the current due date, complete the request
// with no errors.
if status.Completed {
ReturnStatusOK(w)
}
profileMap, err := c.App.GetTrueUpProfile()
if err != nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError)
return
}
profileMapJson, err := json.Marshal(profileMap)
if err != nil {
c.SetJSONEncodingError(err)
return
}
// Do not send true-up review data if the user has already requested one for the quarter.
// And only send a true-up review via as a one-time telemetry request if telemetry is disabled.
telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics
if telemetryEnabled != nil && !*telemetryEnabled {
// Send telemetry data
c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap)
// Update the review status to reflect the completion.
status.Completed = true
c.App.Srv().Store().TrueUpReview().Update(status)
}
// Encode to string rather than byte[] otherwise json.Marshal will encode it further.
encodedData := b64.StdEncoding.EncodeToString(profileMapJson)
responseContent := struct {
Content string `json:"content"`
}{Content: encodedData}
response, _ := json.Marshal(responseContent)
w.Write(response)
}
func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
// Check for license
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
}
json, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitLicenseLocal() {
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
}
func localAddLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localAddLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
license, appErr := c.App.Srv().SaveLicense(buf.Bytes())
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localRemoveLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
userId := c.AppContext.Session().UserId
appErr := c.App.SaveAdminNotification(userId, notifyAdminRequest)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications {
c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden)
return
}
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
// only system admins can manually trigger these notifications
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
appErr := c.App.SendNotifyAdminPosts(c.AppContext, "", "", notifyAdminRequest.TrialNotification)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOAuth() {
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(createOAuthApp)).Methods("POST")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(updateOAuthApp)).Methods("PUT")
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(getOAuthApps)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(getOAuthApp)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("/info", api.APISessionRequired(getOAuthAppInfo)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(deleteOAuthApp)).Methods("DELETE")
api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.APISessionRequired(regenerateOAuthAppSecret)).Methods("POST")
api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.APISessionRequired(getAuthorizedOAuthApps)).Methods("GET")
}
func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOAuthApp", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = false
}
oauthApp.CreatorId = c.AppContext.Session().UserId
rapp, err := c.App.CreateOAuthApp(&oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rapp)
auditRec.AddEventObjectType("oauth_app")
c.LogAudit("client_id=" + rapp.Id)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rapp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
// The app being updated in the payload must be the same one as indicated in the URL.
if oauthApp.Id != c.Params.AppId {
c.SetInvalidParam("app_id")
return
}
oldOAuthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oldOAuthApp)
if c.AppContext.Session().UserId != oldOAuthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = oldOAuthApp.IsTrusted
}
updatedOAuthApp, err := c.App.UpdateOAuthApp(oldOAuthApp, &oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedOAuthApp)
auditRec.AddEventObjectType("oauth_app")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(updatedOAuthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
var apps []*model.OAuthApp
var appErr *model.AppError
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
apps, appErr = c.App.GetOAuthApps(c.Params.Page, c.Params.PerPage)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
apps, appErr = c.App.GetOAuthAppsByCreator(c.AppContext.Session().UserId, c.Params.Page, c.Params.PerPage)
} else {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
oauthApp.Sanitize()
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if c.AppContext.Session().UserId != oauthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
err = c.App.DeleteOAuthApp(oauthApp.Id)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func regenerateOAuthAppSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenerateOAuthAppSecret", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
oauthApp, err = c.App.RegenerateOAuthAppSecret(oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(oauthApp)
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
apps, appErr := c.App.GetAuthorizedAppsForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getAuthorizedOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOpenGraph() {
api.BaseRoutes.OpenGraph.Handle("", api.APISessionRequired(getOpenGraphMetadata)).Methods("POST")
}
func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
props := model.StringInterfaceFromJSON(r.Body)
url := ""
ok := false
if url, ok = props["url"].(string); url == "" || !ok {
c.SetInvalidParam("url")
return
}
buf, err := c.App.GetOpenGraphMetadata(url)
if err != nil {
mlog.Warn("GetOpenGraphMetadata request failed",
mlog.String("requestURL", url),
mlog.Err(err))
w.Write([]byte(`{"url": ""}`))
return
}
w.Write(buf)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitPermissions() {
api.BaseRoutes.Permissions.Handle("/ancillary", api.APISessionRequired(appendAncillaryPermissions)).Methods("GET")
}
func appendAncillaryPermissions(c *Context, w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["subsection_permissions"]
if !ok || len(keys[0]) < 1 {
c.SetInvalidURLParam("subsection_permissions")
return
}
permissions := strings.Split(keys[0], ",")
b, err := json.Marshal(model.AddAncillaryPermissions(permissions))
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaximumPluginFileSize = 50 * 1024 * 1024
)
func (api *API) InitPlugin() {
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/statuses", api.APISessionRequired(getPluginStatuses)).Methods("GET")
api.BaseRoutes.Plugin.Handle("/enable", api.APISessionRequired(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APISessionRequired(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/webapp", api.APIHandler(getWebappPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(getMarketplacePlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APIHandler(setFirstAdminVisitMarketplaceStatus)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APISessionRequired(getFirstAdminVisitMarketplaceStatus)).Methods("GET")
}
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
if !*config.PluginSettings.Enable || !*config.PluginSettings.EnableUploads || *config.PluginSettings.RequirePluginSignature {
c.Err = model.NewAppError("uploadPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("uploadPlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := r.ParseMultipartForm(MaximumPluginFileSize); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
pluginArray, ok := m.File["plugin"]
if !ok {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(pluginArray) <= 0 {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest)
return
}
audit.AddEventParameter(auditRec, "filename", pluginArray[0].Filename)
file, err := pluginArray[0].Open()
if err != nil {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
return
}
defer file.Close()
force := false
if len(m.Value["force"]) > 0 && m.Value["force"][0] == "true" {
force = true
}
installPlugin(c, w, file, force)
auditRec.Success()
}
func installPluginFromURL(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable ||
*c.App.Config().PluginSettings.RequirePluginSignature ||
!*c.App.Config().PluginSettings.EnableUploads {
c.Err = model.NewAppError("installPluginFromURL", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installPluginFromURL", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
force, _ := strconv.ParseBool(r.URL.Query().Get("force"))
downloadURL := r.URL.Query().Get("plugin_download_url")
audit.AddEventParameter(auditRec, "url", downloadURL)
pluginFileBytes, err := c.App.DownloadFromURL(downloadURL)
if err != nil {
c.Err = model.NewAppError("installPluginFromURL", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
installPlugin(c, w, bytes.NewReader(pluginFileBytes), force)
auditRec.Success()
}
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installMarketplacePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
pluginRequest, err := model.PluginRequestFromReader(r.Body)
if err != nil {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
return
}
audit.AddEventParameter(auditRec, "plugin_id", pluginRequest.Id)
// Always install the latest compatible version
// https://mattermost.atlassian.net/browse/MM-41981
pluginRequest.Version = ""
manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddMeta("plugin_name", manifest.Name)
auditRec.AddMeta("plugin_desc", manifest.Description)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetPlugins()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetClusterPluginStatuses()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("removePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
err := c.App.Channels().RemovePlugin(c.Params.PluginId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getWebappPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
manifests, appErr := c.App.GetActivePluginManifests()
if appErr != nil {
c.Err = appErr
return
}
clientManifests := []*model.Manifest{}
for _, m := range manifests {
if m.HasClient() {
manifest := m.ClientManifest()
// There is no reason to expose the SettingsSchema in this API call; it's not used in the webapp.
manifest.SettingsSchema = nil
clientManifests = append(clientManifests, manifest)
}
}
js, err := json.Marshal(clientManifests)
if err != nil {
c.Err = model.NewAppError("getWebappPlugins", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getMarketplacePlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
filter, err := parseMarketplacePluginFilter(r.URL)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// if we are looking for remote only, we don't need to check for permissions
if !filter.RemoteOnly && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
plugins, appErr := c.App.GetMarketplacePlugins(filter)
if appErr != nil {
c.Err = appErr
return
}
json, err := json.Marshal(plugins)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("activatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("enablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.EnablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("deactivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("disablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.DisablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, error) {
page, err := parseInt(u, "page", 0)
if err != nil {
return nil, err
}
perPage, err := parseInt(u, "per_page", 100)
if err != nil {
return nil, err
}
filter := u.Query().Get("filter")
serverVersion := u.Query().Get("server_version")
localOnly, _ := strconv.ParseBool(u.Query().Get("local_only"))
remoteOnly, _ := strconv.ParseBool(u.Query().Get("remote_only"))
if localOnly && remoteOnly {
return nil, errors.New("local_only and remote_only cannot be both true")
}
return &model.MarketplacePluginFilter{
Page: page,
PerPage: perPage,
Filter: filter,
ServerVersion: serverVersion,
LocalOnly: localOnly,
RemoteOnly: remoteOnly,
}, nil
}
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
manifest, appErr := c.App.InstallPlugin(plugin, force)
if appErr != nil {
c.Err = appErr
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("setFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj := model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "true",
}
if err := c.App.Srv().Store().System().SaveOrUpdate(&firstAdminVisitMarketplaceObj); err != nil {
c.Err = model.NewAppError("setFirstAdminVisitMarketplaceStatus", "api.error_set_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
message := model.NewWebSocketEvent(model.WebsocketFirstAdminVisitMarketplaceStatusReceived, "", "", "", nil, "")
message.Add("firstAdminVisitMarketplaceStatus", firstAdminVisitMarketplaceObj.Value)
c.App.Publish(message)
auditRec.Success()
ReturnStatusOK(w)
}
func getFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj, err := c.App.Srv().Store().System().GetByName(model.SystemFirstAdminVisitMarketplace)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
firstAdminVisitMarketplaceObj = &model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "false",
}
default:
c.Err = model.NewAppError("getFirstAdminVisitMarketplaceStatus", "api.error_get_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(firstAdminVisitMarketplaceObj); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPluginLocal() {
api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugin.Handle("/enable", api.APILocal(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APILocal(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(getMarketplacePlugins)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitPost() {
api.BaseRoutes.Posts.Handle("", api.APISessionRequired(createPost)).Methods("POST")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(getPost)).Methods("GET")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(deletePost)).Methods("DELETE")
api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods("POST")
api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods("POST")
api.BaseRoutes.Post.Handle("/edit_history", api.APISessionRequired(getEditHistoryForPost)).Methods("GET")
api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods("GET")
api.BaseRoutes.Post.Handle("/info", api.APISessionRequired(getPostInfo)).Methods("GET")
api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods("GET")
api.BaseRoutes.PostsForChannel.Handle("", api.APISessionRequired(getPostsForChannel)).Methods("GET")
api.BaseRoutes.PostsForUser.Handle("/flagged", api.APISessionRequired(getFlaggedPostsForUser)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/posts/unread", api.APISessionRequired(getPostsForChannelAroundLastUnread)).Methods("GET")
api.BaseRoutes.Team.Handle("/posts/search", api.APISessionRequiredDisableWhenBusy(searchPostsInTeam)).Methods("POST")
api.BaseRoutes.Posts.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchPostsInAllTeams)).Methods("POST")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(updatePost)).Methods("PUT")
api.BaseRoutes.Post.Handle("/patch", api.APISessionRequired(patchPost)).Methods("PUT")
api.BaseRoutes.PostForUser.Handle("/set_unread", api.APISessionRequired(setPostUnread)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/reminder", api.APISessionRequired(setPostReminder)).Methods("POST")
api.BaseRoutes.Post.Handle("/pin", api.APISessionRequired(pinPost)).Methods("POST")
api.BaseRoutes.Post.Handle("/unpin", api.APISessionRequired(unpinPost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods("DELETE")
}
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
// Strip away delete_at if passed
post.DeleteAt = 0
post.UserId = c.AppContext.Session().UserId
auditRec := c.MakeAuditRecord("createPost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameterAuditable(auditRec, "post", &post)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, post.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if post.CreateAt != 0 && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
post.CreateAt = 0
}
setOnline := r.URL.Query().Get("set_online")
setOnlineBool := true // By default, always set online.
var err2 error
if setOnline != "" {
setOnlineBool, err2 = strconv.ParseBool(setOnline)
if err2 != nil {
mlog.Warn("Failed to parse set_online URL query parameter from createPost request", mlog.Err(err2))
setOnlineBool = true // Set online nevertheless.
}
}
rp, err := c.App.CreatePostAsUser(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), c.AppContext.Session().Id, setOnlineBool)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rp)
auditRec.AddEventObjectType("post")
if setOnlineBool {
c.App.SetStatusOnline(c.AppContext.Session().UserId, false)
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
c.ExtendSessionExpiryIfNeeded(w, r)
w.WriteHeader(http.StatusCreated)
// Note that rp has already had PreparePostForClient called on it by App.CreatePost
if err := rp.EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
ephRequest := model.PostEphemeral{}
jsonErr := json.NewDecoder(r.Body).Decode(&ephRequest)
if jsonErr != nil {
c.SetInvalidParamWithErr("body", jsonErr)
return
}
if ephRequest.UserID == "" {
c.SetInvalidParam("user_id")
return
}
if ephRequest.Post == nil {
c.SetInvalidParam("post")
return
}
ephRequest.Post.UserId = c.AppContext.Session().UserId
ephRequest.Post.CreateAt = model.GetMillis()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePostEphemeral) {
c.SetPermissionError(model.PermissionCreatePostEphemeral)
return
}
rp := c.App.SendEphemeralPost(c.AppContext, ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
w.WriteHeader(http.StatusCreated)
rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret())
rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, true, false, true)
rp, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := rp.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
afterPost := r.URL.Query().Get("after")
if afterPost != "" && !model.IsValidId(afterPost) {
c.SetInvalidParam("after")
return
}
beforePost := r.URL.Query().Get("before")
if beforePost != "" && !model.IsValidId(beforePost) {
c.SetInvalidParam("before")
return
}
sinceString := r.URL.Query().Get("since")
var since int64
var parseError error
if sinceString != "" {
since, parseError = strconv.ParseInt(sinceString, 10, 64)
if parseError != nil {
c.SetInvalidParam("since")
return
}
}
skipFetchThreads := r.URL.Query().Get("skipFetchThreads") == "true"
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
includeDeleted := r.URL.Query().Get("include_deleted") == "true"
channelId := c.Params.ChannelId
page := c.Params.Page
perPage := c.Params.PerPage
if !c.IsSystemAdmin() && includeDeleted {
c.SetPermissionError(model.PermissionReadDeletedPosts)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
channel, err := c.App.GetChannel(c.AppContext, channelId)
if err != nil {
c.Err = err
return
}
if channel.DeleteAt != 0 {
c.Err = model.NewAppError("Api4.getPostsForChannel", "api.user.view_archived_channels.get_posts_for_channel.app_error", nil, "", http.StatusForbidden)
return
}
}
var list *model.PostList
var err *model.AppError
etag := ""
if since > 0 {
list, err = c.App.GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: since, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
} else if afterPost != "" {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts After", w, r) {
return
}
list, err = c.App.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelId, PostId: afterPost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
} else if beforePost != "" {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts Before", w, r) {
return
}
list, err = c.App.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelId, PostId: beforePost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
} else {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts", w, r) {
return
}
list, err = c.App.GetPostsPage(model.GetPostsOptions{ChannelId: channelId, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
}
if err != nil {
c.Err = err
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
c.App.AddCursorIdsForPostList(list, afterPost, beforePost, since, page, perPage, collapsedThreads)
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireChannelId()
if c.Err != nil {
return
}
userId := c.Params.UserId
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
channelId := c.Params.ChannelId
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if c.Params.LimitAfter == 0 {
c.SetInvalidURLParam("limit_after")
return
}
skipFetchThreads := r.URL.Query().Get("skipFetchThreads") == "true"
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
postList, err := c.App.GetPostsForChannelAroundLastUnread(c.AppContext, channelId, userId, c.Params.LimitBefore, c.Params.LimitAfter, skipFetchThreads, collapsedThreads, collapsedThreadsExtended)
if err != nil {
c.Err = err
return
}
etag := ""
if len(postList.Order) == 0 {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts", w, r) {
return
}
postList, err = c.App.GetPostsPage(model.GetPostsOptions{ChannelId: channelId, Page: app.PageDefault, PerPage: c.Params.LimitBefore, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
if err != nil {
c.Err = err
return
}
}
postList.NextPostId = c.App.GetNextPostIdFromPostList(postList, collapsedThreads)
postList.PrevPostId = c.App.GetPrevPostIdFromPostList(postList, collapsedThreads)
clientPostList := c.App.PreparePostListForClient(c.AppContext, postList)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
channelId := r.URL.Query().Get("channel_id")
teamId := r.URL.Query().Get("team_id")
var posts *model.PostList
var err *model.AppError
if channelId != "" {
posts, err = c.App.GetFlaggedPostsForChannel(c.Params.UserId, channelId, c.Params.Page, c.Params.PerPage)
} else if teamId != "" {
posts, err = c.App.GetFlaggedPostsForTeam(c.Params.UserId, teamId, c.Params.Page, c.Params.PerPage)
} else {
posts, err = c.App.GetFlaggedPosts(c.Params.UserId, c.Params.Page, c.Params.PerPage)
}
if err != nil {
c.Err = err
return
}
pl := model.NewPostList()
channelReadPermission := make(map[string]bool)
for _, post := range posts.Posts {
allowed, ok := channelReadPermission[post.ChannelId]
if !ok {
allowed = false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
allowed = true
}
channelReadPermission[post.ChannelId] = allowed
}
if !allowed {
continue
}
pl.AddPost(post)
pl.AddOrder(post.Id)
}
pl.SortByCreateAt()
clientPostList := c.App.PreparePostListForClient(c.AppContext, pl)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
// getPost also sets a header to indicate, if post is inaccessible due to the cloud plan's limit.
func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
post, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), includeDeleted)
if err != nil {
c.Err = err
// Post is inaccessible due to cloud plan's limit.
if err.Id == "app.post.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessiblePostTime, "1")
}
return
}
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, false, false, true)
post, err = c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if c.HandleEtag(post.Etag(), "Get Post", w, r) {
return
}
w.Header().Set(model.HeaderEtagServer, post.Etag())
if err := post.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
// getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
postIDs := model.ArrayFromJSON(r.Body)
if len(postIDs) == 0 {
c.SetInvalidParam("post_ids")
return
}
if len(postIDs) > 1000 {
c.Err = model.NewAppError("getPostsByIds", "api.post.posts_by_ids.invalid_body.request_error", map[string]any{"MaxLength": 1000}, "", http.StatusBadRequest)
return
}
postsList, firstInaccessiblePostTime, err := c.App.GetPostsByIds(postIDs)
if err != nil {
c.Err = err
return
}
var posts = []*model.Post{}
channelMap := make(map[string]*model.Channel)
for _, post := range postsList {
var channel *model.Channel
if val, ok := channelMap[post.ChannelId]; ok {
channel = val
} else {
channel, err = c.App.GetChannel(c.AppContext, post.ChannelId)
if err != nil {
c.Err = err
return
}
channelMap[channel.Id] = channel
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
if channel.Type != model.ChannelTypeOpen || (channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)) {
continue
}
}
post = c.App.PreparePostForClient(c.AppContext, post, false, false, true)
post.StripActionIntegrations()
posts = append(posts, post)
}
w.Header().Set(model.HeaderFirstInaccessiblePostTime, strconv.FormatInt(firstInaccessiblePostTime, 10))
if err := json.NewEncoder(w).Encode(posts); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) {
c.SetPermissionError(model.PermissionEditPost)
return
}
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
if c.AppContext.Session().UserId != originalPost.UserId {
c.SetPermissionError(model.PermissionEditPost)
return
}
postsList, err := c.App.GetEditHistoryForPost(c.Params.PostId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(postsList); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deletePost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
post, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionDeletePost)
return
}
auditRec.AddEventPriorState(post)
auditRec.AddEventObjectType("post")
if c.AppContext.Session().UserId == post.UserId {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeletePost) {
c.SetPermissionError(model.PermissionDeletePost)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeleteOthersPosts) {
c.SetPermissionError(model.PermissionDeleteOthersPosts)
return
}
}
if _, err := c.App.DeletePost(c.AppContext, c.Params.PostId, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
// For now, by default we return all items unless it's set to maintain
// backwards compatibility with mobile. But when the next ESR passes, we need to
// change this to web.PerPageDefault.
perPage := 0
if perPageStr := r.URL.Query().Get("perPage"); perPageStr != "" {
var err error
perPage, err = strconv.Atoi(perPageStr)
if err != nil || perPage > web.PerPageMaximum {
c.SetInvalidParam("perPage")
return
}
}
var fromCreateAt int64
if fromCreateAtStr := r.URL.Query().Get("fromCreateAt"); fromCreateAtStr != "" {
var err error
fromCreateAt, err = strconv.ParseInt(fromCreateAtStr, 10, 64)
if err != nil {
c.SetInvalidParam("fromCreateAt")
return
}
}
fromPost := r.URL.Query().Get("fromPost")
// Either only fromCreateAt must be set, or both fromPost and fromCreateAt must be set
if fromPost != "" && fromCreateAt == 0 {
c.SetInvalidParam("if fromPost is set, then fromCreatAt must also be set")
}
direction := ""
if dir := r.URL.Query().Get("direction"); dir != "" {
if dir != "up" && dir != "down" {
c.SetInvalidParam("direction")
return
}
direction = dir
}
opts := model.GetPostsOptions{
SkipFetchThreads: r.URL.Query().Get("skipFetchThreads") == "true",
CollapsedThreads: r.URL.Query().Get("collapsedThreads") == "true",
CollapsedThreadsExtended: r.URL.Query().Get("collapsedThreadsExtended") == "true",
PerPage: perPage,
Direction: direction,
FromPost: fromPost,
FromCreateAt: fromCreateAt,
}
list, err := c.App.GetPostThread(c.Params.PostId, opts, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if list.FirstInaccessiblePostTime != 0 {
// e.g. if root post is archived in a cloud plan,
// we don't want to display the thread,
// but at the same time the request was not bad,
// so we return the time of archival and let the client
// show an error
if err := (&model.PostList{Order: []string{}, FirstInaccessiblePostTime: list.FirstInaccessiblePostTime}).EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
post, ok := list.Posts[c.Params.PostId]
if !ok {
c.SetInvalidURLParam("post_id")
return
}
if _, err = c.App.GetPostIfAuthorized(c.AppContext, post.Id, c.AppContext.Session(), false); err != nil {
c.Err = err
return
}
if c.HandleEtag(list.Etag(), "Get Post Thread", w, r) {
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, clientPostList.Etag())
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func searchPostsInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
searchPosts(c, w, r, c.Params.TeamId)
}
func searchPostsInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
searchPosts(c, w, r, "")
}
func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId string) {
var params model.SearchParameter
if jsonErr := json.NewDecoder(r.Body).Decode(¶ms); jsonErr != nil {
c.Err = model.NewAppError("searchPosts", "api.post.search_posts.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if params.Terms == nil || *params.Terms == "" {
c.SetInvalidParam("terms")
return
}
terms := *params.Terms
timeZoneOffset := 0
if params.TimeZoneOffset != nil {
timeZoneOffset = *params.TimeZoneOffset
}
isOrSearch := false
if params.IsOrSearch != nil {
isOrSearch = *params.IsOrSearch
}
page := 0
if params.Page != nil {
page = *params.Page
}
perPage := 60
if params.PerPage != nil {
perPage = *params.PerPage
}
includeDeletedChannels := false
if params.IncludeDeletedChannels != nil {
includeDeletedChannels = *params.IncludeDeletedChannels
}
modifier := ""
if params.Modifier != nil {
modifier = *params.Modifier
}
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
c.SetInvalidParam("modifier")
return
}
startTime := time.Now()
results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
metrics := c.App.Metrics()
if metrics != nil {
metrics.IncrementPostsSearchCounter()
metrics.ObservePostsSearchDuration(elapsedTime)
}
if err != nil {
c.Err = err
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, results.PostList)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
results = model.MakePostSearchResults(clientPostList, results.Matches)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := results.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updatePost", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "post", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
// The post being updated in the payload must be the same one as indicated in the URL.
if post.Id != c.Params.PostId {
c.SetInvalidParam("id")
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) {
c.SetPermissionError(model.PermissionEditPost)
return
}
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
auditRec.AddEventPriorState(originalPost)
auditRec.AddEventObjectType("post")
// Updating the file_ids of a post is not a supported operation and will be ignored
post.FileIds = originalPost.FileIds
if c.AppContext.Session().UserId != originalPost.UserId {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditOthersPosts) {
c.SetPermissionError(model.PermissionEditOthersPosts)
return
}
}
post.Id = c.Params.PostId
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != originalPost.Message {
c.Err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
return
}
rpost, err := c.App.UpdatePost(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), false)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rpost)
if err := rpost.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var post model.PostPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchPost", audit.Fail)
audit.AddEventParameter(auditRec, "id", c.Params.PostId)
audit.AddEventParameterAuditable(auditRec, "patch", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
// Updating the file_ids of a post is not a supported operation and will be ignored
post.FileIds = nil
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
auditRec.AddEventPriorState(originalPost)
auditRec.AddEventObjectType("post")
var permission *model.Permission
if c.AppContext.Session().UserId == originalPost.UserId {
permission = model.PermissionEditPost
} else {
permission = model.PermissionEditOthersPosts
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, permission) {
c.SetPermissionError(permission)
return
}
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != nil {
c.Err = model.NewAppError("patchPost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
return
}
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post))
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(patchedPost)
if err := patchedPost.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapBoolFromJSON(r.Body)
collapsedThreadsSupported := props["collapsed_threads_supported"]
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
state, err := c.App.MarkChannelAsUnreadFromPost(c.AppContext, c.Params.PostId, c.Params.UserId, collapsedThreadsSupported)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(state); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setPostReminder(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
var reminder model.PostReminder
if jsonErr := json.NewDecoder(r.Body).Decode(&reminder); jsonErr != nil {
c.SetInvalidParamWithErr("target_time", jsonErr)
return
}
appErr := c.App.SetPostReminder(c.Params.PostId, c.Params.UserId, reminder.TargetTime)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) {
c.RequirePostId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("saveIsPinnedPost", audit.Fail)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
post, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(post)
auditRec.AddEventObjectType("post")
patch := &model.PostPatch{}
patch.IsPinned = model.NewBool(isPinned)
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, patch)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(patchedPost)
auditRec.Success()
ReturnStatusOK(w)
}
func pinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
saveIsPinnedPost(c, w, true)
}
func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
saveIsPinnedPost(c, w, false)
}
func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
acknowledgement, appErr := c.App.SaveAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(acknowledgement)
if err != nil {
c.Err = model.NewAppError("acknowledgePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
_, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.Err = err
return
}
appErr := c.App.DeleteAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
infos, appErr := c.App.GetFileInfosForPostWithMigration(c.Params.PostId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if c.HandleEtag(model.GetEtagForFileInfos(infos), "Get File Infos For Post", w, r) {
return
}
js, err := json.Marshal(infos)
if err != nil {
c.Err = model.NewAppError("getFileInfosForPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set(model.HeaderEtagServer, model.GetEtagForFileInfos(infos))
w.Write(js)
}
func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(info)
if err != nil {
c.Err = model.NewAppError("getPostInfo", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPostLocal() {
api.BaseRoutes.Post.Handle("", api.APILocal(getPost)).Methods("GET")
api.BaseRoutes.PostsForChannel.Handle("", api.APILocal(getPostsForChannel)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitPreference() {
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(getPreferences)).Methods("GET")
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(updatePreferences)).Methods("PUT")
api.BaseRoutes.Preferences.Handle("/delete", api.APISessionRequired(deletePreferences)).Methods("POST")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferencesByCategory)).Methods("GET")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/name/{preference_name:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferenceByCategoryAndName)).Methods("GET")
}
func getPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferencesForUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferencesByCategory(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryForUser(c.Params.UserId, c.Params.Category)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferenceByCategoryAndName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory().RequirePreferenceName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.Params.UserId, c.Params.Category, c.Params.PreferenceName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updatePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
var sanitizedPreferences model.Preferences
for _, pref := range preferences {
if pref.Category == model.PreferenceCategoryFlaggedPost {
post, err := c.App.GetSinglePost(pref.Name, false)
if err != nil {
c.SetInvalidParam("preference.name")
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
sanitizedPreferences = append(sanitizedPreferences, pref)
}
if err := c.App.UpdatePreferences(c.Params.UserId, sanitizedPreferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deletePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
if err := c.App.DeletePreferences(c.Params.UserId, preferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitReaction() {
api.BaseRoutes.Reactions.Handle("", api.APISessionRequired(saveReaction)).Methods("POST")
api.BaseRoutes.Post.Handle("/reactions", api.APISessionRequired(getReactions)).Methods("GET")
api.BaseRoutes.ReactionByNameForPostForUser.Handle("", api.APISessionRequired(deleteReaction)).Methods("DELETE")
api.BaseRoutes.Posts.Handle("/ids/reactions", api.APISessionRequired(getBulkReactions)).Methods("POST")
}
func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
var reaction model.Reaction
if jsonErr := json.NewDecoder(r.Body).Decode(&reaction); jsonErr != nil {
c.SetInvalidParamWithErr("reaction", jsonErr)
return
}
if !model.IsValidId(reaction.UserId) || !model.IsValidId(reaction.PostId) || reaction.EmojiName == "" || len(reaction.EmojiName) > model.EmojiNameMaxLength {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.invalid.app_error", nil, "", http.StatusBadRequest)
return
}
if reaction.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), reaction.PostId, model.PermissionAddReaction) {
c.SetPermissionError(model.PermissionAddReaction)
return
}
re, err := c.App.SaveReactionForPost(c.AppContext, &reaction)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(re); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getReactions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
reactions, appErr := c.App.GetReactionsForPost(c.Params.PostId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
c.RequirePostId()
if c.Err != nil {
return
}
c.RequireEmojiName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionRemoveReaction) {
c.SetPermissionError(model.PermissionRemoveReaction)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveOthersReactions) {
c.SetPermissionError(model.PermissionRemoveOthersReactions)
return
}
reaction := &model.Reaction{
UserId: c.Params.UserId,
PostId: c.Params.PostId,
EmojiName: c.Params.EmojiName,
}
err := c.App.DeleteReactionForPost(c.AppContext, reaction)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
postIds := model.ArrayFromJSON(r.Body)
for _, postId := range postIds {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
reactions, appErr := c.App.GetBulkReactionsForPosts(postIds)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getBulkReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitRemoteCluster() {
api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
}
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterPing", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
var ping model.RemoteClusterPing
if err := json.Unmarshal(frame.Msg.Payload, &ping); err != nil {
c.SetInvalidParamWithErr("msg.payload", err)
return
}
ping.RecvAt = model.GetMillis()
if metrics := c.App.Metrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
err := json.NewEncoder(w).Encode(ping)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func remoteClusterAcceptMessage(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
service, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr = frame.IsValid()
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptMessage", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
// pass message to Remote Cluster Service and write response
resp := service.ReceiveIncomingMsg(rc, frame.Msg)
b, err := json.Marshal(resp)
if err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func remoteClusterConfirmInvite(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if jsonErr := json.NewDecoder(r.Body).Decode(&frame); jsonErr != nil {
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, err := c.App.GetRemoteCluster(frame.RemoteId)
if err != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
if time.Since(model.GetTimeForMillis(rc.CreateAt)) > remotecluster.InviteExpiresAfter {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.context.invitation_expired.error", nil, "", http.StatusBadRequest)
return
}
var confirm model.RemoteClusterInvite
if jsonErr := json.Unmarshal(frame.Msg.Payload, &confirm); jsonErr != nil {
c.SetInvalidParam("msg.payload")
return
}
rc.RemoteTeamId = confirm.RemoteTeamId
rc.SiteURL = confirm.SiteURL
rc.RemoteToken = confirm.Token
if _, err := c.App.UpdateRemoteCluster(rc); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadRemoteData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadRemoteData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.RemoteId != c.GetRemoteID(r) {
c.Err = model.NewAppError("uploadRemoteData", "api.context.remote_id_mismatch.app_error",
nil, "", http.StatusUnauthorized)
return
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) == 0 {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("remoteUploadProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil || !user.IsRemote() {
c.SetInvalidURLParam("user_id")
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// cursorPrefix is used to categorize objects
// sent in a cursor. The type is prepended
// to the string with a - to find which
// object the id belongs to.
//
// And after the type is extracted, object
// specific logic can be applied to extract the id.
type cursorPrefix string
const (
channelMemberCursorPrefix cursorPrefix = "channelMember"
channelCursorPrefix cursorPrefix = "channel"
)
type resolver struct {
}
// match with api4.getChannelsForTeamForUser
func (r *resolver) Channels(ctx context.Context, args struct {
TeamID string
UserID string
IncludeDeleted bool
LastDeleteAt float64
LastUpdateAt float64
First int32
After string
}) ([]*channel, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
if args.TeamID != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel string
var ok bool
if args.After != "" {
afterChannel, ok = parseChannelCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
// TODO: convert this to a streaming API.
channels, appErr := c.App.GetChannelsForTeamForUserWithCursor(c.AppContext, args.TeamID, args.UserID, &model.ChannelSearchOpts{
IncludeDeleted: args.IncludeDeleted,
LastDeleteAt: int(args.LastDeleteAt),
LastUpdateAt: int(args.LastUpdateAt),
PerPage: model.NewInt(limit),
}, afterChannel)
if appErr != nil {
return nil, appErr
}
appErr = c.App.FillInChannelsProps(c.AppContext, channels)
if appErr != nil {
return nil, appErr
}
return postProcessChannels(c, channels)
}
// match with api4.getUser
func (r *resolver) User(ctx context.Context, args struct{ ID string }) (*user, error) {
return getGraphQLUser(ctx, args.ID)
}
// match with api4.getClientConfig
func (r *resolver) Config(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.AppContext.Session().UserId == "" {
return c.App.Srv().Platform().LimitedClientConfigWithComputed(), nil
}
return c.App.Srv().Platform().ClientConfigWithComputed(), nil
}
// match with api4.getClientLicense
func (r *resolver) License(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
return c.App.Srv().ClientLicense(), nil
}
return c.App.Srv().GetSanitizedClientLicense(), nil
}
// match with api4.getTeamMembersForUser for teamID=""
// and api4.getTeamMember for teamID != ""
func (r *resolver) TeamMembers(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*teamMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return nil, c.Err
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, args.UserID)
if appErr != nil {
return nil, appErr
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
if args.TeamID != "" && !args.ExcludeTeam {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
tm, appErr2 := c.App.GetTeamMember(args.TeamID, args.UserID)
if appErr2 != nil {
return nil, appErr2
}
return []*teamMember{{*tm}}, nil
}
excludeTeamID := ""
if args.TeamID != "" && args.ExcludeTeam {
excludeTeamID = args.TeamID
}
// Do not return archived team members
members, appErr := c.App.GetTeamMembersForUser(args.UserID, excludeTeamID, false)
if appErr != nil {
return nil, appErr
}
// Convert to the wrapper format.
res := make([]*teamMember, 0, len(members))
for _, tm := range members {
res = append(res, &teamMember{*tm})
}
return res, nil
}
func (*resolver) ChannelsLeft(ctx context.Context, args struct {
UserID string
Since float64
}) ([]string, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
return c.App.Srv().Store().ChannelMemberHistory().GetChannelsLeftSince(args.UserID, int64(args.Since))
}
// match with api4.getChannelMember
func (*resolver) ChannelMembers(ctx context.Context, args struct {
UserID string
TeamID string
ChannelID string
ExcludeTeam bool
First int32
After string
LastUpdateAt float64
}) ([]*channelMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
// If it's a single channel
if args.ChannelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, appErr := c.App.GetChannelMember(ctx, args.ChannelID, args.UserID)
if appErr != nil {
return nil, appErr
}
return []*channelMember{{*member}}, nil
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel, afterUser string
var ok bool
if args.After != "" {
afterChannel, afterUser, ok = parseChannelMemberCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
if args.TeamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*channelMember{}, appErr
}
args.TeamID = team.Id
} else {
return []*channelMember{}, nil
}
}
}
opts := &store.ChannelMemberGraphQLSearchOpts{
AfterChannel: afterChannel,
AfterUser: afterUser,
Limit: limit,
LastUpdateAt: int(args.LastUpdateAt),
ExcludeTeam: args.ExcludeTeam,
}
members, err := c.App.Srv().Store().Channel().GetMembersForUserWithCursor(args.UserID, args.TeamID, opts)
if err != nil {
return nil, err
}
res := make([]*channelMember, 0, len(members))
for _, cm := range members {
res = append(res, &channelMember{cm})
}
return res, nil
}
// match with api4.getCategoriesForTeamForUser
func (*resolver) SidebarCategories(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*model.SidebarCategoryWithChannels, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
// Fallback to primary team logic
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*model.SidebarCategoryWithChannels{}, appErr
}
args.TeamID = team.Id
} else {
return []*model.SidebarCategoryWithChannels{}, nil
}
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
// If it's only for a single team.
var categories *model.OrderedSidebarCategories
var appErr *model.AppError
if !args.ExcludeTeam {
categories, appErr = c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, args.UserID, args.TeamID)
if appErr != nil {
return nil, appErr
}
} else {
opts := &store.SidebarCategorySearchOpts{
TeamID: args.TeamID,
ExcludeTeam: args.ExcludeTeam,
}
categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts)
if appErr != nil {
return nil, appErr
}
}
// TODO: look into optimizing this.
// create map
orderMap := make(map[string]*model.SidebarCategoryWithChannels, len(categories.Categories))
for _, category := range categories.Categories {
orderMap[category.Id] = category
}
// create a new slice based on the order
res := make([]*model.SidebarCategoryWithChannels, 0, len(categories.Categories))
for _, categoryId := range categories.Order {
res = append(res, orderMap[categoryId])
}
return res, nil
}
// getCtx extracts web.Context out of the usual request context.
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
// so we use it for now.
func getCtx(ctx context.Context) (*web.Context, error) {
c, ok := ctx.Value(webCtx).(*web.Context)
if !ok {
return nil, errors.New("no web.Context found in context")
}
return c, nil
}
// getRolesLoader returns the roles loader out of the context.
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getChannelsLoader returns the channels loader out of the context.
func getChannelsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(channelsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getTeamsLoader returns the teams loader out of the context.
func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(teamsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getUsersLoader returns the users loader out of the context.
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channel is an internal graphQL wrapper struct to add resolver methods.
type channel struct {
model.Channel
PrettyDisplayName string
}
// match with api4.getTeam
func (ch *channel) Team(ctx context.Context) (*model.Team, error) {
if ch.TeamId == "" {
return nil, nil
}
return getGraphQLTeam(ctx, ch.TeamId)
}
func (ch *channel) Cursor() *string {
cursor := string(channelCursorPrefix) + "-" + ch.Id
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func parseChannelCursor(cursor string) (channelID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", false
}
prefix, id, found := strings.Cut(string(decoded), "-")
if !found {
return "", false
}
if cursorPrefix(prefix) != channelCursorPrefix {
return "", false
}
return id, true
}
func postProcessChannels(c *web.Context, channels []*model.Channel) ([]*channel, error) {
// This approach becomes effectively similar to a dataloader if the displayName computation
// were to be done at the field level per channel.
// Get DM/GM channelIDs and set empty maps as well.
var channelIDs []string
for _, ch := range channels {
if ch.IsGroupOrDirect() {
channelIDs = append(channelIDs, ch.Id)
}
// This is needed to avoid sending null, which
// does not match with the schema since props is not nullable.
// And making it nullable would mean taking pointer of a map,
// which is not very idiomatic.
ch.MakeNonNil()
}
var nameFormat string
var userInfo map[string][]*model.User
var err error
// Avoiding unnecessary queries unless necessary.
if len(channelIDs) > 0 {
userInfo, err = c.App.Srv().Store().Channel().GetMembersInfoByChannelIds(channelIDs)
if err != nil {
return nil, err
}
user := &model.User{Id: c.AppContext.Session().UserId}
nameFormat = c.App.GetNotificationNameFormat(user)
}
// Convert to the wrapper format.
nameCache := make(map[string]string)
res := make([]*channel, len(channels))
for i, ch := range channels {
prettyName := ch.DisplayName
if ch.IsGroupOrDirect() {
// get users slice for channel id
users := userInfo[ch.Id]
if users == nil {
return nil, fmt.Errorf("user info not found for channel id: %s", ch.Id)
}
prettyName = getPrettyDNForUsers(nameFormat, users, c.AppContext.Session().UserId, nameCache)
}
res[i] = &channel{Channel: *ch, PrettyDisplayName: prettyName}
}
return res, nil
}
func getPrettyDNForUsers(displaySetting string, users []*model.User, omitUserId string, cache map[string]string) string {
displayNames := make([]string, 0, len(users))
for _, u := range users {
if u.Id == omitUserId {
continue
}
displayNames = append(displayNames, getPrettyDNForUser(displaySetting, u, cache))
}
sort.Strings(displayNames)
result := strings.Join(displayNames, ", ")
if result == "" {
// Self DM
result = getPrettyDNForUser(displaySetting, users[0], cache)
}
return result
}
func getPrettyDNForUser(displaySetting string, user *model.User, cache map[string]string) string {
// use the cache first
if name, ok := cache[user.Id]; ok {
return name
}
var displayName string
switch displaySetting {
case "nickname_full_name":
displayName = user.Nickname
if strings.TrimSpace(displayName) == "" {
displayName = user.GetFullName()
}
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
case "full_name":
displayName = user.GetFullName()
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
default: // the "username" case also falls under this one.
displayName = user.Username
}
// update the cache
cache[user.Id] = displayName
return displayName
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channelMember is an internal graphQL wrapper struct to add resolver methods.
type channelMember struct {
model.ChannelMember
}
// match with api4.getUser
func (cm *channelMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, cm.UserId)
}
// match with api4.Channel
func (cm *channelMember) Channel(ctx context.Context) (*channel, error) {
loader, err := getChannelsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(cm.ChannelId))
result, err := thunk()
if err != nil {
return nil, err
}
channel := result.(*channel)
return channel, nil
}
func graphQLChannelsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
channels, err := getGraphQLChannels(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range channels {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLChannels(c *web.Context, channelIDs []string) ([]*channel, error) {
channels, appErr := c.App.GetChannels(c.AppContext, channelIDs)
if appErr != nil {
return nil, appErr
}
if len(channels) != len(channelIDs) {
return nil, fmt.Errorf("all channels were not found. Requested %d; Found %d", len(channelIDs), len(channels))
}
var openChannels, nonOpenChannels, teamsForOpenChannels []string
uniqueTeams := make(map[string]bool)
for _, ch := range channels {
if ch.Type == model.ChannelTypeOpen {
openChannels = append(openChannels, ch.Id)
uniqueTeams[ch.TeamId] = true
} else {
nonOpenChannels = append(nonOpenChannels, ch.Id)
}
}
for teamID := range uniqueTeams {
teamsForOpenChannels = append(teamsForOpenChannels, teamID)
}
if len(openChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), openChannels, model.PermissionReadChannel) &&
!c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsForOpenChannels, model.PermissionReadPublicChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return nil, c.Err
}
if len(nonOpenChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), nonOpenChannels, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
appErr = c.App.FillInChannelsProps(c.AppContext, model.ChannelList(channels))
if appErr != nil {
return nil, appErr
}
res, err := postProcessChannels(c, channels)
if err != nil {
return nil, err
}
// The channels need to be in the exact same order as the input slice.
tmp := make(map[string]*channel)
for _, ch := range res {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the channels.
for i, id := range channelIDs {
res[i] = tmp[id]
}
return res, nil
}
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(cm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
func (cm *channelMember) Cursor() *string {
cursor := string(channelMemberCursorPrefix) + "-" + cm.ChannelId + "-" + cm.UserId
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func graphQLRolesLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
roles, err := getGraphQLRoles(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, role := range roles {
result[i] = &dataloader.Result{Data: role}
}
return result
}
func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error) {
cleanedRoleNames, valid := model.CleanRoleNames(roleNames)
if !valid {
c.SetInvalidParam("rolename")
return nil, c.Err
}
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
if appErr != nil {
return nil, appErr
}
// The roles need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Role)
for _, r := range roles {
tmp[r.Name] = r
}
// We reuse the same slice and just rewrite the roles.
for i, roleName := range roleNames {
roles[i] = tmp[roleName]
}
return roles, nil
}
func parseChannelMemberCursor(cursor string) (channelID, userID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", "", false
}
parts := strings.Split(string(decoded), "-")
if len(parts) != 3 {
return "", "", false
}
if cursorPrefix(parts[0]) != channelMemberCursorPrefix {
return "", "", false
}
return parts[1], parts[2], true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
func getGraphQLTeam(ctx context.Context, id string) (*model.Team, error) {
loader, err := getTeamsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
team := result.(*model.Team)
return team, nil
}
func graphQLTeamsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
teams, err := getGraphQLTeams(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range teams {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLTeams(c *web.Context, teamIDs []string) ([]*model.Team, error) {
teams, appErr := c.App.GetTeams(teamIDs)
if appErr != nil {
return nil, appErr
}
if len(teams) != len(teamIDs) {
return nil, fmt.Errorf("all teams were not found. Requested %d; Found %d", len(teamIDs), len(teams))
}
var teamsToCheck []string
for _, team := range teams {
if !team.AllowOpenInvite || team.Type != model.TeamOpen {
teamsToCheck = append(teamsToCheck, team.Id)
}
}
if !c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsToCheck, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
for i, team := range teams {
teams[i] = c.App.SanitizeTeam(*c.AppContext.Session(), team)
}
// The teams need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Team, len(teams))
for _, ch := range teams {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the teams.
for i, id := range teamIDs {
teams[i] = tmp[id]
}
return teams, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
)
// teamMember is an internal graphQL wrapper struct to add resolver methods.
type teamMember struct {
model.TeamMember
}
// match with api4.getTeam
func (tm *teamMember) Team(ctx context.Context) (*model.Team, error) {
return getGraphQLTeam(ctx, tm.TeamId)
}
// match with api4.getUser
func (tm *teamMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, tm.UserId)
}
// match with api4.getRolesByNames
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(tm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"net/http"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// user is an internal graphQL wrapper struct to add resolver methods.
type user struct {
model.User
}
// match with api4.getUser
func getGraphQLUser(ctx context.Context, id string) (*user, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if id == model.Me {
id = c.AppContext.Session().UserId
}
if !model.IsValidId(id) {
return nil, web.NewInvalidParamError("user_id")
}
loader, err := getUsersLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
usr := result.(*model.User)
if c.IsSystemAdmin() || c.AppContext.Session().UserId == usr.Id {
userTermsOfService, appErr := c.App.GetUserTermsOfService(usr.Id)
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
return nil, appErr
}
if userTermsOfService != nil {
usr.TermsOfServiceId = userTermsOfService.TermsOfServiceId
usr.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
return &user{*usr}, nil
}
// match with api4.getRolesByNames
func (u *user) Roles(ctx context.Context) ([]*model.Role, error) {
roleNames := u.GetRoles()
if len(roleNames) == 0 {
return nil, nil
}
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(roleNames))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
// match with api4.getPreferences
func (u *user) Preferences(ctx context.Context) ([]model.Preference, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), u.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
preferences, appErr := c.App.GetPreferencesForUser(u.Id)
if appErr != nil {
return nil, appErr
}
return preferences, nil
}
// match with api4.getUserStatus
func (u *user) Status(ctx context.Context) (*model.Status, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
statuses, appErr := c.App.GetUserStatusesByIds([]string{u.Id})
if appErr != nil {
return nil, appErr
}
if len(statuses) == 0 {
return nil, model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
}
return statuses[0], nil
}
// match with api4.getSessions
func (u *user) Sessions(ctx context.Context) ([]*model.Session, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), u.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
sessions, appErr := c.App.GetSessions(u.Id)
if appErr != nil {
return nil, appErr
}
for _, session := range sessions {
session.Sanitize()
}
return sessions, nil
}
func graphQLUsersLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
users, err := getGraphQLUsers(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, user := range users {
result[i] = &dataloader.Result{Data: user}
}
return result
}
func getGraphQLUsers(c *web.Context, userIDs []string) ([]*model.User, error) {
// Usually this will be called only for one user
// and cached for the rest of the query. So it's not an issue
// to run this in a loop.
for _, id := range userIDs {
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
if appErr != nil || !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
}
users, appErr := c.App.GetUsers(userIDs)
if appErr != nil {
return nil, appErr
}
// Same as earlier, we want to pre-compute this only once
// because otherwise the resolvers run in multiple goroutines
// and *User.Sanitize causes a race, and we want to avoid
// deep-copying every user in all goroutines.
for _, user := range users {
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
}
// The users need to be in the exact same order as the input slice.
tmp := make(map[string]*model.User)
for _, u := range users {
tmp[u.Id] = u
}
// We reuse the same slice and just rewrite the roles.
for i, uID := range userIDs {
users[i] = tmp[uID]
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var notAllowedPermissions = []string{
model.PermissionSysconsoleWriteUserManagementSystemRoles.Id,
model.PermissionSysconsoleReadUserManagementSystemRoles.Id,
model.PermissionManageRoles.Id,
}
func (api *API) InitRole() {
api.BaseRoutes.Roles.Handle("", api.APISessionRequired(getAllRoles)).Methods("GET")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getRole)).Methods("GET")
api.BaseRoutes.Roles.Handle("/name/{role_name:[a-z0-9_]+}", api.APISessionRequiredTrustRequester(getRoleByName)).Methods("GET")
api.BaseRoutes.Roles.Handle("/names", api.APISessionRequiredTrustRequester(getRolesByNames)).Methods("POST")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchRole)).Methods("PUT")
}
func getAllRoles(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
roles, appErr := c.App.GetAllRoles()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(roles)
if err != nil {
c.Err = model.NewAppError("getAllRoles", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getRole(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleId()
if c.Err != nil {
return
}
role, err := c.App.GetRole(c.Params.RoleId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleName()
if c.Err != nil {
return
}
role, err := c.App.GetRoleByName(r.Context(), c.Params.RoleName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
rolenames := model.ArrayFromJSON(r.Body)
if len(rolenames) == 0 {
c.SetInvalidParam("rolenames")
return
}
cleanedRoleNames, valid := model.CleanRoleNames(rolenames)
if !valid {
c.SetInvalidParam("rolename")
return
}
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(roles)
if err != nil {
c.Err = model.NewAppError("getRolesByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func patchRole(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleId()
if c.Err != nil {
return
}
var patch model.RolePatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
c.SetInvalidParamWithErr("role", err)
return
}
auditRec := c.MakeAuditRecord("patchRole", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "role_patch", &patch)
defer c.LogAuditRec(auditRec)
oldRole, appErr := c.App.GetRole(c.Params.RoleId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(oldRole)
auditRec.AddEventObjectType("role")
// manage_system permission is required to patch system_admin
requiredPermission := model.PermissionSysconsoleWriteUserManagementPermissions
specialProtectedSystemRoles := append(model.NewSystemRoleIDs, model.SystemAdminRoleId)
for _, roleID := range specialProtectedSystemRoles {
if oldRole.Name == roleID {
requiredPermission = model.PermissionManageSystem
}
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), requiredPermission) {
c.SetPermissionError(requiredPermission)
return
}
isGuest := oldRole.Name == model.SystemGuestRoleId || oldRole.Name == model.TeamGuestRoleId || oldRole.Name == model.ChannelGuestRoleId
if c.App.Channels().License() == nil && patch.Permissions != nil {
if isGuest {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
return
}
}
// Licensed instances can not change permissions in the blacklist set.
if patch.Permissions != nil {
deltaPermissions := model.PermissionsChangedByPatch(oldRole, &patch)
for _, permission := range deltaPermissions {
notAllowed := false
for _, notAllowedPermission := range notAllowedPermissions {
if permission == notAllowedPermission {
notAllowed = true
}
}
if notAllowed {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.not_allowed_permission.error", nil, "Cannot add or remove permission: "+permission, http.StatusNotImplemented)
return
}
}
*patch.Permissions = model.RemoveDuplicateStrings(*patch.Permissions)
}
if c.App.Channels().License() != nil && isGuest && !*c.App.Channels().License().Features.GuestAccountsPermissions {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
return
}
if oldRole.Name == model.TeamAdminRoleId ||
oldRole.Name == model.ChannelAdminRoleId ||
oldRole.Name == model.SystemUserRoleId ||
oldRole.Name == model.TeamUserRoleId ||
oldRole.Name == model.ChannelUserRoleId ||
oldRole.Name == model.SystemGuestRoleId ||
oldRole.Name == model.TeamGuestRoleId ||
oldRole.Name == model.ChannelGuestRoleId ||
oldRole.Name == model.PlaybookAdminRoleId ||
oldRole.Name == model.PlaybookMemberRoleId ||
oldRole.Name == model.RunAdminRoleId ||
oldRole.Name == model.RunMemberRoleId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementSystemRoles) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementSystemRoles)
return
}
}
role, appErr := c.App.PatchRole(oldRole, &patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(role)
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitRoleLocal() {
api.BaseRoutes.Roles.Handle("", api.APILocal(getAllRoles)).Methods("GET")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}", api.APILocal(getRole)).Methods("GET")
api.BaseRoutes.Roles.Handle("/name/{role_name:[a-z0-9_]+}", api.APILocal(getRoleByName)).Methods("GET")
api.BaseRoutes.Roles.Handle("/names", api.APILocal(getRolesByNames)).Methods("POST")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}/patch", api.APILocal(patchRole)).Methods("PUT")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitSaml() {
api.BaseRoutes.SAML.Handle("/metadata", api.APIHandler(getSamlMetadata)).Methods("GET")
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(addSamlPublicCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(addSamlPrivateCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(addSamlIdpCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(removeSamlPublicCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(removeSamlPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(removeSamlIdpCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/status", api.APISessionRequired(getSamlCertificateStatus)).Methods("GET")
api.BaseRoutes.SAML.Handle("/metadatafromidp", api.APIHandler(getSamlMetadataFromIdp)).Methods("POST")
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APISessionRequired(resetAuthDataToEmail)).Methods("POST")
}
func (api *API) InitSamlLocal() {
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APILocal(resetAuthDataToEmail)).Methods("POST")
}
func getSamlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
metadata, err := c.App.GetSamlMetadata()
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Disposition", "attachment; filename=\"metadata.xml\"")
w.Write([]byte(metadata))
}
func parseSamlCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, err.Error(), http.StatusBadRequest)
}
m := r.MultipartForm
fileArray, ok := m.File["certificate"]
if !ok {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
}
if len(fileArray) <= 0 {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
}
return fileArray[0], nil
}
func addSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPublicCert) {
c.SetPermissionError(model.PermissionAddSamlPublicCert)
return
}
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addSamlPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlPublicCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPrivateCert) {
c.SetPermissionError(model.PermissionAddSamlPrivateCert)
return
}
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addSamlPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlPrivateCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlIdpCert) {
c.SetPermissionError(model.PermissionAddSamlIdpCert)
return
}
v := r.Header.Get("Content-Type")
if v == "" {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
d, _, err := mime.ParseMediaType(v)
if err != nil {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addSamlIdpCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("type", d)
if d == "application/x-pem-file" {
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := c.App.SetSamlIdpCertificateFromMetadata(body); err != nil {
c.Err = err
return
}
} else if d == "multipart/form-data" {
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlIdpCertificate(fileData); err != nil {
c.Err = err
return
}
} else {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPublicCert) {
c.SetPermissionError(model.PermissionRemoveSamlPublicCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlPublicCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPrivateCert) {
c.SetPermissionError(model.PermissionRemoveSamlPrivateCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlPrivateCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlIdpCert) {
c.SetPermissionError(model.PermissionRemoveSamlIdpCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlIdpCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlIdpCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getSamlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlCertStatus) {
c.SetPermissionError(model.PermissionGetSamlCertStatus)
return
}
status := c.App.GetSamlCertificateStatus()
if err := json.NewEncoder(w).Encode(status); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getSamlMetadataFromIdp(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlMetadataFromIdp) {
c.SetPermissionError(model.PermissionGetSamlMetadataFromIdp)
return
}
props := model.MapFromJSON(r.Body)
url := props["saml_metadata_url"]
if url == "" {
c.SetInvalidParam("saml_metadata_url")
return
}
metadata, err := c.App.GetSamlMetadataFromIdp(url)
if err != nil {
c.Err = model.NewAppError("getSamlMetadataFromIdp", "api.admin.saml.failure_get_metadata_from_idp.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(metadata); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func resetAuthDataToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
type ResetAuthDataParams struct {
IncludeDeleted bool `json:"include_deleted"`
DryRun bool `json:"dry_run"`
SpecifiedUserIDs []string `json:"user_ids"`
}
var params *ResetAuthDataParams
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
if jsonErr != nil {
c.Err = model.NewAppError("resetAuthDataToEmail", "model.utils.decode_json.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
numAffected, appErr := c.App.ResetSamlAuthDataToEmail(params.IncludeDeleted, params.DryRun, params.SpecifiedUserIDs)
if appErr != nil {
c.Err = appErr
return
}
n := struct {
NumAffected int `json:"num_affected"`
}{
NumAffected: numAffected,
}
if err := json.NewEncoder(w).Encode(n); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitScheme() {
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(getSchemes)).Methods("GET")
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(createScheme)).Methods("POST")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteScheme)).Methods("DELETE")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getScheme)).Methods("GET")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchScheme)).Methods("PUT")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/teams", api.APISessionRequiredTrustRequester(getTeamsForScheme)).Methods("GET")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/channels", api.APISessionRequiredTrustRequester(getChannelsForScheme)).Methods("GET")
}
func createScheme(c *Context, w http.ResponseWriter, r *http.Request) {
var scheme model.Scheme
if jsonErr := json.NewDecoder(r.Body).Decode(&scheme); jsonErr != nil {
c.SetInvalidParamWithErr("scheme", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createScheme", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "scheme", &scheme)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.CreateScheme", "api.scheme.create_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
returnedScheme, err := c.App.CreateScheme(&scheme)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(returnedScheme)
auditRec.AddEventObjectType("scheme")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(returnedScheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
return
}
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(scheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getSchemes(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
return
}
scope := c.Params.Scope
if scope != "" && scope != model.SchemeScopeTeam && scope != model.SchemeScopeChannel {
c.SetInvalidParam("scope")
return
}
schemes, appErr := c.App.GetSchemesPage(c.Params.Scope, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(schemes)
if err != nil {
c.Err = model.NewAppError("getSchemes", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementTeams) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementTeams)
return
}
scheme, appErr := c.App.GetScheme(c.Params.SchemeId)
if appErr != nil {
c.Err = appErr
return
}
if scheme.Scope != model.SchemeScopeTeam {
c.Err = model.NewAppError("Api4.GetTeamsForScheme", "api.scheme.get_teams_for_scheme.scope.error", nil, "", http.StatusBadRequest)
return
}
teams, appErr := c.App.GetTeamsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("getTeamsForScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getChannelsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
if scheme.Scope != model.SchemeScopeChannel {
c.Err = model.NewAppError("Api4.GetChannelsForScheme", "api.scheme.get_channels_for_scheme.scope.error", nil, "", http.StatusBadRequest)
return
}
channels, err := c.App.GetChannelsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
var patch model.SchemePatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("scheme", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchScheme", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "scheme_patch", &patch)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.PatchScheme", "api.scheme.patch_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(scheme)
auditRec.AddEventObjectType("scheme")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
scheme, err = c.App.PatchScheme(scheme, &patch)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(scheme)
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(scheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteScheme", audit.Fail)
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.DeleteScheme", "api.scheme.delete_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
scheme, err := c.App.DeleteScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(scheme)
auditRec.AddEventObjectType("scheme")
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitSharedChannels() {
api.BaseRoutes.SharedChannels.Handle("/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getSharedChannels)).Methods("GET")
api.BaseRoutes.SharedChannels.Handle("/remote_info/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteClusterInfo)).Methods("GET")
}
func getSharedChannels(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
// make sure user has access to the team.
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
opts := model.SharedChannelFilterOpts{
TeamId: c.Params.TeamId,
}
// only return channels the user is a member of, unless they are a shared channels manager.
if !c.App.HasPermissionTo(c.AppContext.Session().UserId, model.PermissionManageSharedChannels) {
opts.MemberId = c.AppContext.Session().UserId
}
channels, appErr := c.App.GetSharedChannels(c.Params.Page, c.Params.PerPage, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channels)
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
func getRemoteClusterInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
// GetRemoteClusterForUser will only return a remote if the user is a member of at
// least one channel shared by the remote. All other cases return error.
rc, appErr := c.App.GetRemoteClusterForUser(c.Params.RemoteId, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
remoteInfo := rc.ToRemoteClusterInfo()
b, err := json.Marshal(remoteInfo)
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitStatus() {
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(getUserStatus)).Methods("GET")
api.BaseRoutes.Users.Handle("/status/ids", api.APISessionRequired(getUserStatusesByIds)).Methods("POST")
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(updateUserStatus)).Methods("PUT")
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(updateUserCustomStatus)).Methods("PUT")
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(removeUserCustomStatus)).Methods("DELETE")
// Both these handlers are for removing the recent custom status but the one with the POST method should be preferred
// as DELETE method doesn't support request body in the mobile app.
api.BaseRoutes.User.Handle("/status/custom/recent", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("DELETE")
api.BaseRoutes.User.Handle("/status/custom/recent/delete", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("POST")
}
func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
// No permission check required
statusMap, err := c.App.GetUserStatusesByIds([]string{c.Params.UserId})
if err != nil {
c.Err = err
return
}
if len(statusMap) == 0 {
c.Err = model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(statusMap[0]); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
for _, userId := range userIds {
if len(userId) != 26 {
c.SetInvalidParam("user_ids")
return
}
}
// No permission check required
statuses, appErr := c.App.GetUserStatusesByIds(userIds)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(statuses)
if err != nil {
c.Err = model.NewAppError("getUserStatusesByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var status model.Status
if jsonErr := json.NewDecoder(r.Body).Decode(&status); jsonErr != nil {
c.SetInvalidParamWithErr("status", jsonErr)
return
}
// The user being updated in the payload must be the same one as indicated in the URL.
if status.UserId != c.Params.UserId {
c.SetInvalidParam("user_id")
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
currentStatus, err := c.App.GetStatus(c.Params.UserId)
if err == nil && currentStatus.Status == model.StatusOutOfOffice && status.Status != model.StatusOutOfOffice {
c.App.DisableAutoResponder(c.AppContext, c.Params.UserId, c.IsSystemAdmin())
}
switch status.Status {
case "online":
c.App.SetStatusOnline(c.Params.UserId, true)
case "offline":
c.App.SetStatusOffline(c.Params.UserId, true)
case "away":
c.App.SetStatusAwayIfNeeded(c.Params.UserId, true)
case "dnd":
c.App.SetStatusDoNotDisturbTimed(c.Params.UserId, status.DNDEndTime)
default:
c.SetInvalidParam("status")
return
}
getUserStatus(c, w, r)
}
func updateUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("updateUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
var customStatus model.CustomStatus
jsonErr := json.NewDecoder(r.Body).Decode(&customStatus)
if jsonErr != nil || (customStatus.Emoji == "" && customStatus.Text == "") || !customStatus.AreDurationAndExpirationTimeValid() {
c.SetInvalidParamWithErr("custom_status", jsonErr)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
customStatus.PreSave()
err := c.App.SetCustomStatus(c.AppContext, c.Params.UserId, &customStatus)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func removeUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("removeUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RemoveCustomStatus(c.AppContext, c.Params.UserId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func removeUserRecentCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("removeUserRecentCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
var recentCustomStatus model.CustomStatus
if jsonErr := json.NewDecoder(r.Body).Decode(&recentCustomStatus); jsonErr != nil {
c.SetInvalidParamWithErr("recent_custom_status", jsonErr)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RemoveRecentCustomStatus(c.Params.UserId, &recentCustomStatus); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"reflect"
"runtime"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/upgrader"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
const (
RedirectLocationCacheSize = 10000
DefaultServerBusySeconds = 3600
MaxServerBusySeconds = 86400
)
var redirectLocationDataCache = cache.NewLRU(cache.LRUOptions{
Size: RedirectLocationCacheSize,
})
func (api *API) InitSystem() {
api.BaseRoutes.System.Handle("/ping", api.APIHandler(getSystemPing)).Methods("GET")
api.BaseRoutes.System.Handle("/timezones", api.APISessionRequired(getSupportedTimezones)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/audits", api.APISessionRequired(getAudits)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/email/test", api.APISessionRequired(testEmail)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/site_url/test", api.APISessionRequired(testSiteURL)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/file/s3_test", api.APISessionRequired(testS3)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/database/recycle", api.APISessionRequired(databaseRecycle)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/caches/invalidate", api.APISessionRequired(invalidateCaches)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APISessionRequired(getLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs/query", api.APISessionRequired(queryLogs)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APIHandler(postLog)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/analytics/old", api.APISessionRequired(getAnalytics)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/latest_version", api.APISessionRequired(getLatestVersion)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/redirect_location", api.APISessionRequiredTrustRequester(getRedirectLocation)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/notifications/ack", api.APISessionRequired(pushNotificationAck)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(setServerBusy)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(clearServerBusy)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/upgrade_to_enterprise", api.APISessionRequired(upgradeToEnterprise)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/upgrade_to_enterprise/status", api.APISessionRequired(upgradeToEnterpriseStatus)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/restart", api.APISessionRequired(restart)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/status", api.APISessionRequired(getWarnMetricsStatus)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.APIHandler(sendWarnMetricAckEmail)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/trial-license-ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.APIHandler(requestTrialLicenseAndAckWarnMetric)).Methods("POST")
api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getProductNotices)).Methods("GET")
api.BaseRoutes.System.Handle("/notices/view", api.APISessionRequired(updateViewedProductNotices)).Methods("PUT")
api.BaseRoutes.System.Handle("/support_packet", api.APISessionRequired(generateSupportPacket)).Methods("GET")
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(getOnboarding)).Methods("GET")
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(completeOnboarding)).Methods("POST")
api.BaseRoutes.System.Handle("/schema/version", api.APISessionRequired(getAppliedSchemaMigrations)).Methods("GET")
}
func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
const FileMime = "application/zip"
const OutputDirectory = "support_packet"
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("generateSupportPacket", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
// Support packet generation is limited to system admins (MM-42271).
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// Checking to see if the server has a e10 or e20 license (this feature is only permitted for servers with licenses)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden)
return
}
fileDatas := c.App.GenerateSupportPacket()
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
now := time.Now()
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))
fileStorageBackend := c.App.FileBackend()
// We do this incase we get concurrent requests, we will always have a unique directory.
// This is to avoid the situation where we try to write to the same directory while we are trying to delete it (further down)
outputDirectoryToUse := OutputDirectory + "_" + model.NewId()
err := c.App.CreateZipFileAndAddFiles(fileStorageBackend, fileDatas, outputZipFilename, outputDirectoryToUse)
if err != nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_create_zip_file", nil, err.Error(), http.StatusForbidden)
return
}
fileBytes, err := fileStorageBackend.ReadFile(path.Join(outputDirectoryToUse, outputZipFilename))
defer fileStorageBackend.RemoveDirectory(outputDirectoryToUse)
if err != nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_read_file_from_backend", nil, err.Error(), http.StatusForbidden)
return
}
fileBytesReader := bytes.NewReader(fileBytes)
// Send the zip file back to client
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
// already sets that for us
web.WriteFileResponse(outputZipFilename, FileMime, 0, now, *c.App.Config().ServiceSettings.WebserverMode, fileBytesReader, true, w, r)
}
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
reqs := c.App.Config().ClientRequirements
s := make(map[string]string)
s[model.STATUS] = model.StatusOk
s["AndroidLatestVersion"] = reqs.AndroidLatestVersion
s["AndroidMinVersion"] = reqs.AndroidMinVersion
s["IosLatestVersion"] = reqs.IosLatestVersion
s["IosMinVersion"] = reqs.IosMinVersion
testflag := c.App.Config().FeatureFlags.TestFeature
if testflag != "off" {
s["TestFeatureFlag"] = testflag
}
actualGoroutines := runtime.NumGoroutine()
if *c.App.Config().ServiceSettings.GoroutineHealthThreshold > 0 && actualGoroutines >= *c.App.Config().ServiceSettings.GoroutineHealthThreshold {
mlog.Warn("The number of running goroutines is over the health threshold", mlog.Int("goroutines", actualGoroutines), mlog.Int("health_threshold", *c.App.Config().ServiceSettings.GoroutineHealthThreshold))
s[model.STATUS] = model.StatusUnhealthy
}
// Enhanced ping health check:
// If an extra form value is provided then perform extra health checks for
// database and file storage backends.
if r.FormValue("get_server_status") != "" {
dbStatusKey := "database_status"
s[dbStatusKey] = model.StatusOk
writeErr := c.App.DBHealthCheckWrite()
if writeErr != nil {
mlog.Warn("Unable to write to database.", mlog.Err(writeErr))
s[dbStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
writeErr = c.App.DBHealthCheckDelete()
if writeErr != nil {
mlog.Warn("Unable to remove ping health check value from database.", mlog.Err(writeErr))
s[dbStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
if s[dbStatusKey] == model.StatusOk {
mlog.Debug("Able to write to database.")
}
filestoreStatusKey := "filestore_status"
s[filestoreStatusKey] = model.StatusOk
appErr := c.App.TestFileStoreConnection()
if appErr != nil {
s[filestoreStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
w.Header().Set(model.STATUS, s[model.STATUS])
w.Header().Set(dbStatusKey, s[dbStatusKey])
w.Header().Set(filestoreStatusKey, s[filestoreStatusKey])
}
if deviceID := r.FormValue("device_id"); deviceID != "" {
s["CanReceiveNotifications"] = c.App.SendTestPushNotification(deviceID)
}
if s[model.STATUS] != model.StatusOk {
w.WriteHeader(http.StatusInternalServerError)
}
w.Write([]byte(model.MapToJSON(s)))
}
func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding the config", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
if checkHasNilFields(&cfg.EmailSettings) {
c.Err = model.NewAppError("testEmail", "api.file.test_connection_email_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestEmail) {
c.SetPermissionError(model.PermissionTestEmail)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testEmail", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.TestEmail(c.AppContext.Session().UserId, cfg)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func testSiteURL(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestSiteURL) {
c.SetPermissionError(model.PermissionTestSiteURL)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testSiteURL", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
props := model.MapFromJSON(r.Body)
siteURL := props["site_url"]
if siteURL == "" {
c.SetInvalidParam("site_url")
return
}
appErr := c.App.TestSiteURL(siteURL)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getAudits(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getAudits", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadAudits) {
c.SetPermissionError(model.PermissionReadAudits)
return
}
audits, appErr := c.App.GetAuditsPage("", c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
audit.AddEventParameter(auditRec, "page", c.Params.Page)
audit.AddEventParameter(auditRec, "audits_per_page", c.Params.LogsPerPage)
if err := json.NewEncoder(w).Encode(audits); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func databaseRecycle(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRecycleDatabaseConnections) {
c.SetPermissionError(model.PermissionRecycleDatabaseConnections)
return
}
auditRec := c.MakeAuditRecord("databaseRecycle", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("databaseRecycle", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
c.App.RecycleDatabaseConnection()
auditRec.Success()
ReturnStatusOK(w)
}
func invalidateCaches(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionInvalidateCaches) {
c.SetPermissionError(model.PermissionInvalidateCaches)
return
}
auditRec := c.MakeAuditRecord("invalidateCaches", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("invalidateCaches", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.Srv().InvalidateAllCaches()
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func queryLogs(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("queryLogs", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("queryLogs", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetLogs) {
c.SetPermissionError(model.PermissionGetLogs)
return
}
var logFilter *model.LogFilter
err := json.NewDecoder(r.Body).Decode(&logFilter)
if err != nil {
c.Err = model.NewAppError("queryLogs", "api.system.logs.invalidFilter", nil, "", http.StatusInternalServerError)
return
}
logs, logerr := c.App.QueryLogs(c.Params.Page, c.Params.LogsPerPage, logFilter)
if logerr != nil {
c.Err = logerr
return
}
logsJSON := make(map[string][]interface{})
var result interface{}
for node, logLines := range logs {
for _, log := range logLines {
err2 := json.Unmarshal([]byte(log), &result)
if err2 == nil {
logsJSON[node] = append(logsJSON[node], result)
} else {
mlog.Warn("Error parsing log line in Server Logs")
}
}
}
auditRec.AddMeta("page", c.Params.Page)
auditRec.AddMeta("logs_per_page", c.Params.LogsPerPage)
w.Write(model.ToJSON(logsJSON))
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getLogs", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("getLogs", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetLogs) {
c.SetPermissionError(model.PermissionGetLogs)
return
}
lines, appErr := c.App.GetLogs(c.Params.Page, c.Params.LogsPerPage)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameter(auditRec, "page", c.Params.Page)
audit.AddEventParameter(auditRec, "logs_per_page", c.Params.LogsPerPage)
w.Write([]byte(model.ArrayToJSON(lines)))
}
func postLog(c *Context, w http.ResponseWriter, r *http.Request) {
forceToDebug := false
if !*c.App.Config().ServiceSettings.EnableDeveloper {
if c.AppContext.Session().UserId == "" {
c.Err = model.NewAppError("postLog", "api.context.permissions.app_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
forceToDebug = true
}
}
var m map[string]string
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
c.Logger.Warn("Error decoding request.", mlog.Err(err))
}
if m == nil {
m = map[string]string{}
}
lvl := m["level"]
msg := m["message"]
if len(msg) > 400 {
msg = msg[0:399]
}
msg = "Client Logs API Endpoint Message: " + msg
fields := []mlog.Field{
mlog.String("type", "client_message"),
mlog.String("user_agent", c.AppContext.UserAgent()),
}
if !forceToDebug && lvl == "ERROR" {
mlog.Error(msg, fields...)
} else {
mlog.Debug(msg, fields...)
}
m["message"] = msg
err = json.NewEncoder(w).Encode(m)
if err != nil {
c.Logger.Warn("Error while writing response.", mlog.Err(err))
}
}
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
teamId := r.URL.Query().Get("team_id")
if name == "" {
name = "standard"
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetAnalytics) {
c.SetPermissionError(model.PermissionGetAnalytics)
return
}
rows, appErr := c.App.GetAnalytics(name, teamId)
if appErr != nil {
c.Err = appErr
return
}
if rows == nil {
c.SetInvalidParam("name")
return
}
if err := json.NewEncoder(w).Encode(rows); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getLatestVersion(c *Context, w http.ResponseWriter, r *http.Request) {
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("latestVersion", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
resp, appErr := c.App.GetLatestVersion("https://api.github.com/repos/mattermost/mattermost-server/releases/latest")
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(resp)
if err != nil {
c.Logger.Warn("Unable to marshal JSON for latest version.", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
}
w.Write(b)
}
func getSupportedTimezones(c *Context, w http.ResponseWriter, r *http.Request) {
supportedTimezones := c.App.Timezones().GetSupported()
if supportedTimezones == nil {
supportedTimezones = make([]string, 0)
}
b, err := json.Marshal(supportedTimezones)
if err != nil {
c.Logger.Warn("Unable to marshal JSON in timezones.", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
}
w.Write(b)
}
func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding the config", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
if checkHasNilFields(&cfg.FileSettings) {
c.Err = model.NewAppError("testS3", "api.file.test_connection_s3_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestS3) {
c.SetPermissionError(model.PermissionTestS3)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testS3", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.CheckMandatoryS3Fields(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr
return
}
if *cfg.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
cfg.FileSettings.AmazonS3SecretAccessKey = c.App.Config().FileSettings.AmazonS3SecretAccessKey
}
appErr = c.App.TestFileStoreConnectionWithConfig(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getRedirectLocation(c *Context, w http.ResponseWriter, r *http.Request) {
m := make(map[string]string)
m["location"] = ""
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
w.Write([]byte(model.MapToJSON(m)))
return
}
url := r.URL.Query().Get("url")
if url == "" {
c.SetInvalidParam("url")
return
}
var location string
if err := redirectLocationDataCache.Get(url, &location); err == nil {
m["location"] = location
w.Write([]byte(model.MapToJSON(m)))
return
}
client := c.App.HTTPService().MakeClient(false)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
res, err := client.Head(url)
if err != nil {
// Cache failures to prevent retries.
redirectLocationDataCache.SetWithExpiry(url, "", 1*time.Hour)
// Always return a success status and a JSON string to limit information returned to client.
w.Write([]byte(model.MapToJSON(m)))
return
}
defer func() {
io.Copy(io.Discard, res.Body)
res.Body.Close()
}()
location = res.Header.Get("Location")
redirectLocationDataCache.SetWithExpiry(url, location, 1*time.Hour)
m["location"] = location
w.Write([]byte(model.MapToJSON(m)))
}
func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) {
var ack model.PushNotificationAck
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
c.Err = model.NewAppError("pushNotificationAck",
"api.push_notifications_ack.message.parse.app_error",
nil,
"",
http.StatusBadRequest,
).Wrap(jsonErr)
return
}
if !*c.App.Config().EmailSettings.SendPushNotifications {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notification.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
err := c.App.SendAckToPushProxy(&ack)
if ack.IsIdLoaded {
if err != nil {
// Log the error only, then continue to fetch notification message
c.App.NotificationsLog().Error("Notification ack not sent to push proxy",
mlog.String("ackId", ack.Id),
mlog.String("type", ack.NotificationType),
mlog.String("postId", ack.PostId),
mlog.String("status", err.Error()),
)
}
// Return post data only when PostId is passed.
if ack.PostId != "" && ack.NotificationType == model.PushTypeMessage {
if _, appErr := c.App.GetPostIfAuthorized(c.AppContext, ack.PostId, c.AppContext.Session(), false); appErr != nil {
c.Err = appErr
return
}
notificationInterface := c.App.Notification()
if notificationInterface == nil {
c.Err = model.NewAppError("pushNotificationAck", "api.system.id_loaded.not_available.app_error", nil, "", http.StatusFound)
return
}
msg, appError := notificationInterface.GetNotificationMessage(&ack, c.AppContext.Session().UserId)
if appError != nil {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notification.id_loaded.fetch.app_error", nil, appError.Error(), http.StatusInternalServerError)
return
}
if err2 := json.NewEncoder(w).Encode(msg); err2 != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err2))
}
}
return
} else if err != nil {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notifications_ack.forward.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}
func setServerBusy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// number of seconds to keep server marked busy
secs := r.URL.Query().Get("seconds")
if secs == "" {
secs = strconv.FormatInt(DefaultServerBusySeconds, 10)
}
i, err := strconv.ParseInt(secs, 10, 64)
if err != nil || i <= 0 || i > MaxServerBusySeconds {
c.SetInvalidURLParam(fmt.Sprintf("seconds must be 1 - %d", MaxServerBusySeconds))
return
}
auditRec := c.MakeAuditRecord("setServerBusy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "seconds", i)
c.App.Srv().Platform().Busy.Set(time.Second * time.Duration(i))
mlog.Warn("server busy state activated - non-critical services disabled", mlog.Int64("seconds", i))
auditRec.Success()
ReturnStatusOK(w)
}
func clearServerBusy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec := c.MakeAuditRecord("clearServerBusy", audit.Fail)
defer c.LogAuditRec(auditRec)
c.App.Srv().Platform().Busy.Clear()
mlog.Info("server busy state cleared - non-critical services enabled")
auditRec.Success()
ReturnStatusOK(w)
}
func getServerBusyExpires(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// We call to ToJSON because it actually returns a different struct
// along with doing some computations.
sbsJSON, jsonErr := c.App.Srv().Platform().Busy.ToJSON()
if jsonErr != nil {
mlog.Warn(jsonErr.Error())
}
if _, err := w.Write(sbsJSON); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func upgradeToEnterprise(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("upgradeToEnterprise", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if model.BuildEnterpriseReady == "true" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.already-enterprise.app_error", nil, "", http.StatusTooManyRequests)
return
}
percentage, _ := c.App.Srv().UpgradeToE0Status()
if percentage > 0 {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.app_error", nil, "", http.StatusTooManyRequests)
return
}
if percentage == 100 {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.already-done.app_error", nil, "", http.StatusTooManyRequests)
return
}
if err := c.App.Srv().CanIUpgradeToE0(); err != nil {
var ipErr *upgrader.InvalidPermissions
var iaErr *upgrader.InvalidArch
switch {
case errors.As(err, &ipErr):
params := map[string]any{
"MattermostUsername": ipErr.MattermostUsername,
"FileUsername": ipErr.FileUsername,
"Path": ipErr.Path,
}
if ipErr.ErrType == "invalid-user-and-permission" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", params, err.Error(), http.StatusForbidden)
} else if ipErr.ErrType == "invalid-user" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-user.app_error", params, err.Error(), http.StatusForbidden)
} else if ipErr.ErrType == "invalid-permission" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-permission.app_error", params, err.Error(), http.StatusForbidden)
}
case errors.As(err, &iaErr):
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.system_not_supported.app_error", nil, err.Error(), http.StatusForbidden)
default:
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.generic_error.app_error", nil, err.Error(), http.StatusForbidden)
}
return
}
c.App.Srv().Go(func() {
c.App.Srv().UpgradeToE0()
})
auditRec.Success()
w.WriteHeader(http.StatusAccepted)
ReturnStatusOK(w)
}
func upgradeToEnterpriseStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
percentage, err := c.App.Srv().UpgradeToE0Status()
var s map[string]any
if err != nil {
var isErr *upgrader.InvalidSignature
switch {
case errors.As(err, &isErr):
appErr := model.NewAppError("upgradeToEnterpriseStatus", "api.upgrade_to_enterprise_status.app_error", nil, err.Error(), http.StatusBadRequest)
s = map[string]any{"percentage": 0, "error": appErr.Message}
default:
appErr := model.NewAppError("upgradeToEnterpriseStatus", "api.upgrade_to_enterprise_status.signature.app_error", nil, err.Error(), http.StatusBadRequest)
s = map[string]any{"percentage": 0, "error": appErr.Message}
}
} else {
s = map[string]any{"percentage": percentage, "error": nil}
}
w.Write([]byte(model.StringInterfaceToJSON(s)))
}
func restart(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("restartServer", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.Success()
ReturnStatusOK(w)
time.Sleep(1 * time.Second)
go func() {
c.App.Srv().Restart()
}()
}
func getWarnMetricsStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
status, appErr := c.App.GetWarnMetricsStatus()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("getWarnMetricsStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("sendWarnMetricAckEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
var ack model.SendWarnMetricAck
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
c.SetInvalidParamWithErr("ack", jsonErr)
return
}
appErr = c.App.NotifyAndSetWarnMetricAck(c.Params.WarnMetricId, user, ack.ForceAck, false)
if appErr != nil {
c.Err = appErr
}
auditRec.Success()
ReturnStatusOK(w)
}
func requestTrialLicenseAndAckWarnMetric(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestTrialLicenseAndAckWarnMetric", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if model.BuildEnterpriseReady != "true" {
mlog.Debug("Not Enterprise Edition, skip.")
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
if err := c.App.RequestLicenseAndAckWarnMetric(c.AppContext, c.Params.WarnMetricId, false); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
client, parseError := model.NoticeClientTypeFromString(r.URL.Query().Get("client"))
if parseError != nil {
c.SetInvalidParam("client")
return
}
clientVersion := r.URL.Query().Get("clientVersion")
locale := r.URL.Query().Get("locale")
notices, appErr := c.App.GetProductNotices(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, client, clientVersion, locale)
if appErr != nil {
c.Err = appErr
return
}
result, _ := notices.Marshal()
_, _ = w.Write(result)
}
func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("updateViewedProductNotices", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
ids := model.ArrayFromJSON(r.Body)
appErr := c.App.UpdateViewedProductNotices(c.AppContext.Session().UserId, ids)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getOnboarding", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminCompleteSetupObj, err := c.App.GetOnboarding()
if err != nil {
c.Err = model.NewAppError("getOnboarding", "app.system.get_onboarding_request.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(firstAdminCompleteSetupObj); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func completeOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.Err = model.NewAppError("completeOnboarding", "app.system.complete_onboarding_request.no_first_user", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("completeOnboarding", audit.Fail)
defer c.LogAuditRec(auditRec)
onboardingRequest, err := model.CompleteOnboardingRequestFromReader(r.Body)
if err != nil {
c.Err = model.NewAppError("completeOnboarding", "app.system.complete_onboarding_request.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
audit.AddEventParameter(auditRec, "install_plugin", onboardingRequest.InstallPlugins)
audit.AddEventParameterAuditable(auditRec, "onboarding_request", onboardingRequest)
appErr := c.App.CompleteOnboarding(c.AppContext, onboardingRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getAppliedSchemaMigrations(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
auditRec := c.MakeAuditRecord("getAppliedSchemaMigrations", audit.Fail)
defer c.LogAuditRec(auditRec)
migrations, appErr := c.App.GetAppliedSchemaMigrations()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(migrations)
if err != nil {
c.Err = model.NewAppError("getAppliedMigrations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
auditRec.Success()
}
// returns true if the data has nil fields
// this is being used for testS3 and testEmail methods
func checkHasNilFields(value any) bool {
v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct {
return false
}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && field.IsNil() {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitSystemLocal() {
api.BaseRoutes.System.Handle("/ping", api.APILocal(getSystemPing)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs", api.APILocal(getLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(setServerBusy)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(clearServerBusy)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/integrity", api.APILocal(localCheckIntegrity)).Methods("POST")
api.BaseRoutes.System.Handle("/schema/version", api.APILocal(getAppliedSchemaMigrations)).Methods("GET")
}
func localCheckIntegrity(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localCheckIntegrity", audit.Fail)
defer c.LogAuditRec(auditRec)
var results []model.IntegrityCheckResult
resultsChan := c.App.CheckIntegrity()
for result := range resultsChan {
results = append(results, result)
}
data, err := json.Marshal(results)
if err != nil {
c.Err = model.NewAppError("Api4.localCheckIntegrity", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(data)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxAddMembersBatch = 256
MaximumBulkImportSize = 10 * 1024 * 1024
groupIDsParamPattern = "[^a-zA-Z0-9,]*"
)
var groupIDsQueryParamRegex *regexp.Regexp
func init() {
groupIDsQueryParamRegex = regexp.MustCompile(groupIDsParamPattern)
}
func (api *API) InitTeam() {
api.BaseRoutes.Teams.Handle("", api.APISessionRequired(createTeam)).Methods("POST")
api.BaseRoutes.Teams.Handle("", api.APISessionRequired(getAllTeams)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateTeamScheme)).Methods("PUT")
api.BaseRoutes.Teams.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchTeams)).Methods("POST")
api.BaseRoutes.TeamsForUser.Handle("", api.APISessionRequired(getTeamsForUser)).Methods("GET")
api.BaseRoutes.TeamsForUser.Handle("/unread", api.APISessionRequired(getTeamsUnreadForUser)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(getTeam)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(updateTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(deleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/except", api.APISessionRequired(softDeleteTeamsExcept)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/patch", api.APISessionRequired(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/restore", api.APISessionRequired(restoreTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/privacy", api.APISessionRequired(updateTeamPrivacy)).Methods("PUT")
api.BaseRoutes.Team.Handle("/stats", api.APISessionRequired(getTeamStats)).Methods("GET")
api.BaseRoutes.Team.Handle("/regenerate_invite_id", api.APISessionRequired(regenerateTeamInviteId)).Methods("POST")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon)).Methods("POST")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(removeTeamIcon)).Methods("DELETE")
api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(getTeamMembers)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("/ids", api.APISessionRequired(getTeamMembersByIds)).Methods("POST")
api.BaseRoutes.TeamMembersForUser.Handle("", api.APISessionRequired(getTeamMembersForUser)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(addTeamMember)).Methods("POST")
api.BaseRoutes.Teams.Handle("/members/invite", api.APISessionRequired(addUserToTeamFromInvite)).Methods("POST")
api.BaseRoutes.TeamMembers.Handle("/batch", api.APISessionRequired(addTeamMembers)).Methods("POST")
api.BaseRoutes.TeamMember.Handle("", api.APISessionRequired(removeTeamMember)).Methods("DELETE")
api.BaseRoutes.TeamForUser.Handle("/unread", api.APISessionRequired(getTeamUnread)).Methods("GET")
api.BaseRoutes.TeamByName.Handle("", api.APISessionRequired(getTeamByName)).Methods("GET")
api.BaseRoutes.TeamMember.Handle("", api.APISessionRequired(getTeamMember)).Methods("GET")
api.BaseRoutes.TeamByName.Handle("/exists", api.APISessionRequired(teamExists)).Methods("GET")
api.BaseRoutes.TeamMember.Handle("/roles", api.APISessionRequired(updateTeamMemberRoles)).Methods("PUT")
api.BaseRoutes.TeamMember.Handle("/schemeRoles", api.APISessionRequired(updateTeamMemberSchemeRoles)).Methods("PUT")
api.BaseRoutes.Team.Handle("/import", api.APISessionRequired(importTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/invite/email", api.APISessionRequired(inviteUsersToTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/invite-guests/email", api.APISessionRequired(inviteGuestsToChannels)).Methods("POST")
api.BaseRoutes.Teams.Handle("/invites/email", api.APISessionRequired(invalidateAllEmailInvites)).Methods("DELETE")
api.BaseRoutes.Teams.Handle("/invite/{invite_id:[A-Za-z0-9]+}", api.APIHandler(getInviteInfo)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/members_minus_group_members", api.APISessionRequired(teamMembersMinusGroupMembers)).Methods("GET")
}
func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
auditRec := c.MakeAuditRecord("createTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateTeam) {
c.Err = model.NewAppError("createTeam", "api.team.is_team_creation_allowed.disabled.app_error", nil, "", http.StatusForbidden)
return
}
// On a cloud license, we must check limits before allowing to create
if c.App.Channels().License().IsCloud() {
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createTeam", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// If there are no limits for teams, for active teams, or the limit for active teams is less than 0, do nothing
if !(limits == nil || limits.Teams == nil || limits.Teams.Active == nil || *limits.Teams.Active <= 0) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = appErr
return
}
// if the number of active teams is greater than or equal to the limit, return 400
if teamsUsage.Active >= int64(*limits.Teams.Active) {
c.Err = model.NewAppError("Api4.createTeam", "api.cloud.teams_limit_reached.create", nil, "", http.StatusBadRequest)
return
}
}
}
rteam, err := c.App.CreateTeamWithUser(c.AppContext, &team, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// Don't sanitize the team here since the user will be a team admin and their session won't reflect that yet
auditRec.Success()
auditRec.AddEventResultState(&team)
auditRec.AddEventObjectType("team")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rteam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if (!team.AllowOpenInvite || team.Type != model.TeamOpen) && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), team)
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName()
if c.Err != nil {
return
}
team, err := c.App.GetTeamByName(c.Params.TeamName)
if err != nil {
c.Err = err
return
}
if (!team.AllowOpenInvite || team.Type != model.TeamOpen) && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), team)
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
// The team being updated in the payload must be the same one as indicated in the URL.
if team.Id != c.Params.TeamId {
c.SetInvalidParam("id")
return
}
auditRec := c.MakeAuditRecord("updateTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
updatedTeam, err := c.App.UpdateTeam(&team)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedTeam)
auditRec.AddEventObjectType("team")
c.App.SanitizeTeam(*c.AppContext.Session(), updatedTeam)
if err := json.NewEncoder(w).Encode(updatedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var team model.TeamPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchTeam", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "team_patch", &team)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if oldTeam, err := c.App.GetTeam(c.Params.TeamId); err == nil {
auditRec.AddEventPriorState(oldTeam)
auditRec.AddEventObjectType("team")
}
patchedTeam, err := c.App.PatchTeam(c.Params.TeamId, &team)
if err != nil {
c.Err = err
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
auditRec.Success()
auditRec.AddEventResultState(patchedTeam)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(patchedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func restoreTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("restoreTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
// On a cloud license, we must check limits before allowing to restore
if c.App.Channels().License().IsCloud() {
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.restoreTeam", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// If there are no limits for teams, for active teams, or the limit for active teams is less than 0, do nothing
if !(limits == nil || limits.Teams == nil || limits.Teams.Active == nil || *limits.Teams.Active <= 0) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = appErr
return
}
// if the number of active teams is greater than or equal to the limit, return 400
if teamsUsage.Active >= int64(*limits.Teams.Active) {
c.Err = model.NewAppError("Api4.restoreTeam", "api.cloud.teams_limit_reached.restore", nil, "", http.StatusBadRequest)
return
}
}
}
err := c.App.RestoreTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
// Return the restored team to be consistent with RestoreChannel.
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeamPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok {
c.SetInvalidParam("privacy")
return
}
var openInvite bool
switch privacy {
case model.TeamOpen:
openInvite = true
case model.TeamInvite:
openInvite = false
default:
c.SetInvalidParam("privacy")
return
}
auditRec := c.MakeAuditRecord("updateTeamPrivacy", audit.Fail)
audit.AddEventParameter(auditRec, "privacy", privacy)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
c.SetPermissionError(model.PermissionManageTeam)
return
}
if err := c.App.UpdateTeamPrivacy(c.Params.TeamId, privacy, openInvite); err != nil {
c.Err = err
return
}
// Return the updated team to be consistent with UpdateChannelPrivacy
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func regenerateTeamInviteId(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
auditRec := c.MakeAuditRecord("regenerateTeamInviteId", audit.Fail)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
defer c.LogAuditRec(auditRec)
patchedTeam, err := c.App.RegenerateTeamInviteId(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
if !*c.App.Config().PrivacySettings.ShowEmailAddress && !c.IsSystemAdmin() {
patchedTeam.Email = ""
}
auditRec.Success()
auditRec.AddEventResultState(patchedTeam)
auditRec.AddEventObjectType("team")
c.LogAudit("")
if err := json.NewEncoder(w).Encode(patchedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
auditRec := c.MakeAuditRecord("deleteTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
if team, err := c.App.GetTeam(c.Params.TeamId); err == nil {
audit.AddEventParameterAuditable(auditRec, "team", team)
}
var err *model.AppError
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPITeamDeletion {
err = c.App.PermanentDeleteTeamId(c.AppContext, c.Params.TeamId)
} else {
err = model.NewAppError("deleteTeam", "api.user.delete_team.not_enabled.app_error", nil, "teamId="+c.Params.TeamId, http.StatusUnauthorized)
}
} else {
err = c.App.SoftDeleteTeam(c.Params.TeamId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func softDeleteTeamsExcept(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
err := c.App.SoftDeleteAllTeamsExcept(c.Params.TeamId)
if err != nil {
c.Err = err
}
ReturnStatusOK(w)
}
func getTeamsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
teams, appErr := c.App.GetTeamsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
if !*c.App.Config().PrivacySettings.ShowEmailAddress && !c.IsSystemAdmin() {
for _, team := range teams {
team.Email = ""
}
}
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("getTeamsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamsUnreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// optional team id to be excluded from the result
teamId := r.URL.Query().Get("exclude_team")
includeCollapsedThreads := r.URL.Query().Get("include_collapsed_threads") == "true"
unreadTeamsList, appErr := c.App.GetTeamsUnreadForUser(teamId, c.Params.UserId, includeCollapsedThreads)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(unreadTeamsList)
if err != nil {
c.Err = model.NewAppError("getTeamsUnreadForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
team, appErr := c.App.GetTeamMember(c.Params.TeamId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
sort := r.URL.Query().Get("sort")
excludeDeletedUsers := r.URL.Query().Get("exclude_deleted_users")
excludeDeletedUsersBool, _ := strconv.ParseBool(excludeDeletedUsers)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
teamMembersGetOptions := &model.TeamMembersGetOptions{
Sort: sort,
ExcludeDeletedUsers: excludeDeletedUsersBool,
ViewRestrictions: restrictions,
}
members, appErr := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, teamMembersGetOptions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
members, appErr := c.App.GetTeamMembersForUser(c.Params.UserId, "", true)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembersForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var userIDs []string
err := json.NewDecoder(r.Body).Decode(&userIDs)
if err != nil || len(userIDs) == 0 {
c.SetInvalidParamWithErr("user_ids", err)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
members, appErr := c.App.GetTeamMembersByIds(c.Params.TeamId, userIDs, restrictions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func addTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var err *model.AppError
var member model.TeamMember
if jsonErr := json.NewDecoder(r.Body).Decode(&member); jsonErr != nil {
c.Err = model.NewAppError("addTeamMember", "api.team.add_team_member.invalid_body.app_error", nil, "Error in model.TeamMemberFromJSON()", http.StatusBadRequest).Wrap(jsonErr)
return
}
if member.TeamId != c.Params.TeamId {
c.SetInvalidParam("team_id")
return
}
if !model.IsValidId(member.UserId) {
c.SetInvalidParam("user_id")
return
}
auditRec := c.MakeAuditRecord("addTeamMember", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "member", &member)
defer c.LogAuditRec(auditRec)
if member.UserId == c.AppContext.Session().UserId {
var team *model.Team
team, err = c.App.GetTeam(member.TeamId)
if err != nil {
c.Err = err
return
}
if team.AllowOpenInvite && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionJoinPublicTeams) {
c.SetPermissionError(model.PermissionJoinPublicTeams)
return
}
if !team.AllowOpenInvite && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionJoinPrivateTeams) {
c.SetPermissionError(model.PermissionJoinPrivateTeams)
return
}
} else {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), member.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionAddUserToTeam)
return
}
}
team, err := c.App.GetTeam(member.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
if team.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupTeamMembers([]string{member.UserId}, team)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
var tm *model.TeamMember
tm, err = c.App.AddTeamMember(c.AppContext, member.TeamId, member.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(tm)
auditRec.AddEventObjectType("team_member") // TODO verify this is the final state. should it be the team instead?
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(tm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) {
tokenId := r.URL.Query().Get("token")
inviteId := r.URL.Query().Get("invite_id")
var member *model.TeamMember
var err *model.AppError
auditRec := c.MakeAuditRecord("addUserToTeamFromInvite", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "invite_id", inviteId)
if tokenId != "" {
member, err = c.App.AddTeamMemberByToken(c.AppContext, c.AppContext.Session().UserId, tokenId)
} else if inviteId != "" {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
c.Err = model.NewAppError("addUserToTeamFromInvite", "api.team.add_user_to_team_from_invite.guest.app_error", nil, "", http.StatusForbidden)
return
}
member, err = c.App.AddTeamMemberByInviteId(c.AppContext, inviteId, c.AppContext.Session().UserId)
} else {
err = model.NewAppError("addTeamMember", "api.team.add_user_to_team.missing_parameter.app_error", nil, "", http.StatusBadRequest)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
if member != nil {
auditRec.AddMeta("member", member)
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(member); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
c.RequireTeamId()
if c.Err != nil {
return
}
var appErr *model.AppError
var members []*model.TeamMember
if jsonErr := json.NewDecoder(r.Body).Decode(&members); jsonErr != nil {
c.SetInvalidParamWithErr("members", jsonErr)
return
}
if len(members) > MaxAddMembersBatch {
c.SetInvalidParam("too many members in batch")
return
}
if len(members) == 0 {
c.SetInvalidParam("no members in batch")
return
}
auditRec := c.MakeAuditRecord("addTeamMembers", audit.Fail)
audit.AddEventParameterAuditableArray(auditRec, "members", members)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("count", len(members))
var memberIDs []string
for _, member := range members {
memberIDs = append(memberIDs, member.UserId)
}
auditRec.AddMeta("user_ids", memberIDs)
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
if team.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupTeamMembers(memberIDs, team)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.error", nil, "", http.StatusBadRequest).Wrap(err)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
var userIDs []string
for _, member := range members {
if member.TeamId != c.Params.TeamId {
c.SetInvalidParam("team_id for member with user_id=" + member.UserId)
return
}
if !model.IsValidId(member.UserId) {
c.SetInvalidParam("user_id")
return
}
userIDs = append(userIDs, member.UserId)
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionAddUserToTeam)
return
}
membersWithErrors, appErr := c.App.AddTeamMembers(c.AppContext, c.Params.TeamId, userIDs, c.AppContext.Session().UserId, graceful)
if len(membersWithErrors) != 0 {
errList := make([]string, 0, len(membersWithErrors))
for _, m := range membersWithErrors {
if m.Error != nil {
errList = append(errList, model.TeamMemberWithErrorToString(m))
}
}
auditRec.AddMeta("errors", errList)
}
if appErr != nil {
c.Err = appErr
return
}
var (
js []byte
err error
)
if graceful {
// in 'graceful' mode we allow a different return value, notifying the client which users were not added
js, err = json.Marshal(membersWithErrors)
} else {
js, err = json.Marshal(model.TeamMembersWithErrorToTeamMembers(membersWithErrors))
}
if err != nil {
c.Err = model.NewAppError("addTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func removeTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeTeamMember", audit.Fail)
defer c.LogAuditRec(auditRec)
if c.AppContext.Session().UserId != c.Params.UserId {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionRemoveUserFromTeam) {
c.SetPermissionError(model.PermissionRemoveUserFromTeam)
return
}
}
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
if team.IsGroupConstrained() && (c.Params.UserId != c.AppContext.Session().UserId) && !user.IsBot {
c.Err = model.NewAppError("removeTeamMember", "api.team.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
if err := c.App.RemoveUserFromTeam(c.AppContext, c.Params.TeamId, c.Params.UserId, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
unreadTeam, err := c.App.GetTeamUnread(c.Params.TeamId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(unreadTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, err := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
stats, err := c.App.GetTeamStats(c.Params.TeamId, restrictions)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeamMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !model.IsValidUserRoles(newRoles) {
c.SetInvalidParam("team_member_roles")
return
}
auditRec := c.MakeAuditRecord("updateTeamMemberRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "roles", newRoles)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
c.SetPermissionError(model.PermissionManageTeamRoles)
return
}
teamMember, err := c.App.UpdateTeamMemberRoles(c.Params.TeamId, c.Params.UserId, newRoles)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(teamMember)
auditRec.AddEventObjectType("team_member")
ReturnStatusOK(w)
}
func updateTeamMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
var schemeRoles model.SchemeRoles
if jsonErr := json.NewDecoder(r.Body).Decode(&schemeRoles); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_roles", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateTeamMemberSchemeRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "scheme_roles", &schemeRoles)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
c.SetPermissionError(model.PermissionManageTeamRoles)
return
}
teamMember, err := c.App.UpdateTeamMemberSchemeRoles(c.Params.TeamId, c.Params.UserId, schemeRoles.SchemeGuest, schemeRoles.SchemeUser, schemeRoles.SchemeAdmin)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(teamMember)
auditRec.AddEventObjectType("team_member")
ReturnStatusOK(w)
}
func getAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
teams := []*model.Team{}
var appErr *model.AppError
var teamsWithCount *model.TeamsWithCount
opts := &model.TeamSearch{}
if c.Params.ExcludePolicyConstrained {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts.ExcludePolicyConstrained = model.NewBool(true)
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = model.NewBool(true)
}
listPrivate := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams)
listPublic := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams)
limit := c.Params.PerPage
offset := limit * c.Params.Page
if listPrivate && listPublic {
} else if listPrivate {
opts.AllowOpenInvite = model.NewBool(false)
} else if listPublic {
opts.AllowOpenInvite = model.NewBool(true)
} else {
// The user doesn't have permissions to list private as well as public teams.
c.Err = model.NewAppError("getAllTeams", "api.team.get_all_teams.insufficient_permissions", nil, "", http.StatusForbidden)
return
}
if c.Params.IncludeTotalCount {
teamsWithCount, appErr = c.App.GetAllTeamsPageWithCount(offset, limit, opts)
} else {
teams, appErr = c.App.GetAllTeamsPage(offset, limit, opts)
}
if appErr != nil {
c.Err = appErr
return
}
var (
js []byte
err error
)
if c.Params.IncludeTotalCount {
c.App.SanitizeTeams(*c.AppContext.Session(), teamsWithCount.Teams)
js, err = json.Marshal(teamsWithCount)
} else {
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
js, err = json.Marshal(teams)
}
if err != nil {
c.Err = model.NewAppError("getAllTeams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func searchTeams(c *Context, w http.ResponseWriter, r *http.Request) {
var props model.TeamSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("team_search", err)
return
}
// Only system managers may use the ExcludePolicyConstrained field
if props.ExcludePolicyConstrained != nil && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
// policy ID may only be used through the /data_retention/policies endpoint
props.PolicyID = nil
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
props.IncludePolicyID = model.NewBool(true)
}
var (
teams []*model.Team
totalCount int64
appErr *model.AppError
)
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams) && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams) {
teams, totalCount, appErr = c.App.SearchAllTeams(&props)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams) {
if props.Page != nil || props.PerPage != nil {
c.Err = model.NewAppError("searchTeams", "api.team.search_teams.pagination_not_implemented.private_team_search", nil, "", http.StatusNotImplemented)
return
}
teams, appErr = c.App.SearchPrivateTeams(&props)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams) {
if props.Page != nil || props.PerPage != nil {
c.Err = model.NewAppError("searchTeams", "api.team.search_teams.pagination_not_implemented.public_team_search", nil, "", http.StatusNotImplemented)
return
}
teams, appErr = c.App.SearchPublicTeams(&props)
} else {
teams = []*model.Team{}
}
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
var payload []byte
if props.Page != nil && props.PerPage != nil {
twc := map[string]any{"teams": teams, "total_count": totalCount}
payload = model.ToJSON(twc)
} else {
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("searchTeams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
payload = js
}
w.Write(payload)
}
func teamExists(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName()
if c.Err != nil {
return
}
team, err := c.App.GetTeamByName(c.Params.TeamName)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
exists := false
if team != nil {
var teamMember *model.TeamMember
teamMember, err = c.App.GetTeamMember(team.Id, c.AppContext.Session().UserId)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
// Verify that the user can see the team (be a member or have the permission to list the team)
if (teamMember != nil && teamMember.DeleteAt == 0) ||
(team.AllowOpenInvite && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams)) ||
(!team.AllowOpenInvite && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams)) {
exists = true
}
}
resp := map[string]bool{"exists": exists}
w.Write([]byte(model.MapBoolToJSON(resp)))
}
func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("importTeam", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionImportTeam) {
c.SetPermissionError(model.PermissionImportTeam)
return
}
if err := r.ParseMultipartForm(MaximumBulkImportSize); err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
importFromArray, ok := r.MultipartForm.Value["importFrom"]
if !ok || len(importFromArray) < 1 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.no_import_from.app_error", nil, "", http.StatusBadRequest)
return
}
importFrom := importFromArray[0]
fileSizeStr, ok := r.MultipartForm.Value["filesize"]
if !ok || len(fileSizeStr) < 1 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.unavailable.app_error", nil, "", http.StatusBadRequest)
return
}
fileSize, err := strconv.ParseInt(fileSizeStr[0], 10, 64)
if err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.integer.app_error", nil, "", http.StatusBadRequest)
return
}
fileInfoArray, ok := r.MultipartForm.File["file"]
if !ok {
c.Err = model.NewAppError("importTeam", "api.team.import_team.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileInfoArray) <= 0 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("importTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
fileInfo := fileInfoArray[0]
fileData, err := fileInfo.Open()
if err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer fileData.Close()
audit.AddEventParameter(auditRec, "filename", fileInfo.Filename)
audit.AddEventParameter(auditRec, "filesize", fileSize)
audit.AddEventParameter(auditRec, "from", importFrom)
var log *bytes.Buffer
data := map[string]string{}
switch importFrom {
case "slack":
var err *model.AppError
if err, log = c.App.SlackImport(c.AppContext, fileData, fileSize, c.Params.TeamId); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusBadRequest
}
data["results"] = base64.StdEncoding.EncodeToString(log.Bytes())
default:
c.Err = model.NewAppError("importTeam", "api.team.import_team.unknown_import_from.app_error", nil, "", http.StatusBadRequest)
}
if c.Err != nil {
w.WriteHeader(c.Err.StatusCode)
return
}
auditRec.Success()
w.Write([]byte(model.MapToJSON(data)))
}
func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionInviteUser) {
c.SetPermissionError(model.PermissionInviteUser)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionInviteUser)
return
}
bf, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
memberInvite := &model.MemberInvite{}
if err := json.Unmarshal(bf, memberInvite); err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
c.SetInvalidParam("user_email")
return
}
for i := range emailList {
emailList[i] = strings.ToLower(emailList[i])
}
auditRec := c.MakeAuditRecord("inviteUsersToTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "member_invite", memberInvite)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
auditRec.AddMeta("count", len(emailList))
auditRec.AddMeta("emails", emailList)
if len(memberInvite.ChannelIds) > 0 {
// Check if the user sending the invitation has access to the channels where the invitation is being sent
memberInvite.ChannelIds = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, memberInvite.ChannelIds)
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
auditRec.AddMeta("channels", memberInvite.ChannelIds)
}
if graceful {
var invitesWithError []*model.EmailInviteWithError
var appErr *model.AppError
if emailList != nil {
invitesWithError, appErr = c.App.InviteNewUsersToTeamGracefully(memberInvite, c.Params.TeamId, c.AppContext.Session().UserId, "")
}
if invitesWithError != nil {
errList := make([]string, 0, len(invitesWithError))
for _, inv := range invitesWithError {
if inv.Error != nil {
errList = append(errList, model.EmailInviteWithErrorToString(inv))
}
}
auditRec.AddMeta("errors", errList)
}
if appErr != nil {
c.Err = appErr
return
}
// we get the emailList after it has finished checks like the emails over the list
scheduledAt := model.GetMillis()
jobData := map[string]string{
"emailList": model.ArrayToJSON(emailList),
"teamID": c.Params.TeamId,
"senderID": c.AppContext.Session().UserId,
"scheduledAt": strconv.FormatInt(scheduledAt, 10),
}
if len(memberInvite.ChannelIds) > 0 {
jobData["channelList"] = model.ArrayToJSON(memberInvite.ChannelIds)
}
// we then manually schedule the job to send another invite after 48 hours
_, appErr = c.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, jobData)
if appErr != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeam", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithError)
if err != nil {
c.Err = model.NewAppError("inviteUsersToTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
appErr := c.App.InviteNewUsersToTeam(emailList, c.Params.TeamId, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.license.error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusNotImplemented)
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("inviteGuestsToChannels", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionInviteGuest) {
c.SetPermissionError(model.PermissionInviteGuest)
return
}
guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts
if !guestEnabled {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden)
return
}
var guestsInvite model.GuestsInvite
if err := json.NewDecoder(r.Body).Decode(&guestsInvite); err != nil {
c.Err = model.NewAppError("Api4.inviteGuestsToChannels", "api.team.invite_guests_to_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
audit.AddEventParameterAuditable(auditRec, "guests_invite", &guestsInvite)
for i, email := range guestsInvite.Emails {
guestsInvite.Emails[i] = strings.ToLower(email)
}
if appErr := guestsInvite.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec.AddMeta("email_count", len(guestsInvite.Emails))
auditRec.AddMeta("emails", guestsInvite.Emails)
auditRec.AddMeta("channel_count", len(guestsInvite.Channels))
auditRec.AddMeta("channels", guestsInvite.Channels)
// Check if the user sending the invitation has access to the channels where the invitation is being sent
guestsInvite.Channels = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, guestsInvite.Channels)
if graceful {
var invitesWithError []*model.EmailInviteWithError
var appErr *model.AppError
if guestsInvite.Emails != nil {
invitesWithError, appErr = c.App.InviteGuestsToChannelsGracefully(c.Params.TeamId, &guestsInvite, c.AppContext.Session().UserId)
}
if appErr != nil {
errList := make([]string, 0, len(invitesWithError))
for _, inv := range invitesWithError {
errList = append(errList, model.EmailInviteWithErrorToString(inv))
}
auditRec.AddMeta("errors", errList)
c.Err = appErr
return
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithError)
if err != nil {
c.Err = model.NewAppError("inviteGuestsToChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
appErr := c.App.InviteGuestsToChannels(c.Params.TeamId, &guestsInvite, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireInviteId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeamByInviteId(c.Params.InviteId)
if appErr != nil {
c.Err = appErr
return
}
if team.Type != model.TeamOpen {
c.Err = model.NewAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+c.Params.InviteId, http.StatusForbidden)
return
}
result := struct {
DisplayName string `json:"display_name"`
Description string `json:"description"`
Name string `json:"name"`
ID string `json:"id"`
}{
DisplayName: team.DisplayName,
Description: team.Description,
Name: team.Name,
ID: team.Id,
}
err := json.NewEncoder(w).Encode(result)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func invalidateAllEmailInvites(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionInvalidateEmailInvite) {
c.SetPermissionError(model.PermissionInvalidateEmailInvite)
return
}
auditRec := c.MakeAuditRecord("invalidateAllEmailInvites", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.InvalidateAllEmailInvites(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) &&
(team.Type != model.TeamOpen || !team.AllowOpenInvite) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
etag := strconv.FormatInt(team.LastTeamIconUpdate, 10)
if c.HandleEtag(etag, "Get Team Icon", w, r) {
return
}
img, err := c.App.GetTeamIcon(team)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set(model.HeaderEtagServer, etag)
w.Write(img)
}
func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("setTeamIcon", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, "", http.StatusBadRequest)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.array.app_error", nil, "", http.StatusBadRequest)
return
}
imageData := imageArray[0]
if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func removeTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeTeamIcon", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if err := c.App.RemoveTeamIcon(c.Params.TeamId); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var p model.SchemeIDPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_id", jsonErr)
return
}
schemeID := p.SchemeID
if p.SchemeID == nil || (!model.IsValidId(*p.SchemeID) && *p.SchemeID != "") {
c.SetInvalidParam("scheme_id")
return
}
auditRec := c.MakeAuditRecord("updateTeamScheme", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "scheme_id_patch", &p)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
if *schemeID != "" {
scheme, err := c.App.GetScheme(*schemeID)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "scheme", scheme)
if scheme.Scope != model.SchemeScopeTeam {
c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
return
}
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
team.SchemeId = schemeID
team, err = c.App.UpdateTeamScheme(team)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
ReturnStatusOK(w)
}
func teamMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
groupIDsParam := groupIDsQueryParamRegex.ReplaceAllString(c.Params.GroupIDs, "")
if len(groupIDsParam) < 26 {
c.SetInvalidParam("group_ids")
return
}
groupIDs := []string{}
for _, gid := range strings.Split(c.Params.GroupIDs, ",") {
if !model.IsValidId(gid) {
c.SetInvalidParam("group_ids")
return
}
groupIDs = append(groupIDs, gid)
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
users, totalCount, appErr := c.App.TeamMembersMinusGroupMembers(
c.Params.TeamId,
groupIDs,
c.Params.Page,
c.Params.PerPage,
)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(&model.UsersWithGroupsAndCount{
Users: users,
Count: totalCount,
})
if err != nil {
c.Err = model.NewAppError("Api4.teamMembersMinusGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitTeamLocal() {
api.BaseRoutes.Teams.Handle("", api.APILocal(localCreateTeam)).Methods("POST")
api.BaseRoutes.Teams.Handle("", api.APILocal(getAllTeams)).Methods("GET")
api.BaseRoutes.Teams.Handle("/search", api.APILocal(searchTeams)).Methods("POST")
api.BaseRoutes.Team.Handle("", api.APILocal(getTeam)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APILocal(updateTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("", api.APILocal(localDeleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/invite/email", api.APILocal(localInviteUsersToTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/patch", api.APILocal(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/privacy", api.APILocal(updateTeamPrivacy)).Methods("PUT")
api.BaseRoutes.Team.Handle("/restore", api.APILocal(restoreTeam)).Methods("POST")
api.BaseRoutes.TeamByName.Handle("", api.APILocal(getTeamByName)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("", api.APILocal(addTeamMember)).Methods("POST")
api.BaseRoutes.TeamMember.Handle("", api.APILocal(removeTeamMember)).Methods("DELETE")
}
func localDeleteTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("localDeleteTeam", audit.Fail)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
defer c.LogAuditRec(auditRec)
if team, err := c.App.GetTeam(c.Params.TeamId); err == nil {
auditRec.AddEventPriorState(team)
auditRec.AddEventObjectType("team")
}
var err *model.AppError
if c.Params.Permanent {
err = c.App.PermanentDeleteTeamId(c.AppContext, c.Params.TeamId)
} else {
err = c.App.SoftDeleteTeam(c.Params.TeamId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableEmailInvitations {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
bf, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
memberInvite := &model.MemberInvite{}
err = json.Unmarshal(bf, memberInvite)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
c.SetInvalidParam("user_email")
return
}
for i := range emailList {
email := strings.ToLower(emailList[i])
if !model.IsValidEmail(email) {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Address": email}, "", http.StatusBadRequest)
return
}
emailList[i] = email
}
auditRec := c.MakeAuditRecord("localInviteUsersToTeam", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "member_invite", memberInvite)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
auditRec.AddMeta("count", len(emailList))
auditRec.AddMeta("emails", emailList)
if len(memberInvite.ChannelIds) > 0 {
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
auditRec.AddMeta("channels", memberInvite.ChannelIds)
}
team, err := c.App.Srv().Store().Team().Get(c.Params.TeamId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
c.Err = model.NewAppError("localInviteUsersToTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
c.Err = model.NewAppError("localInviteUsersToTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
allowedDomains := []string{team.AllowedDomains, *c.App.Config().TeamSettings.RestrictCreationToDomains}
var channels []*model.Channel
if len(memberInvite.ChannelIds) > 0 {
channels, err = c.App.Srv().Store().Channel().GetChannelsByIds(memberInvite.ChannelIds, false)
if err != nil {
c.Err = model.NewAppError("prepareLocalInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if r.URL.Query().Get("graceful") != "" {
var invitesWithErrors []*model.EmailInviteWithError
var goodEmails, errList []string
for _, email := range emailList {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !isEmailAddressAllowed(email, allowedDomains) {
invite.Error = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
errList = append(errList, model.EmailInviteWithErrorToString(invite))
} else {
goodEmails = append(goodEmails, email)
}
invitesWithErrors = append(invitesWithErrors, invite)
}
auditRec.AddMeta("errors", errList)
if len(goodEmails) > 0 {
var invitesWithErrors2 []*model.EmailInviteWithError
if len(channels) > 0 {
invitesWithErrors2, err = c.App.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, "Administrator", "mmctl "+model.NewId(), nil, goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, memberInvite.Message, true, true, false)
invitesWithErrors = append(invitesWithErrors, invitesWithErrors2...)
} else {
err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false)
}
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError).Wrap(err)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError).Wrap(err)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge).Wrap(err)
}
return
}
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithErrors)
if err != nil {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
var invalidEmailList []string
for _, email := range emailList {
if !isEmailAddressAllowed(email, allowedDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
return
}
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false)
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError).Wrap(err)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError).Wrap(err)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge).Wrap(err)
}
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func isEmailAddressAllowed(email string, allowedDomains []string) bool {
for _, restriction := range allowedDomains {
domains := normalizeDomains(restriction)
if len(domains) <= 0 {
continue
}
matched := false
for _, d := range domains {
if strings.HasSuffix(email, "@"+d) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
}
func localCreateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
auditRec := c.MakeAuditRecord("localCreateTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
rteam, err := c.App.CreateTeam(c.AppContext, &team)
if err != nil {
c.Err = err
return
}
// Don't sanitize the team here since the user will be a team admin and their session won't reflect that yet
auditRec.AddEventResultState(rteam)
auditRec.AddEventObjectType("type")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rteam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitTermsOfService() {
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(getLatestTermsOfService)).Methods("GET")
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(createTermsOfService)).Methods("POST")
}
func getLatestTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
termsOfService, err := c.App.GetLatestTermsOfService()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomTermsOfService {
c.Err = model.NewAppError("createTermsOfService", "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createTermsOfService", audit.Fail)
defer c.LogAuditRec(auditRec)
props := model.MapFromJSON(r.Body)
text := props["text"]
userId := c.AppContext.Session().UserId
if text == "" {
c.Err = model.NewAppError("Config.IsValid", "api.create_terms_of_service.empty_text.app_error", nil, "", http.StatusBadRequest)
return
}
oldTermsOfService, err := c.App.GetLatestTermsOfService()
if err != nil && err.Id != app.ErrorTermsOfServiceNoRowsFound {
c.Err = err
return
}
if oldTermsOfService == nil || oldTermsOfService.Text != text {
termsOfService, err := c.App.CreateTermsOfService(text, userId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
} else {
if err := json.NewEncoder(w).Encode(oldTermsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"errors"
"io"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUpload() {
api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
}
func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("createUpload",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
var us model.UploadSession
if jsonErr := json.NewDecoder(r.Body).Decode(&us); jsonErr != nil {
c.SetInvalidParamWithErr("upload", jsonErr)
return
}
// these are not supported for client uploads; shared channels only.
us.RemoteId = ""
us.ReqFileId = ""
auditRec := c.MakeAuditRecord("createUpload", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "upload", &us)
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("createUpload", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return
}
us.Type = model.UploadTypeAttachment
}
us.Id = model.NewId()
if c.AppContext.Session().UserId != "" {
us.UserId = c.AppContext.Session().UserId
}
if us.FileSize > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("createUpload", "api.upload.create.upload_too_large.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
return
}
rus, err := c.App.CreateUploadSession(c.AppContext, &us)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rus); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUpload(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUploadId()
if c.Err != nil {
return
}
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.UserId != c.AppContext.Session().UserId && !c.IsSystemAdmin() {
c.Err = model.NewAppError("getUpload", "api.upload.get_upload.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if err := json.NewEncoder(w).Encode(us); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func uploadData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("UploadData", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
} else {
if us.UserId != c.AppContext.Session().UserId || !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return
}
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func doUploadData(c *Context, us *model.UploadSession, r *http.Request) (*model.FileInfo, *model.AppError) {
boundary, parseErr := parseMultipartRequestHeader(r)
if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type",
nil, parseErr.Error(), http.StatusBadRequest)
}
var rd io.Reader
if boundary != "" {
mr := multipart.NewReader(r.Body, boundary)
p, partErr := mr.NextPart()
if partErr != nil {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.multipart_error",
nil, partErr.Error(), http.StatusBadRequest)
}
rd = p
} else {
if r.ContentLength > (us.FileSize - us.FileOffset) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
nil, "", http.StatusBadRequest)
}
rd = r.Body
}
return c.App.UploadData(c.AppContext, us, rd)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitUploadLocal() {
api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload)).Methods("POST")
api.BaseRoutes.Upload.Handle("", api.APILocal(getUpload)).Methods("GET")
api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData)).Methods("POST")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func (api *API) InitUsage() {
// GET /api/v4/usage/posts
api.BaseRoutes.Usage.Handle("/posts", api.APISessionRequired(getPostsUsage)).Methods("GET")
// GET /api/v4/usage/storage
api.BaseRoutes.Usage.Handle("/storage", api.APISessionRequired(getStorageUsage)).Methods("GET")
// GET /api/v4/usage/teams
api.BaseRoutes.Usage.Handle("/teams", api.APISessionRequired(getTeamsUsage)).Methods("GET")
}
func getPostsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
count, appErr := c.App.GetPostsUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(&model.PostsUsage{Count: count})
if err != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getStorageUsage(c *Context, w http.ResponseWriter, r *http.Request) {
usage, appErr := c.App.GetStorageUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getStorageUsage", "app.usage.get_storage_usage.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
usage = utils.RoundOffToZeroesResolution(float64(usage), 8)
json, err := json.Marshal(&model.StorageUsage{Bytes: usage})
if err != nil {
c.Err = model.NewAppError("Api4.getStorageUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getTeamsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
if teamsUsage == nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
json, err := json.Marshal(teamsUsage)
if err != nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUser() {
api.BaseRoutes.Users.Handle("", api.APIHandler(createUser)).Methods("POST")
api.BaseRoutes.Users.Handle("", api.APISessionRequired(getUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/ids", api.APISessionRequired(getUsersByIds)).Methods("POST")
api.BaseRoutes.Users.Handle("/usernames", api.APISessionRequired(getUsersByNames)).Methods("POST")
api.BaseRoutes.Users.Handle("/known", api.APISessionRequired(getKnownUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchUsers)).Methods("POST")
api.BaseRoutes.Users.Handle("/autocomplete", api.APISessionRequired(autocompleteUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/stats", api.APISessionRequired(getTotalUsersStats)).Methods("GET")
api.BaseRoutes.Users.Handle("/stats/filtered", api.APISessionRequired(getFilteredUsersStats)).Methods("GET")
api.BaseRoutes.Users.Handle("/group_channels", api.APISessionRequired(getUsersByGroupChannelIds)).Methods("POST")
api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods("GET")
api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET")
api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods("GET")
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage)).Methods("POST")
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods("DELETE")
api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods("PUT")
api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods("PUT")
api.BaseRoutes.User.Handle("", api.APISessionRequired(deleteUser)).Methods("DELETE")
api.BaseRoutes.User.Handle("/roles", api.APISessionRequired(updateUserRoles)).Methods("PUT")
api.BaseRoutes.User.Handle("/active", api.APISessionRequired(updateUserActive)).Methods("PUT")
api.BaseRoutes.User.Handle("/password", api.APISessionRequired(updatePassword)).Methods("PUT")
api.BaseRoutes.User.Handle("/promote", api.APISessionRequired(promoteGuestToUser)).Methods("POST")
api.BaseRoutes.User.Handle("/demote", api.APISessionRequired(demoteUserToGuest)).Methods("POST")
api.BaseRoutes.User.Handle("/convert_to_bot", api.APISessionRequired(convertUserToBot)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset", api.APIHandler(resetPassword)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset/send", api.APIHandler(sendPasswordReset)).Methods("POST")
api.BaseRoutes.Users.Handle("/email/verify", api.APIHandler(verifyUserEmail)).Methods("POST")
api.BaseRoutes.Users.Handle("/email/verify/send", api.APIHandler(sendVerificationEmail)).Methods("POST")
api.BaseRoutes.User.Handle("/email/verify/member", api.APISessionRequired(verifyUserEmailWithoutToken)).Methods("POST")
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(saveUserTermsOfService)).Methods("POST")
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(getUserTermsOfService)).Methods("GET")
api.BaseRoutes.User.Handle("/auth", api.APISessionRequiredTrustRequester(updateUserAuth)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa", api.APISessionRequiredMfa(updateUserMfa)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods("POST")
api.BaseRoutes.Users.Handle("/login", api.APIHandler(login)).Methods("POST")
api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods("POST")
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods("POST")
api.BaseRoutes.Users.Handle("/logout", api.APIHandler(logout)).Methods("POST")
api.BaseRoutes.UserByUsername.Handle("", api.APISessionRequired(getUserByUsername)).Methods("GET")
api.BaseRoutes.UserByEmail.Handle("", api.APISessionRequired(getUserByEmail)).Methods("GET")
api.BaseRoutes.User.Handle("/sessions", api.APISessionRequired(getSessions)).Methods("GET")
api.BaseRoutes.User.Handle("/sessions/revoke", api.APISessionRequired(revokeSession)).Methods("POST")
api.BaseRoutes.User.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsForUser)).Methods("POST")
api.BaseRoutes.Users.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsAllUsers)).Methods("POST")
api.BaseRoutes.Users.Handle("/sessions/device", api.APISessionRequired(attachDeviceId)).Methods("PUT")
api.BaseRoutes.User.Handle("/audits", api.APISessionRequired(getUserAudits)).Methods("GET")
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(createUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(getUserAccessTokensForUser)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens", api.APISessionRequired(getUserAccessTokens)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/search", api.APISessionRequired(searchUserAccessTokens)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", api.APISessionRequired(getUserAccessToken)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/revoke", api.APISessionRequired(revokeUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/disable", api.APISessionRequired(disableUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/enable", api.APISessionRequired(enableUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/typing", api.APISessionRequiredDisableWhenBusy(publishUserTyping)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.APISessionRequired(migrateAuthToLDAP)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.APISessionRequired(migrateAuthToSaml)).Methods("POST")
api.BaseRoutes.User.Handle("/uploads", api.APISessionRequired(getUploadsForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/channel_members", api.APISessionRequired(getChannelMembersForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/recent_searches", api.APISessionRequiredDisableWhenBusy(getRecentSearches)).Methods("GET")
api.BaseRoutes.Users.Handle("/invalid_emails", api.APISessionRequired(getUsersWithInvalidEmails)).Methods("GET")
api.BaseRoutes.UserThreads.Handle("", api.APISessionRequired(getThreadsForUser)).Methods("GET")
api.BaseRoutes.UserThreads.Handle("/read", api.APISessionRequired(updateReadStateAllThreadsByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("", api.APISessionRequired(getThreadForUser)).Methods("GET")
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(followThreadByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST")
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST")
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
user.SanitizeInput(c.IsSystemAdmin())
tokenId := r.URL.Query().Get("t")
inviteId := r.URL.Query().Get("iid")
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord("createUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "invite_id", inviteId)
audit.AddEventParameter(auditRec, "redirect", redirect)
audit.AddEventParameterAuditable(auditRec, "user", &user)
// No permission check required
var ruser *model.User
var err *model.AppError
if tokenId != "" {
token, appErr := c.App.GetTokenById(tokenId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddMeta("token_type", token.Type)
if token.Type == app.TokenTypeGuestInvitation {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.license.app_error", nil, "", http.StatusBadRequest)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.disabled.app_error", nil, "", http.StatusBadRequest)
return
}
}
ruser, err = c.App.CreateUserWithToken(c.AppContext, &user, token)
} else if inviteId != "" {
ruser, err = c.App.CreateUserWithInviteId(c.AppContext, &user, inviteId, redirect)
} else if c.IsSystemAdmin() {
ruser, err = c.App.CreateUserAsAdmin(c.AppContext, &user, redirect)
auditRec.AddMeta("admin", true)
} else {
ruser, err = c.App.CreateUserFromSignup(c.AppContext, &user, redirect)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
auditRec.AddEventObjectType("user")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUsername()
if c.Err != nil {
return
}
user, err := c.App.GetUserByUsername(c.Params.Username)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.SanitizeEmail()
if c.Err != nil {
return
}
sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin())
if !sanitizeOptions["email"] {
c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
user, err := c.App.GetUserByEmail(c.Params.Email)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
img, err := c.App.GetDefaultProfileImage(user)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
etag := strconv.FormatInt(user.LastPictureUpdate, 10)
if c.HandleEtag(etag, "Get Profile Image", w, r) {
return
}
img, readFailed, err := c.App.GetProfileImage(user)
if err != nil {
c.Err = err
return
}
if readFailed {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", 5*60)) // 5 mins
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set(model.HeaderEtagServer, etag)
}
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func setProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("setProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidURLParam("user_id")
return
}
auditRec.AddEventResultState(user)
if (user.IsLDAPUser() || (user.IsSAMLUser() && *c.App.Config().SamlSettings.EnableSyncWithLdap)) &&
*c.App.Config().LdapSettings.PictureAttribute != "" {
c.Err = model.NewAppError(
"uploadProfileImage", "api.user.upload_profile_user.login_provider_attribute_set.app_error",
nil, "", http.StatusConflict)
return
}
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func setDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("setDefaultProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("setDefaultProfileImage", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
if err := c.App.SetDefaultProfileImage(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
restrictions, err := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
stats, err := c.App.GetTotalUsersStats(restrictions)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
teamID := r.URL.Query().Get("in_team")
channelID := r.URL.Query().Get("in_channel")
includeDeleted := r.URL.Query().Get("include_deleted")
includeBotAccounts := r.URL.Query().Get("include_bots")
rolesString := r.URL.Query().Get("roles")
channelRolesString := r.URL.Query().Get("channel_roles")
teamRolesString := r.URL.Query().Get("team_roles")
includeDeletedBool, _ := strconv.ParseBool(includeDeleted)
includeBotAccountsBool, _ := strconv.ParseBool(includeBotAccounts)
roles := []string{}
var rolesValid bool
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && channelID != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && teamID != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
}
options := &model.UserCountOptions{
IncludeDeleted: includeDeletedBool,
IncludeBotAccounts: includeBotAccountsBool,
TeamId: teamID,
ChannelId: channelID,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
stats, err := c.App.GetFilteredUsersStats(options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
channelIds := model.ArrayFromJSON(r.Body)
if len(channelIds) == 0 {
c.SetInvalidParam("channel_ids")
return
}
usersByChannelId, appErr := c.App.GetUsersByGroupChannelIds(c.AppContext, channelIds, c.IsSystemAdmin())
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(usersByChannelId)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var (
query = r.URL.Query()
inTeamId = query.Get("in_team")
notInTeamId = query.Get("not_in_team")
inChannelId = query.Get("in_channel")
inGroupId = query.Get("in_group")
notInGroupId = query.Get("not_in_group")
notInChannelId = query.Get("not_in_channel")
groupConstrained = query.Get("group_constrained")
withoutTeam = query.Get("without_team")
inactive = query.Get("inactive")
active = query.Get("active")
role = query.Get("role")
sort = query.Get("sort")
rolesString = query.Get("roles")
channelRolesString = query.Get("channel_roles")
teamRolesString = query.Get("team_roles")
)
if notInChannelId != "" && inTeamId == "" {
c.SetInvalidURLParam("team_id")
return
}
if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" && sort != "admin" && sort != "display_name" {
c.SetInvalidURLParam("sort")
return
}
// Currently only supports sorting on a team
// or sort="status" on inChannelId
// or sort="display_name" on inGroupId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "" || notInGroupId != "") {
c.SetInvalidURLParam("sort")
return
}
if sort == "status" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "admin" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "display_name" && (inGroupId == "" || notInGroupId != "" || inTeamId != "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidURLParam("sort")
return
}
var (
withoutTeamBool, _ = strconv.ParseBool(withoutTeam)
groupConstrainedBool, _ = strconv.ParseBool(groupConstrained)
inactiveBool, _ = strconv.ParseBool(inactive)
activeBool, _ = strconv.ParseBool(active)
)
if inactiveBool && activeBool {
c.SetInvalidURLParam("inactive")
}
roleNamesAll := []string{}
// MM-47378: validate 'role' related parameters
if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" {
// fetch all role names
rolesAll, err := c.App.GetAllRoles()
if err != nil {
c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest)
return
}
for _, role := range rolesAll {
roleNamesAll = append(roleNamesAll, role.Name)
}
}
roles := []string{}
var rolesValid bool
if role != "" {
roles, rolesValid = model.CleanRoleNames([]string{role})
if !rolesValid {
c.SetInvalidParam("role")
return
}
roleValid := utils.StringInSlice(role, roleNamesAll)
if !roleValid {
c.SetInvalidParam("role")
return
}
}
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles)
if len(validRoleNames) != len(roles) {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && inChannelId != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles)
if len(validRoleNames) != len(channelRoles) {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && inTeamId != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles)
if len(validRoleNames) != len(teamRoles) {
c.SetInvalidParam("teamRoles")
return
}
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
userGetOptions := &model.UserGetOptions{
InTeamId: inTeamId,
InChannelId: inChannelId,
NotInTeamId: notInTeamId,
NotInChannelId: notInChannelId,
InGroupId: inGroupId,
NotInGroupId: notInGroupId,
GroupConstrained: groupConstrainedBool,
WithoutTeam: withoutTeamBool,
Inactive: inactiveBool,
Active: activeBool,
Role: role,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
Sort: sort,
Page: c.Params.Page,
PerPage: c.Params.PerPage,
ViewRestrictions: restrictions,
}
var (
profiles []*model.User
etag string
)
if inChannelId != "" {
if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
channel, cErr := c.App.GetChannel(c.AppContext, inChannelId)
if cErr != nil {
c.Err = cErr
return
}
if channel.DeleteAt != 0 {
c.Err = model.NewAppError("Api4.getUsersInChannel", "api.user.view_archived_channels.get_users_in_channel.app_error", nil, "", http.StatusForbidden)
return
}
}
}
if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
// Use a special permission for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListUsersWithoutTeam) {
c.SetPermissionError(model.PermissionListUsersWithoutTeam)
return
}
profiles, appErr = c.App.GetUsersWithoutTeamPage(userGetOptions, c.IsSystemAdmin())
} else if notInChannelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), notInChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if notInTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), notInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
etag = c.App.GetUsersNotInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users Not in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersNotInTeamPage(notInTeamId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if inTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), inTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if sort == "last_activity_at" {
profiles, appErr = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if sort == "create_at" {
profiles, appErr = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else {
etag = c.App.GetUsersInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersInTeamPage(userGetOptions, c.IsSystemAdmin())
}
} else if inChannelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), inChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if sort == "status" {
profiles, appErr = c.App.GetUsersInChannelPageByStatus(userGetOptions, c.IsSystemAdmin())
} else if sort == "admin" {
profiles, appErr = c.App.GetUsersInChannelPageByAdmin(userGetOptions, c.IsSystemAdmin())
} else {
profiles, appErr = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin())
}
} else if inGroupId != "" {
if gErr := requireGroupAccess(c, inGroupId); gErr != nil {
gErr.Where = "Api.getUsers"
c.Err = gErr
return
}
if sort == "display_name" {
var user *model.User
user, appErr = c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
profiles, _, appErr = c.App.GetGroupMemberUsersSortedPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions, c.App.GetNotificationNameFormat(user))
} else {
profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
}
} else if notInGroupId != "" {
appErr = requireGroupAccess(c, notInGroupId)
if appErr != nil {
appErr.Where = "Api.getUsers"
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
if appErr != nil {
c.Err = appErr
return
}
} else {
userGetOptions, appErr = c.App.RestrictUsersGetByPermissions(c.AppContext.Session().UserId, userGetOptions)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin())
}
if appErr != nil {
c.Err = appErr
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("getUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func requireGroupAccess(c *web.Context, groupID string) *model.AppError {
group, err := c.App.GetGroup(groupID, nil, nil)
if err != nil {
return err
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil {
return lcErr
}
if group.Source == model.GroupSourceLdap {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionSysconsoleReadUserManagementGroups})
}
}
return nil
}
func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
var userIDs []string
err := json.NewDecoder(r.Body).Decode(&userIDs)
if err != nil || len(userIDs) == 0 {
c.SetInvalidParamWithErr("user_ids", err)
return
}
sinceString := r.URL.Query().Get("since")
options := &store.UserGetByIdsOpts{
IsAdmin: c.IsSystemAdmin(),
}
if sinceString != "" {
since, sErr := strconv.ParseInt(sinceString, 10, 64)
if sErr != nil {
c.SetInvalidParamWithErr("since", sErr)
return
}
options.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
options.ViewRestrictions = restrictions
users, appErr := c.App.GetUsersByIds(userIDs, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
var usernames []string
err := json.NewDecoder(r.Body).Decode(&usernames)
if err != nil || len(usernames) == 0 {
c.SetInvalidParamWithErr("usernames", err)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
users, appErr := c.App.GetUsersByUsernames(usernames, c.IsSystemAdmin(), restrictions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getKnownUsers(c *Context, w http.ResponseWriter, r *http.Request) {
userIDs, appErr := c.App.GetKnownUsers(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(userIDs)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var props model.UserSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("props", err)
return
}
if props.Limit == 0 {
props.Limit = model.UserSearchDefaultLimit
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
if props.TeamId == "" && props.NotInChannelId != "" {
c.SetInvalidParam("team_id")
return
}
if props.InGroupId != "" {
if appErr := requireGroupAccess(c, props.InGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.NotInGroupId != "" {
if appErr := requireGroupAccess(c, props.NotInGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.InChannelId != "" && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.InChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if props.NotInChannelId != "" && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.NotInChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if props.TeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.NotInTeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.NotInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.Limit <= 0 || props.Limit > model.UserSearchMaxLimit {
c.SetInvalidParam("limit")
return
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
AllowInactive: props.AllowInactive,
GroupConstrained: props.GroupConstrained,
Limit: props.Limit,
Role: props.Role,
Roles: props.Roles,
ChannelRoles: props.ChannelRoles,
TeamRoles: props.TeamRoles,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowEmails = true
options.AllowFullNames = true
} else {
options.AllowEmails = *c.App.Config().PrivacySettings.ShowEmailAddress
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
options, appErr := c.App.RestrictUsersSearchByPermissions(c.AppContext.Session().UserId, options)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr := c.App.SearchUsers(&props, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("searchUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
channelId := r.URL.Query().Get("in_channel")
teamId := r.URL.Query().Get("in_team")
name := r.URL.Query().Get("name")
limitStr := r.URL.Query().Get("limit")
limit, _ := strconv.Atoi(limitStr)
if limitStr == "" {
limit = model.UserSearchDefaultLimit
} else if limit > model.UserSearchMaxLimit {
limit = model.UserSearchMaxLimit
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
// Never autocomplete on emails.
AllowEmails: false,
Limit: limit,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowFullNames = true
} else {
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
if channelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
if teamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
var autocomplete model.UserAutocomplete
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.AppContext.Session().UserId, options)
if err != nil {
c.Err = err
return
}
if channelId != "" {
// We're using the channelId to search for users inside that channel and the team
// to get the not in channel list. Also we want to include the DM and GM users for
// that team which could only be obtained having the team id.
if teamId == "" {
c.Err = model.NewAppError("autocompleteUser",
"api.user.autocomplete_users.missing_team_id.app_error",
nil,
"channelId="+channelId,
http.StatusInternalServerError,
)
return
}
result, err := c.App.AutocompleteUsersInChannel(teamId, channelId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InChannel
autocomplete.OutOfChannel = result.OutOfChannel
} else if teamId != "" {
result, err := c.App.AutocompleteUsersInTeam(teamId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InTeam
} else {
result, err := c.App.SearchUsersInTeam("", name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result
}
if err := json.NewEncoder(w).Encode(autocomplete); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "user", &user)
// The user being updated in the payload must be the same one as indicated in the URL.
if user.Id != c.Params.UserId {
c.SetInvalidParam("user_id")
return
}
// Cannot update a system admin unless user making request is a systemadmin also.
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), user.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(user.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
if c.AppContext.Session().IsOAuth {
if ouser.Email != user.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
// Check that the fields being updated are not set by the login provider
conflictField := c.App.CheckProviderAttributes(ouser, user.ToPatch())
if conflictField != "" {
c.Err = model.NewAppError(
"updateUser", "api.user.update_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if user.Email != "" && ouser.Email != user.Email && c.AppContext.Session().UserId == c.Params.UserId {
err = c.App.DoubleCheckPassword(ouser, user.Password)
if err != nil {
c.SetInvalidParam("password")
return
}
}
ruser, err := c.App.UpdateUserAsUser(c.AppContext, &user, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var patch model.UserPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchUser", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "user_patch", &patch)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidParam("user_id")
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if ouser.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.AppContext.Session().IsOAuth && patch.Email != nil {
if ouser.Email != *patch.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
conflictField := c.App.CheckProviderAttributes(ouser, &patch)
if conflictField != "" {
c.Err = model.NewAppError(
"patchUser", "api.user.patch_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if patch.Email != nil && ouser.Email != *patch.Email && c.AppContext.Session().UserId == c.Params.UserId {
if patch.Password == nil {
c.SetInvalidParam("password")
return
}
if err = c.App.DoubleCheckPassword(ouser, *patch.Password); err != nil {
c.Err = err
return
}
}
ruser, err := c.App.PatchUser(c.AppContext, c.Params.UserId, &patch, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
c.App.SetAutoResponderStatus(ruser, ouser.NotifyProps)
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("deleteUser", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if c.Params.UserId == c.AppContext.Session().UserId && !*c.App.Config().TeamSettings.EnableUserDeactivation && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.Err = model.NewAppError("deleteUser", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(userId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPIUserDeletion {
err = c.App.PermanentDeleteUser(c.AppContext, user)
} else {
err = model.NewAppError("deleteUser", "api.user.delete_user.not_enabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
}
} else {
_, err = c.App.UpdateActive(c.AppContext, user, false)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !model.IsValidUserRoles(newRoles) {
c.SetInvalidParam("roles")
return
}
// require license feature to assign "new system roles"
for _, roleName := range strings.Fields(newRoles) {
for _, id := range model.NewSystemRoleIDs {
if roleName == id {
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomPermissionsSchemes {
c.Err = model.NewAppError("updateUserRoles", "api.user.update_user_roles.license.app_error", nil, "", http.StatusBadRequest)
return
}
}
}
}
auditRec := c.MakeAuditRecord("updateUserRoles", audit.Fail)
audit.AddEventParameter(auditRec, "roles", newRoles)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageRoles) {
c.SetPermissionError(model.PermissionManageRoles)
return
}
user, err := c.App.UpdateUserRoles(c.AppContext, c.Params.UserId, newRoles, true)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
c.LogAudit(fmt.Sprintf("user=%s roles=%s", c.Params.UserId, newRoles))
ReturnStatusOK(w)
}
func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
active, ok := props["active"].(bool)
if !ok {
c.SetInvalidParam("active")
return
}
auditRec := c.MakeAuditRecord("updateUserActive", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "active", active)
// true when you're trying to de-activate yourself
isSelfDeactivate := !active && c.Params.UserId == c.AppContext.Session().UserId
if !isSelfDeactivate && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers) {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.permissions.app_error", nil, "userId="+c.Params.UserId, http.StatusForbidden)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if isSelfDeactivate && !*c.App.Config().TeamSettings.EnableUserDeactivation {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if active && user.IsGuest() && !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
if _, err = c.App.UpdateActive(c.AppContext, user, active); err != nil {
c.Err = err
}
auditRec.Success()
c.LogAudit(fmt.Sprintf("user_id=%s active=%v", user.Id, active))
if isSelfDeactivate {
c.App.Srv().Go(func() {
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError))
}
})
}
message := model.NewWebSocketEvent(model.WebsocketEventUserActivationStatusChange, "", "", "", nil, "")
c.App.Publish(message)
ReturnStatusOK(w)
}
func updateUserAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUserAuth", audit.Fail)
defer c.LogAuditRec(auditRec)
var userAuth model.UserAuth
if jsonErr := json.NewDecoder(r.Body).Decode(&userAuth); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "user_auth", &userAuth)
if userAuth.AuthData == nil || *userAuth.AuthData == "" || userAuth.AuthService == "" {
c.Err = model.NewAppError("updateUserAuth", "api.user.update_user_auth.invalid_request", nil, "", http.StatusBadRequest)
return
}
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
auditRec.AddEventPriorState(user)
}
user, err := c.App.UpdateUserAuth(c.Params.UserId, &userAuth)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
auditRec.Success()
auditRec.AddMeta("auth_service", user.AuthService)
c.LogAudit(fmt.Sprintf("updated user %s auth to service=%v", c.Params.UserId, user.AuthService))
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUserMfa", audit.Fail)
defer c.LogAuditRec(auditRec)
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
props := model.StringInterfaceFromJSON(r.Body)
activate, ok := props["activate"].(bool)
if !ok {
c.SetInvalidParam("activate")
return
}
code := ""
if activate {
code, ok = props["code"].(string)
if !ok || code == "" {
c.SetInvalidParam("code")
return
}
}
c.LogAudit("attempt")
if err := c.App.UpdateMfa(c.AppContext, activate, c.Params.UserId, code); err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("activate", activate)
c.LogAudit("success - mfa updated")
ReturnStatusOK(w)
}
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
secret, err := c.App.GenerateMfaSecret(c.Params.UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if err := json.NewEncoder(w).Encode(secret); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord("updatePassword", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempted")
var canUpdatePassword bool
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
if user.IsSystemAdmin() {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
} else {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers)
}
}
var err *model.AppError
// There are two main update flows depending on whether the provided password
// is already hashed or not.
if props["already_hashed"] == "true" {
if canUpdatePassword {
err = c.App.UpdateHashedPasswordByUserId(c.Params.UserId, newPassword)
} else if c.Params.UserId == c.AppContext.Session().UserId {
err = model.NewAppError("updatePassword", "api.user.update_password.user_and_hashed.app_error", nil, "", http.StatusUnauthorized)
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
} else {
if c.Params.UserId == c.AppContext.Session().UserId {
currentPassword := props["current_password"]
if currentPassword == "" {
c.SetInvalidParam("current_password")
return
}
err = c.App.UpdatePasswordAsUser(c.AppContext, c.Params.UserId, currentPassword, newPassword)
} else if canUpdatePassword {
err = c.App.UpdatePasswordByUserIdSendEmail(c.AppContext, c.Params.UserId, newPassword, c.AppContext.T("api.user.reset_password.method"))
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
}
if err != nil {
c.LogAudit("failed")
c.Err = err
return
}
auditRec.Success()
c.LogAudit("completed")
ReturnStatusOK(w)
}
func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord("resetPassword", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt - token=" + token)
if err := c.App.ResetPasswordFromToken(c.AppContext, token, newPassword); err != nil {
c.LogAudit("fail - token=" + token)
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token=" + token)
ReturnStatusOK(w)
}
func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
auditRec := c.MakeAuditRecord("sendPasswordReset", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "email", email)
sent, err := c.App.SendPasswordReset(email, c.App.GetSiteURL())
if err != nil {
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
ReturnStatusOK(w)
} else {
c.Err = err
}
return
}
if sent {
auditRec.Success()
c.LogAudit("sent=" + email)
}
ReturnStatusOK(w)
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
// Mask all sensitive errors, with the exception of the following
defer func() {
if c.Err == nil {
return
}
unmaskedErrors := []string{
"mfa.validate_token.authenticate.app_error",
"api.user.check_user_mfa.bad_code.app_error",
"api.user.login.blank_pwd.app_error",
"api.user.login.bot_login_forbidden.app_error",
"api.user.login.client_side_cert.certificate.app_error",
"api.user.login.inactive.app_error",
"api.user.login.not_verified.app_error",
"api.user.check_user_login_attempts.too_many.app_error",
"app.team.join_user_to_team.max_accounts.app_error",
"store.sql_user.save.max_accounts.app_error",
}
maskError := true
for _, unmaskedError := range unmaskedErrors {
if c.Err.Id == unmaskedError {
maskError = false
}
}
if !maskError {
return
}
config := c.App.Config()
enableUsername := *config.EmailSettings.EnableSignInWithUsername
enableEmail := *config.EmailSettings.EnableSignInWithEmail
samlEnabled := *config.SamlSettings.Enable
gitlabEnabled := *config.GitLabSettings.Enable
openidEnabled := *config.OpenIdSettings.Enable
googleEnabled := *config.GoogleSettings.Enable
office365Enabled := *config.Office365Settings.Enable
if samlEnabled || gitlabEnabled || googleEnabled || office365Enabled || openidEnabled {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_sso", nil, "", http.StatusUnauthorized)
return
}
if enableUsername && !enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_username", nil, "", http.StatusUnauthorized)
return
}
if !enableUsername && enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email", nil, "", http.StatusUnauthorized)
return
}
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email_username", nil, "", http.StatusUnauthorized)
}()
props := model.MapFromJSON(r.Body)
id := props["id"]
loginId := props["login_id"]
password := props["password"]
mfaToken := props["token"]
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
if *c.App.Config().ExperimentalSettings.ClientSideCertEnable {
if license := c.App.Channels().License(); license == nil || !*license.Features.FutureFeatures {
c.Err = model.NewAppError("ClientSideCertNotAllowed", "api.user.login.client_side_cert.license.app_error", nil, "", http.StatusBadRequest)
return
}
certPem, certSubject, certEmail := c.App.CheckForClientSideCert(r)
c.Logger.Debug("Client Cert", mlog.String("cert_subject", certSubject), mlog.String("cert_email", certEmail))
if certPem == "" || certEmail == "" {
c.Err = model.NewAppError("ClientSideCertMissing", "api.user.login.client_side_cert.certificate.app_error", nil, "", http.StatusBadRequest)
return
}
if *c.App.Config().ExperimentalSettings.ClientSideCertCheck == model.ClientSideCertCheckPrimaryAuth {
loginId = certEmail
password = "certificate"
}
}
auditRec := c.MakeAuditRecord("login", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "login_id", loginId)
audit.AddEventParameter(auditRec, "device_id", deviceId)
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
user, err := c.App.AuthenticateUserForLogin(c.AppContext, id, loginId, password, mfaToken, "", ldapOnly)
if err != nil {
c.LogAuditWithUserId(id, "failure - login_id="+loginId)
c.Err = err
return
}
auditRec.AddEventResultState(user)
if user.IsGuest() {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.license.error", nil, "", http.StatusUnauthorized)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.disabled.error", nil, "", http.StatusUnauthorized)
return
}
}
c.LogAuditWithUserId(user.Id, "authenticated")
err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false)
if err != nil {
c.Err = err
return
}
c.LogAuditWithUserId(user.Id, "success")
if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
c.App.AttachSessionCookies(c.AppContext, w, r)
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
user.Sanitize(map[string]bool{})
auditRec.Success()
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
campaignToURL := map[string]string{
"focalboard": "/boards",
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized)
return
}
r.ParseForm()
var loginID string
var token string
var campaign string
if len(r.Form) > 0 {
for key, value := range r.Form {
if key == "login_id" {
loginID = value[0]
}
if key == "cws_token" {
token = value[0]
}
if key == "utm_campaign" {
campaign = value[0]
}
}
}
auditRec := c.MakeAuditRecord("login", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "login_id", loginID)
user, err := c.App.AuthenticateUserForLogin(c.AppContext, "", loginID, "", "", token, false)
if err != nil {
c.LogAuditWithUserId("", "failure - login_id="+loginID)
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
c.LogAuditWithUserId(user.Id, "authenticated")
err = c.App.DoLogin(c.AppContext, w, r, user, "", false, false, false)
if err != nil {
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
c.LogAuditWithUserId(user.Id, "success")
c.App.AttachSessionCookies(c.AppContext, w, r)
redirectURL := *c.App.Config().ServiceSettings.SiteURL
if campaign != "" {
if url, ok := campaignToURL[campaign]; ok {
properties := map[string]any{
"category": "acquisition",
"redirect_to": strings.TrimSuffix(url, "/"),
}
c.App.Srv().GetTelemetryService().SendTelemetry("product_start_redirect", properties)
redirectURL += url
}
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func logout(c *Context, w http.ResponseWriter, r *http.Request) {
Logout(c, w, r)
}
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("Logout", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("")
c.RemoveSessionCookie(w, r)
if c.AppContext.Session().Id != "" {
if err := c.App.RevokeSessionById(c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
}
auditRec.Success()
ReturnStatusOK(w)
}
func getSessions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
sessions, appErr := c.App.GetSessions(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
for _, session := range sessions {
session.Sanitize()
}
js, err := json.Marshal(sessions)
if err != nil {
c.Err = model.NewAppError("getSessions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("revokeSession", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
props := model.MapFromJSON(r.Body)
sessionId := props["session_id"]
if sessionId == "" {
c.SetInvalidParam("session_id")
return
}
audit.AddEventParameter(auditRec, "session_id", sessionId)
session, err := c.App.GetSessionById(sessionId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(session)
auditRec.AddEventObjectType("session")
if session.UserId != c.Params.UserId {
c.SetInvalidURLParam("user_id")
return
}
if err := c.App.RevokeSession(session); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("revokeAllSessionsForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RevokeAllSessions(c.Params.UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsAllUsers(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec := c.MakeAuditRecord("revokeAllSessionsAllUsers", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RevokeSessionsFromAllUsers(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
deviceId := props["device_id"]
if deviceId == "" {
c.SetInvalidParam("device_id")
return
}
auditRec := c.MakeAuditRecord("attachDeviceId", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "device_id", deviceId)
// A special case where we logout of all other sessions with the same device id
if err := c.App.RevokeSessionsForDeviceId(c.AppContext.Session().UserId, deviceId, c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
c.App.ClearSessionCacheForUser(c.AppContext.Session().UserId)
c.App.SetSessionExpireInHours(c.AppContext.Session(), *c.App.Config().ServiceSettings.SessionLengthMobileInHours)
maxAgeSeconds := *c.App.Config().ServiceSettings.SessionLengthMobileInHours * 60 * 60
secure := false
if app.GetProtocol(r) == "https" {
secure = true
}
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: c.AppContext.Session().Token,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Domain: c.App.GetCookieDomain(),
Secure: secure,
}
http.SetCookie(w, sessionCookie)
if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func getUserAudits(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("getUserAudits", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
audits, err := c.App.GetAuditsPage(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("page", c.Params.Page)
auditRec.AddMeta("audits_per_page", c.Params.LogsPerPage)
if err := json.NewEncoder(w).Encode(audits); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func verifyUserEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
auditRec := c.MakeAuditRecord("verifyUserEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.VerifyEmailFromToken(c.AppContext, token); err != nil {
c.Err = model.NewAppError("verifyUserEmail", "api.user.verify_email.bad_link.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
c.LogAudit("Email Verified")
ReturnStatusOK(w)
}
func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord("sendVerificationEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "email", email)
audit.AddEventParameter(auditRec, "redirect", redirect)
user, err := c.App.GetUserForLogin("", email)
if err != nil {
// Don't want to leak whether the email is valid or not
ReturnStatusOK(w)
return
}
auditRec.AddEventResultState(user)
if err = c.App.SendEmailVerification(user, user.Email, redirect); err != nil {
// Don't want to leak whether the email is valid or not
c.LogErrorByCode(err)
ReturnStatusOK(w)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) {
var switchRequest model.SwitchRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&switchRequest); jsonErr != nil {
c.SetInvalidParamWithErr("switch_request", jsonErr)
return
}
auditRec := c.MakeAuditRecord("switchAccountType", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "switch_request", &switchRequest)
link := ""
var err *model.AppError
if switchRequest.EmailToOAuth() {
link, err = c.App.SwitchEmailToOAuth(w, r, switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.NewService)
} else if switchRequest.OAuthToEmail() {
c.SessionRequired()
if c.Err != nil {
return
}
link, err = c.App.SwitchOAuthToEmail(switchRequest.Email, switchRequest.NewPassword, c.AppContext.Session().UserId)
} else if switchRequest.EmailToLdap() {
link, err = c.App.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapLoginId, switchRequest.NewPassword)
} else if switchRequest.LdapToEmail() {
link, err = c.App.SwitchLdapToEmail(switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword)
} else {
c.SetInvalidParam("switch_request")
return
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
w.Write([]byte(model.MapToJSON(map[string]string{"follow_link": link})))
}
func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("createUserAccessToken", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
var accessToken model.UserAccessToken
if jsonErr := json.NewDecoder(r.Body).Decode(&accessToken); jsonErr != nil {
c.SetInvalidParamWithErr("user_access_token", jsonErr)
return
}
if accessToken.Description == "" {
c.SetInvalidParam("description")
return
}
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
accessToken.UserId = c.Params.UserId
accessToken.Token = ""
token, err := c.App.CreateUserAccessToken(&accessToken)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("token_id", token.Id)
c.LogAudit("success - token_id=" + token.Id)
if err := json.NewEncoder(w).Encode(token); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
var props model.UserAccessTokenSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("user_access_token_search", err)
return
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
accessTokens, appErr := c.App.SearchUserAccessTokens(props.Term)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
accessTokens, appErr := c.App.GetUserAccessTokens(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessTokensForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
accessTokens, appErr := c.App.GetUserAccessTokensForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTokenId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
accessToken, appErr := c.App.GetUserAccessToken(c.Params.TokenId, true)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("revokeUserAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "token_id", tokenId)
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.RevokeUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func disableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("disableUserAccessToken", audit.Fail)
audit.AddEventParameter(auditRec, "token_id", tokenId)
defer c.LogAuditRec(auditRec)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.DisableUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("enableUserAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "token_id", tokenId)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.EnableUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func saveUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
auditRec := c.MakeAuditRecord("saveUserTermsOfService", audit.Fail)
defer c.LogAuditRec(auditRec)
userId := c.AppContext.Session().UserId
termsOfServiceId, ok := props["termsOfServiceId"].(string)
if !ok {
c.SetInvalidParam("termsOfServiceId")
return
}
audit.AddEventParameter(auditRec, "terms_of_service_id", termsOfServiceId)
accepted, ok := props["accepted"].(bool)
if !ok {
c.SetInvalidParam("accepted")
return
}
audit.AddEventParameter(auditRec, "accepted", accepted)
if user, err := c.App.GetUser(userId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if _, err := c.App.GetTermsOfService(termsOfServiceId); err != nil {
c.Err = err
return
}
if err := c.App.SaveUserTermsOfService(userId, termsOfServiceId, accepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("TermsOfServiceId=" + termsOfServiceId + ", accepted=" + strconv.FormatBool(accepted))
ReturnStatusOK(w)
}
func getUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
result, err := c.App.GetUserTermsOfService(userId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func promoteGuestToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("promoteGuestToUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPromoteGuest) {
c.SetPermissionError(model.PermissionPromoteGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
if !user.IsGuest() {
c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.user.promote_guest_to_user.no_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.PromoteGuestToUser(c.AppContext, user, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.license.error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.disabled.error", nil, "", http.StatusNotImplemented)
return
}
guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts
if !guestEnabled {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("demoteUserToGuest", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDemoteToGuest) {
c.SetPermissionError(model.PermissionDemoteToGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.AddEventResultState(user)
if user.IsGuest() {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.user.demote_user_to_guest.already_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.DemoteUserToGuest(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func publishUserTyping(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var typingRequest model.TypingRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&typingRequest); jsonErr != nil {
c.SetInvalidParamWithErr("typing_request", jsonErr)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !c.App.HasPermissionToChannel(c.AppContext, c.Params.UserId, typingRequest.ChannelId, model.PermissionCreatePost) {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if err := c.App.PublishUserTyping(c.Params.UserId, typingRequest.ChannelId, typingRequest.ParentId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func verifyUserEmailWithoutToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("verifyUserEmailWithoutToken", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("user_id", user.Id)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.VerifyUserEmail(user.Id, user.Email); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("user verified")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertUserToBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("convertUserToBot", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "user", user)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
bot, appErr := c.App.ConvertUserToBot(user)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
js, err := json.Marshal(bot)
if err != nil {
c.Err = model.NewAppError("convertUserToBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(js)
}
func getUploadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.Params.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("getUploadsForUser", "api.user.get_uploads_for_user.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
uss, appErr := c.App.GetUploadSessionsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(uss)
if err != nil {
c.Err = model.NewAppError("getUploadsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getChannelMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
members, err := c.App.GetChannelMembersWithTeamDataForUserWithPagination(c.AppContext, c.Params.UserId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "saml" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
matchField, ok := props["match_field"].(string)
if !ok {
c.SetInvalidParam("match_field")
return
}
auditRec := c.MakeAuditRecord("migrateAuthToLdap", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "from", from)
audit.AddEventParameter(auditRec, "force", force)
audit.AddEventParameter(auditRec, "match_field", matchField)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("api.migrateAuthToLDAP", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToLdap(from, matchField, force, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "ldap" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
auto, ok := props["auto"].(bool)
if !ok {
c.SetInvalidParam("auto")
return
}
matches, ok := props["matches"].(map[string]any)
if !ok {
c.SetInvalidParam("matches")
return
}
usersMap := model.MapFromJSON(strings.NewReader(model.StringInterfaceToJSON(matches)))
auditRec := c.MakeAuditRecord("migrateAuthToSaml", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "from", from)
audit.AddEventParameter(auditRec, "auto", auto)
audit.AddEventParameter(auditRec, "users_map", usersMap)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.SAML {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToSaml(from, usersMap, auto, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireThreadId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
extendedStr := r.URL.Query().Get("extended")
extended, _ := strconv.ParseBool(extendedStr)
threadMembership, err := c.App.GetThreadMembershipForUser(c.Params.UserId, c.Params.ThreadId)
if err != nil {
c.Err = err
return
}
thread, err := c.App.GetThreadForUser(threadMembership, extended)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
options := model.GetUserThreadsOpts{
Since: 0,
Before: "",
After: "",
PageSize: uint64(c.Params.PerPage),
Unread: false,
Extended: false,
Deleted: false,
TotalsOnly: false,
ThreadsOnly: false,
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, parseError := strconv.ParseUint(sinceString, 10, 64)
if parseError != nil {
c.SetInvalidParam("since")
return
}
options.Since = since
}
options.Before = r.URL.Query().Get("before")
options.After = r.URL.Query().Get("after")
totalsOnlyStr := r.URL.Query().Get("totalsOnly")
threadsOnlyStr := r.URL.Query().Get("threadsOnly")
options.TotalsOnly, _ = strconv.ParseBool(totalsOnlyStr)
options.ThreadsOnly, _ = strconv.ParseBool(threadsOnlyStr)
// parameters are mutually exclusive
if options.Before != "" && options.After != "" {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_params", nil, "", http.StatusBadRequest)
return
}
// parameters are mutually exclusive
if options.TotalsOnly && options.ThreadsOnly {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_only_params", nil, "", http.StatusBadRequest)
return
}
deletedStr := r.URL.Query().Get("deleted")
unreadStr := r.URL.Query().Get("unread")
extendedStr := r.URL.Query().Get("extended")
options.Deleted, _ = strconv.ParseBool(deletedStr)
options.Unread, _ = strconv.ParseBool(unreadStr)
options.Extended, _ = strconv.ParseBool(extendedStr)
threads, err := c.App.GetThreadsForUser(c.Params.UserId, c.Params.TeamId, options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(threads); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTimestamp().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateReadStateThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "timestamp", c.Params.Timestamp)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func setUnreadThreadByPostId(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequirePostId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("setUnreadThreadByPostId", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
thread, err := c.App.UpdateThreadReadForUserByPost(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.PostId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("unfollowThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func followThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("followThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, true)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateReadStateAllThreadsByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Request) {
if *c.App.Config().TeamSettings.EnableOpenServer {
c.Err = model.NewAppError("GetUsersWithInvalidEmails", model.NoTranslation, nil, "TeamSettings.EnableOpenServer is enabled", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
users, appErr := c.App.GetUsersWithInvalidEmails(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(users)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getRecentSearches(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
searchParams, err := c.App.GetRecentSearchesForUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(searchParams); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUserLocal() {
api.BaseRoutes.Users.Handle("", api.APILocal(localGetUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("", api.APILocal(localPermanentDeleteAllUsers)).Methods("DELETE")
api.BaseRoutes.Users.Handle("", api.APILocal(createUser)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset/send", api.APILocal(sendPasswordReset)).Methods("POST")
api.BaseRoutes.Users.Handle("/ids", api.APILocal(localGetUsersByIds)).Methods("POST")
api.BaseRoutes.User.Handle("", api.APILocal(localGetUser)).Methods("GET")
api.BaseRoutes.User.Handle("", api.APILocal(updateUser)).Methods("PUT")
api.BaseRoutes.User.Handle("", api.APILocal(localDeleteUser)).Methods("DELETE")
api.BaseRoutes.User.Handle("/roles", api.APILocal(updateUserRoles)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa", api.APILocal(updateUserMfa)).Methods("PUT")
api.BaseRoutes.User.Handle("/active", api.APILocal(updateUserActive)).Methods("PUT")
api.BaseRoutes.User.Handle("/password", api.APILocal(updatePassword)).Methods("PUT")
api.BaseRoutes.User.Handle("/convert_to_bot", api.APILocal(convertUserToBot)).Methods("POST")
api.BaseRoutes.User.Handle("/email/verify/member", api.APILocal(verifyUserEmailWithoutToken)).Methods("POST")
api.BaseRoutes.User.Handle("/promote", api.APILocal(promoteGuestToUser)).Methods("POST")
api.BaseRoutes.User.Handle("/demote", api.APILocal(demoteUserToGuest)).Methods("POST")
api.BaseRoutes.UserByUsername.Handle("", api.APILocal(localGetUserByUsername)).Methods("GET")
api.BaseRoutes.UserByEmail.Handle("", api.APILocal(localGetUserByEmail)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/revoke", api.APILocal(revokeUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/tokens", api.APILocal(getUserAccessTokensForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/tokens", api.APILocal(createUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.APILocal(migrateAuthToLDAP)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.APILocal(migrateAuthToSaml)).Methods("POST")
api.BaseRoutes.User.Handle("/uploads", api.APILocal(localGetUploadsForUser)).Methods("GET")
}
func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) {
inTeamId := r.URL.Query().Get("in_team")
notInTeamId := r.URL.Query().Get("not_in_team")
inChannelId := r.URL.Query().Get("in_channel")
notInChannelId := r.URL.Query().Get("not_in_channel")
groupConstrained := r.URL.Query().Get("group_constrained")
withoutTeam := r.URL.Query().Get("without_team")
active := r.URL.Query().Get("active")
inactive := r.URL.Query().Get("inactive")
role := r.URL.Query().Get("role")
rolesString := r.URL.Query().Get("roles")
channelRolesString := r.URL.Query().Get("channel_roles")
teamRolesString := r.URL.Query().Get("team_roles")
sort := r.URL.Query().Get("sort")
roleNamesAll := []string{}
// MM-47378: validate 'role' related parameters
if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" {
// fetch all role names
rolesAll, err := c.App.GetAllRoles()
if err != nil {
c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest)
return
}
for _, role := range rolesAll {
roleNamesAll = append(roleNamesAll, role.Name)
}
}
var roles []string
var rolesValid bool
if role != "" {
_, rolesValid = model.CleanRoleNames([]string{role})
if !rolesValid {
c.SetInvalidParam("role")
return
}
roleValid := utils.StringInSlice(role, roleNamesAll)
if !roleValid {
c.SetInvalidParam("role")
return
}
}
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles)
if len(validRoleNames) != len(roles) {
c.SetInvalidParam("roles")
return
}
}
var channelRoles []string
if channelRolesString != "" && inChannelId != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles)
if len(validRoleNames) != len(channelRoles) {
c.SetInvalidParam("channelRoles")
return
}
}
var teamRoles []string
if teamRolesString != "" && inTeamId != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles)
if len(validRoleNames) != len(teamRoles) {
c.SetInvalidParam("teamRoles")
return
}
}
if notInChannelId != "" && inTeamId == "" {
c.SetInvalidURLParam("team_id")
return
}
if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" {
c.SetInvalidURLParam("sort")
return
}
// Currently only supports sorting on a team
// or sort="status" on inChannelId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidURLParam("sort")
return
}
if sort == "status" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
withoutTeamBool, _ := strconv.ParseBool(withoutTeam)
groupConstrainedBool, _ := strconv.ParseBool(groupConstrained)
activeBool, _ := strconv.ParseBool(active)
inactiveBool, _ := strconv.ParseBool(inactive)
userGetOptions := &model.UserGetOptions{
InTeamId: inTeamId,
InChannelId: inChannelId,
NotInTeamId: notInTeamId,
NotInChannelId: notInChannelId,
GroupConstrained: groupConstrainedBool,
WithoutTeam: withoutTeamBool,
Active: activeBool,
Inactive: inactiveBool,
Role: role,
Sort: sort,
Page: c.Params.Page,
PerPage: c.Params.PerPage,
ViewRestrictions: nil,
}
var (
appErr *model.AppError
profiles []*model.User
etag string
)
if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
profiles, appErr = c.App.GetUsersWithoutTeamPage(userGetOptions, c.IsSystemAdmin())
} else if notInChannelId != "" {
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if notInTeamId != "" {
etag = c.App.GetUsersNotInTeamEtag(inTeamId, "")
if c.HandleEtag(etag, "Get Users Not in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersNotInTeamPage(notInTeamId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if inTeamId != "" {
if sort == "last_activity_at" {
profiles, appErr = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if sort == "create_at" {
profiles, appErr = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else {
etag = c.App.GetUsersInTeamEtag(inTeamId, "")
if c.HandleEtag(etag, "Get Users in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersInTeamPage(userGetOptions, c.IsSystemAdmin())
}
} else if inChannelId != "" {
if sort == "status" {
profiles, appErr = c.App.GetUsersInChannelPageByStatus(userGetOptions, c.IsSystemAdmin())
} else {
profiles, appErr = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin())
}
} else {
profiles, appErr = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin())
}
if appErr != nil {
c.Err = appErr
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("localGetUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
sinceString := r.URL.Query().Get("since")
options := &store.UserGetByIdsOpts{
IsAdmin: c.IsSystemAdmin(),
}
if sinceString != "" {
since, err := strconv.ParseInt(sinceString, 10, 64)
if err != nil {
c.SetInvalidParamWithErr("since", err)
return
}
options.Since = since
}
users, appErr := c.App.GetUsersByIds(userIds, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("localGetUsersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func localGetUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localDeleteUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("localDeleteUser", audit.Fail)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(userId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if c.Params.Permanent {
err = c.App.PermanentDeleteUser(c.AppContext, user)
} else {
_, err = c.App.UpdateActive(c.AppContext, user, false)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localPermanentDeleteAllUsers(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localPermanentDeleteAllUsers", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.PermanentDeleteAllUsers(c.AppContext); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localGetUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUsername()
if c.Err != nil {
return
}
user, err := c.App.GetUserByUsername(c.Params.Username)
if err != nil {
c.Err = err
return
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localGetUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.SanitizeEmail()
if c.Err != nil {
return
}
sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin())
if !sanitizeOptions["email"] {
c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
user, err := c.App.GetUserByEmail(c.Params.Email)
if err != nil {
c.Err = err
return
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localGetUploadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
uss, appErr := c.App.GetUploadSessionsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(uss)
if err != nil {
c.Err = model.NewAppError("localGetUploadsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitWebhook() {
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(createIncomingHook)).Methods("POST")
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(getIncomingHooks)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(getIncomingHook)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(updateIncomingHook)).Methods("PUT")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(deleteIncomingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(createOutgoingHook)).Methods("POST")
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(getOutgoingHooks)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(getOutgoingHook)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(deleteOutgoingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHook.Handle("/regen_token", api.APISessionRequired(regenOutgoingHookToken)).Methods("POST")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
channel, err := c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("createIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "incoming_webhook", &hook)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.LogAudit("fail - bad channel permissions")
c.SetPermissionError(model.PermissionReadChannel)
return
}
userId := c.AppContext.Session().UserId
if hook.UserId != "" && hook.UserId != userId {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if _, err = c.App.GetUser(hook.UserId); err != nil {
c.Err = err
return
}
userId = hook.UserId
}
incomingHook, err := c.App.CreateIncomingWebhookForChannel(userId, channel, &hook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(incomingHook)
auditRec.AddEventObjectType("hook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
var updatedHook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
// The hook being updated in the payload must be the same one as indicated in the URL.
if updatedHook.Id != c.Params.HookId {
c.SetInvalidParam("hook_id")
return
}
auditRec := c.MakeAuditRecord("updateIncomingHook", audit.Fail)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
oldHook, err := c.App.GetIncomingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oldHook)
auditRec.AddEventObjectType("incoming_webhook")
if updatedHook.TeamId == "" {
updatedHook.TeamId = oldHook.TeamId
}
if updatedHook.TeamId != oldHook.TeamId {
c.Err = model.NewAppError("updateIncomingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
channel, err := c.App.GetChannel(c.AppContext, updatedHook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
if channel.TeamId != updatedHook.TeamId {
c.SetInvalidParam("channel_id")
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != oldHook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.LogAudit("fail - bad channel permissions")
c.SetPermissionError(model.PermissionReadChannel)
return
}
incomingHook, err := c.App.UpdateIncomingWebhook(oldHook, &updatedHook)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(incomingHook)
auditRec.Success()
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
var (
teamID = r.URL.Query().Get("team_id")
userID = c.AppContext.Session().UserId
hooks []*model.IncomingWebhook
appErr *model.AppError
)
if teamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersIncomingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetIncomingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersIncomingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetIncomingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(hooks)
if err != nil {
c.Err = model.NewAppError("getIncomingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hookId := c.Params.HookId
var err *model.AppError
var hook *model.IncomingWebhook
var channel *model.Channel
hook, err = c.App.GetIncomingWebhook(hookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("getIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
c.LogAudit("fail - bad permissions")
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(hook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hookId := c.Params.HookId
var err *model.AppError
var hook *model.IncomingWebhook
var channel *model.Channel
hook, err = c.App.GetIncomingWebhook(hookId)
if err != nil {
c.Err = err
return
}
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", hook.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
c.LogAudit("fail - bad permissions")
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if err = c.App.DeleteIncomingWebhook(hookId); err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(hook)
auditRec.AddEventObjectType("incoming_webhook")
auditRec.Success()
ReturnStatusOK(w)
}
func updateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
var updatedHook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
// The hook being updated in the payload must be the same one as indicated in the URL.
if updatedHook.Id != c.Params.HookId {
c.SetInvalidParam("hook_id")
return
}
auditRec := c.MakeAuditRecord("updateOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
c.LogAudit("attempt")
oldHook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
if updatedHook.TeamId == "" {
updatedHook.TeamId = oldHook.TeamId
}
if updatedHook.TeamId != oldHook.TeamId {
c.Err = model.NewAppError("updateOutgoingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != oldHook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
updatedHook.CreatorId = c.AppContext.Session().UserId
rhook, err := c.App.UpdateOutgoingWebhook(c.AppContext, oldHook, &updatedHook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOutgoingHook", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if hook.CreatorId == "" {
hook.CreatorId = c.AppContext.Session().UserId
} else {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
_, err := c.App.GetUser(hook.CreatorId)
if err != nil {
c.Err = err
return
}
}
rhook, err := c.App.CreateOutgoingWebhook(&hook)
if err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
var (
query = r.URL.Query()
channelID = query.Get("channel_id")
teamID = query.Get("team_id")
userID = c.AppContext.Session().UserId
hooks []*model.OutgoingWebhook
appErr *model.AppError
)
if channelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, c.Params.Page, c.Params.PerPage)
} else if teamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(hooks)
if err != nil {
c.Err = model.NewAppError("getOutgoingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("getOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(hook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("regenOutgoingHookToken", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
rhook, err := c.App.RegenOutgoingWebhookToken(hook)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
if err := c.App.DeleteOutgoingWebhook(hook.Id); err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitWebhookLocal() {
api.BaseRoutes.IncomingHooks.Handle("", api.APILocal(localCreateIncomingHook)).Methods("POST")
api.BaseRoutes.IncomingHooks.Handle("", api.APILocal(getIncomingHooks)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(getIncomingHook)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(updateIncomingHook)).Methods("PUT")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(deleteIncomingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHooks.Handle("", api.APILocal(localCreateOutgoingHook)).Methods("POST")
api.BaseRoutes.OutgoingHooks.Handle("", api.APILocal(getOutgoingHooks)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(getOutgoingHook)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(deleteOutgoingHook)).Methods("DELETE")
}
func localCreateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
if hook.UserId == "" {
c.SetInvalidParam("user_id")
return
}
channel, err := c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
if _, err = c.App.GetUser(hook.UserId); err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localCreateIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
c.LogAudit("attempt")
incomingHook, err := c.App.CreateIncomingWebhookForChannel(hook.UserId, channel, &hook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(incomingHook)
auditRec.AddEventObjectType("incoming_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localCreateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
c.LogAudit("attempt")
if hook.CreatorId == "" {
c.SetInvalidParam("creator_id")
return
}
_, err := c.App.GetUser(hook.CreatorId)
if err != nil {
c.Err = err
return
}
rhook, err := c.App.CreateOutgoingWebhook(&hook)
if err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
connectionIDParam = "connection_id"
sequenceNumberParam = "sequence_number"
)
func (api *API) InitWebSocket() {
// Optionally supports a trailing slash
api.BaseRoutes.APIRoot.Handle("/{websocket:websocket(?:\\/)?}", api.APIHandlerTrustRequester(connectWebSocket)).Methods("GET")
}
func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: model.SocketMaxMessageSizeKb,
WriteBufferSize: model.SocketMaxMessageSizeKb,
CheckOrigin: c.App.OriginChecker(),
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
c.Err = model.NewAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
// We initialize webconn with all the necessary data.
// If the queues are empty, they are initialized in the constructor.
cfg := &platform.WebConnConfig{
WebSocket: ws,
Session: *c.AppContext.Session(),
TFunc: c.AppContext.T,
Locale: "",
Active: true,
}
cfg.ConnectionID = r.URL.Query().Get(connectionIDParam)
if cfg.ConnectionID == "" || c.AppContext.Session().UserId == "" {
// If not present, we assume client is not capable yet, or it's a fresh connection.
// We just create a new ID.
cfg.ConnectionID = model.NewId()
// In case of fresh connection id, sequence number is already zero.
} else {
cfg, err = c.App.Srv().Platform().PopulateWebConnConfig(c.AppContext.Session(), cfg, r.URL.Query().Get(sequenceNumberParam))
if err != nil {
mlog.Warn("Error while populating webconn config", mlog.String("id", r.URL.Query().Get(connectionIDParam)), mlog.Err(err))
ws.Close()
return
}
}
wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels())
if c.AppContext.Session().UserId != "" {
c.App.Srv().Platform().HubRegister(wc)
}
wc.Pump()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
)
func (api *API) InitWorkTemplate() {
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(getWorkTemplateCategories)).Methods("GET")
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(getWorkTemplates)).Methods("GET")
api.BaseRoutes.WorkTemplates.Handle("/execute", api.APIHandler(executeWorkTemplate)).Methods("POST")
}
func areWorkTemplatesEnabled(c *Context) *model.AppError {
if !c.App.Config().FeatureFlags.WorkTemplate {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "feature flag is off", http.StatusNotFound)
}
// we have to make sure that playbooks plugin is enabled and board is a product
pbActive, err := c.App.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !pbActive {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "playbook plugin not active", http.StatusNotFound)
}
hasBoard, err := c.App.HasBoardProduct()
if err != nil {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !hasBoard {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "board product not found", http.StatusNotFound)
}
return nil
}
func getWorkTemplateCategories(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
t := c.AppContext.GetT()
categories, appErr := c.App.GetWorkTemplateCategories(t)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getWorkTemplateCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
c.RequireCategory()
if c.Err != nil {
return
}
t := c.AppContext.GetT()
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), t)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(workTemplates)
if err != nil {
c.Err = model.NewAppError("getWorkTemplates", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func executeWorkTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
wtcr := &worktemplates.ExecutionRequest{}
err := json.NewDecoder(r.Body).Decode(wtcr)
if err != nil {
c.Err = model.NewAppError("executeWorkTemplate", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
canCreatePublicChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePublicChannel)
canCreatePrivateChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePrivateChannel)
// focalboard uses channel permissions for board creation
canCreatePublicBoard := canCreatePublicChannel
canCreatePrivateBoard := canCreatePrivateChannel
canCreatePublicPlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPublicPlaybookCreate)
canCreatePrivatePlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPrivatePlaybookCreate)
appErr = wtcr.CanBeExecuted(worktemplates.PermissionSet{
License: c.App.License(),
CanCreatePublicChannel: canCreatePublicChannel,
CanCreatePrivateChannel: canCreatePrivateChannel,
CanCreatePublicBoard: canCreatePublicBoard,
CanCreatePrivateBoard: canCreatePrivateBoard,
CanCreatePublicPlaybook: canCreatePublicPlaybook,
CanCreatePrivatePlaybook: canCreatePrivatePlaybook,
})
if appErr != nil {
c.Err = appErr
return
}
canInstallPlugin := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins)
if !*c.App.Config().PluginSettings.Enable || !*c.App.Config().PluginSettings.EnableMarketplace || *c.App.Config().PluginSettings.MarketplaceURL != model.PluginSettingsDefaultMarketplaceURL {
canInstallPlugin = false
}
res, appErr := c.App.ExecuteWorkTemplate(c.AppContext, wtcr, canInstallPlugin)
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(res)
if err != nil {
c.Err = model.NewAppError("executeWorkTemplate", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var latestVersionCache = cache.NewLRU(cache.LRUOptions{
Size: 1,
})
func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) {
var lines []string
license := s.License()
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, info.Hostname)
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
} else {
mlog.Error("Could not get cluster info")
}
}
melines, err := s.GetLogsSkipSend(page, perPage, &model.LogFilter{})
if err != nil {
return nil, err
}
lines = append(lines, melines...)
if s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
clines, err := s.platform.Cluster().GetLogs(page, perPage)
if err != nil {
return nil, err
}
lines = append(lines, clines...)
}
return lines, nil
}
func (s *Server) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
logData := make(map[string][]string)
serverName := "default"
license := s.License()
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
serverName = info.Hostname
} else {
mlog.Error("Could not get cluster info")
}
}
serverNames := logFilter.ServerNames
if len(serverNames) > 0 {
for _, nodeName := range serverNames {
if nodeName == "default" {
AddLocalLogs(logData, s, page, perPage, nodeName, logFilter)
}
}
} else {
AddLocalLogs(logData, s, page, perPage, serverName, logFilter)
}
if s.platform.Cluster() != nil && *s.Config().ClusterSettings.Enable {
clusterLogs, err := s.platform.Cluster().QueryLogs(page, perPage)
if err != nil {
return nil, err
}
if clusterLogs != nil && len(serverNames) > 0 {
for _, filteredNodeName := range serverNames {
logData[filteredNodeName] = clusterLogs[filteredNodeName]
}
} else {
for nodeName, logs := range clusterLogs {
logData[nodeName] = logs
}
}
}
return logData, nil
}
func AddLocalLogs(logData map[string][]string, s *Server, page, perPage int, serverName string, logFilter *model.LogFilter) *model.AppError {
currentServerLogs, err := s.GetLogsSkipSend(page, perPage, logFilter)
if err != nil {
return err
}
logData[serverName] = currentServerLogs
return nil
}
func (a *App) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
return a.Srv().QueryLogs(page, perPage, logFilter)
}
func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) {
return a.Srv().GetLogs(page, perPage)
}
func (s *Server) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
return s.platform.GetLogsSkipSend(page, perPage, logFilter)
}
func (a *App) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
return a.Srv().GetLogsSkipSend(page, perPage, logFilter)
}
func (a *App) GetClusterStatus() []*model.ClusterInfo {
infos := make([]*model.ClusterInfo, 0)
if a.Cluster() != nil {
infos = a.Cluster().GetClusterInfos()
}
return infos
}
func (s *Server) InvalidateAllCaches() *model.AppError {
return s.platform.InvalidateAllCaches()
}
func (s *Server) InvalidateAllCachesSkipSend() {
s.platform.InvalidateAllCachesSkipSend()
}
func (a *App) RecycleDatabaseConnection() {
mlog.Info("Attempting to recycle database connections.")
// This works by setting 10 seconds as the max conn lifetime for all DB connections.
// This allows in gradually closing connections as they expire. In future, we can think
// of exposing this as a param from the REST api.
a.Srv().Store().RecycleDBConnections(10 * time.Second)
mlog.Info("Finished recycling database connections.")
}
func (a *App) TestSiteURL(siteURL string) *model.AppError {
url := fmt.Sprintf("%s/api/v4/system/ping", siteURL)
res, err := http.Get(url)
if err != nil || res.StatusCode != 200 {
return model.NewAppError("testSiteURL", "app.admin.test_site_url.failure", nil, "", http.StatusBadRequest)
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
return nil
}
func (a *App) TestEmail(userID string, cfg *model.Config) *model.AppError {
if *cfg.EmailSettings.SMTPServer == "" {
return model.NewAppError("testEmail", "api.admin.test_email.missing_server", nil, i18n.T("api.context.invalid_param.app_error", map[string]any{"Name": "SMTPServer"}), http.StatusBadRequest)
}
// if the user hasn't changed their email settings, fill in the actual SMTP password so that
// the user can verify an existing SMTP connection
if *cfg.EmailSettings.SMTPPassword == model.FakeSetting {
if *cfg.EmailSettings.SMTPServer == *a.Config().EmailSettings.SMTPServer &&
*cfg.EmailSettings.SMTPPort == *a.Config().EmailSettings.SMTPPort &&
*cfg.EmailSettings.SMTPUsername == *a.Config().EmailSettings.SMTPUsername {
*cfg.EmailSettings.SMTPPassword = *a.Config().EmailSettings.SMTPPassword
} else {
return model.NewAppError("testEmail", "api.admin.test_email.reenter_password", nil, "", http.StatusBadRequest)
}
}
user, err := a.GetUser(userID)
if err != nil {
return err
}
T := i18n.GetUserTranslations(user.Locale)
license := a.Srv().License()
mailConfig := a.Srv().MailServiceConfig()
if err := mail.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", ""); err != nil {
return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
return nil
}
func (a *App) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) {
var cachedLatestVersion *model.GithubReleaseInfo
if cacheErr := latestVersionCache.Get("latest_version_cache", &cachedLatestVersion); cacheErr == nil {
return cachedLatestVersion, nil
}
res, err := http.Get(latestVersionUrl)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
defer res.Body.Close()
responseData, err := io.ReadAll(res.Body)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
var releaseInfoResponse *model.GithubReleaseInfo
err = json.Unmarshal(responseData, &releaseInfoResponse)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if validErr := releaseInfoResponse.IsValid(); validErr != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(validErr)
}
err = latestVersionCache.Set("latest_version_cache", releaseInfoResponse)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return releaseInfoResponse, nil
}
func (a *App) ClearLatestVersionCache() {
latestVersionCache.Remove("latest_version_cache")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
systemDataList, nErr := a.Srv().Store().System().Get()
if nErr != nil {
return nil, model.NewAppError("GetWarnMetricsStatus", "app.system.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
isE0Edition := model.BuildEnterpriseReady == "true" // license == nil was already validated upstream
result := map[string]*model.WarnMetricStatus{}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WarnMetricStatusStorePrefix) {
if warnMetric, ok := model.WarnMetricsTable[key]; ok {
if !warnMetric.IsBotOnly && (value == model.WarnMetricStatusRunonce || value == model.WarnMetricStatusLimitReached) {
result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil, isE0Edition)
}
}
}
}
return result, nil
}
func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc, isE0Edition bool) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) {
var warnMetricStatus *model.WarnMetricStatus
var warnMetricDisplayTexts = &model.WarnMetricDisplayTexts{}
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
warnMetricStatus = &model.WarnMetricStatus{
Id: warnMetric.Id,
Limit: warnMetric.Limit,
Acked: false,
}
if T == nil {
mlog.Debug("No translation function")
return warnMetricStatus, nil
}
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.bot_response.notification_success.message")
switch warnMetricId {
case model.SystemWarnMetricNumberOfTeams5:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_teams_5.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_teams_5.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.notification_body")
}
case model.SystemWarnMetricMfa:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.mfa.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.mfa.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.mfa.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.notification_body")
}
case model.SystemWarnMetricEmailDomain:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.email_domain.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.email_domain.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.email_domain.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.notification_body")
}
case model.SystemWarnMetricNumberOfChannels50:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_channels_50.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_channels_50.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers100:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_100.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_100.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers200:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_200.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_200.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers300:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_300.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers500:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_500.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_500.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body")
}
case model.SystemWarnMetricNumberOfPosts2m:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_posts_2M.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_posts_2M.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.notification_body")
}
default:
mlog.Debug("Invalid metric id", mlog.String("id", warnMetricId))
return nil, nil
}
return warnMetricStatus, warnMetricDisplayTexts
}
return nil, nil
}
func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
data, nErr := a.Srv().Store().System().GetByName(warnMetric.Id)
if nErr == nil && data != nil && data.Value == model.WarnMetricStatusAck {
mlog.Debug("This metric warning has already been acknowledged", mlog.String("id", warnMetric.Id))
return nil
}
if !forceAck {
if *a.Config().EmailSettings.SMTPServer == "" {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, i18n.T("api.context.invalid_param.app_error", map[string]any{"Name": "SMTPServer"}), http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sender.Locale)
data := a.Srv().EmailService.NewEmailTemplateData(sender.Locale)
data.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
data.Props["ContactNameValue"] = sender.GetFullName()
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
data.Props["ContactEmailValue"] = sender.Email
//same definition as the active users count metric displayed in the SystemConsole Analytics section
registeredUsersCount, cerr := a.Srv().Store().User().Count(model.UserCountOptions{})
if cerr != nil {
mlog.Warn("Error retrieving the number of registered users", mlog.Err(cerr))
} else {
data.Props["RegisteredUsersHeader"] = T("api.templates.warn_metric_ack.body.registered_users_header")
data.Props["RegisteredUsersValue"] = registeredUsersCount
}
data.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header")
data.Props["SiteURL"] = a.GetSiteURL()
data.Props["TelemetryIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header")
data.Props["TelemetryIdValue"] = a.TelemetryId()
data.Props["Footer"] = T("api.templates.warn_metric_ack.footer")
warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false)
if warnMetricStatus == nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", nil, "", http.StatusInternalServerError)
}
subject := T("api.templates.warn_metric_ack.subject")
data.Props["Title"] = warnMetricDisplayTexts.EmailBody
mailConfig := a.Srv().MailServiceConfig()
body, err := a.Srv().TemplatesContainer().RenderToString("warn_metric_ack", data)
if err != nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
if err := mail.SendMailUsingConfig(model.MmSupportAdvisorAddress, subject, body, mailConfig, false, "", "", "", sender.Email, "NotifyAndSetWarnMetricAck"); err != nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
}
if err := a.setWarnMetricsStatusAndNotify(warnMetric.Id); err != nil {
return err
}
}
return nil
}
func (a *App) setWarnMetricsStatusAndNotify(warnMetricId string) *model.AppError {
// Ack all metric warnings on the server
if err := a.setWarnMetricsStatus(model.WarnMetricStatusAck); err != nil {
return err
}
// Inform client that this metric warning has been acked
message := model.NewWebSocketEvent(model.WebsocketWarnMetricStatusRemoved, "", "", "", nil, "")
message.Add("warnMetricId", warnMetricId)
a.Publish(message)
return nil
}
func (a *App) setWarnMetricsStatus(status string) *model.AppError {
mlog.Debug("Set monitoring status for all warn metrics", mlog.String("status", status))
for _, warnMetric := range model.WarnMetricsTable {
if err := a.setWarnMetricsStatusForId(warnMetric.Id, status); err != nil {
return err
}
}
return nil
}
func (a *App) setWarnMetricsStatusForId(warnMetricId string, status string) *model.AppError {
mlog.Debug("Store status for warn metric", mlog.String("warnMetricId", warnMetricId), mlog.String("status", status))
if err := a.Srv().Store().System().SaveOrUpdateWithWarnMetricHandling(&model.System{
Name: warnMetricId,
Value: status,
}); err != nil {
return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]any{"WarnMetricName": warnMetricId}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RequestLicenseAndAckWarnMetric(c *request.Context, warnMetricId string, isBot bool) *model.AppError {
if *a.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
currentUser, appErr := a.GetUser(c.Session().UserId)
if appErr != nil {
return appErr
}
registeredUsersCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.license.request_trial_license.fail_get_user_count.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Channels().RequestTrialLicense(c.Session().UserId, int(registeredUsersCount), true, true); err != nil {
// turn off warn metric warning even in case of StartTrial failure
if nerr := a.setWarnMetricsStatusAndNotify(warnMetricId); nerr != nil {
return nerr
}
return err
}
if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, currentUser, true, isBot); appErr != nil {
return appErr
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
)
func (a *App) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *model.AppError) {
skipIntensiveQueries := false
var systemUserCount int64
systemUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, model.NewAppError("GetAnalytics", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if systemUserCount > int64(*a.Config().AnalyticsSettings.MaxUsersForStatistics) {
mlog.Debug("More than limit users are on the system, intensive queries skipped", mlog.Int("limit", *a.Config().AnalyticsSettings.MaxUsersForStatistics))
skipIntensiveQueries = true
}
if name == "standard" {
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 11)
rows[0] = &model.AnalyticsRow{Name: "channel_open_count", Value: 0}
rows[1] = &model.AnalyticsRow{Name: "channel_private_count", Value: 0}
rows[2] = &model.AnalyticsRow{Name: "post_count", Value: 0}
rows[3] = &model.AnalyticsRow{Name: "unique_user_count", Value: 0}
rows[4] = &model.AnalyticsRow{Name: "team_count", Value: 0}
rows[5] = &model.AnalyticsRow{Name: "total_websocket_connections", Value: 0}
rows[6] = &model.AnalyticsRow{Name: "total_master_db_connections", Value: 0}
rows[7] = &model.AnalyticsRow{Name: "total_read_db_connections", Value: 0}
rows[8] = &model.AnalyticsRow{Name: "daily_active_users", Value: 0}
rows[9] = &model.AnalyticsRow{Name: "monthly_active_users", Value: 0}
rows[10] = &model.AnalyticsRow{Name: "inactive_user_count", Value: 0}
var g errgroup.Group
var openChannelsCount int64
g.Go(func() error {
var err error
if openChannelsCount, err = a.Srv().Store().Channel().AnalyticsTypeCount(teamID, model.ChannelTypeOpen); err != nil {
return model.NewAppError("GetAnalytics", "app.channel.analytics_type_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var privateChannelsCount int64
g.Go(func() error {
var err error
if privateChannelsCount, err = a.Srv().Store().Channel().AnalyticsTypeCount(teamID, model.ChannelTypePrivate); err != nil {
return model.NewAppError("GetAnalytics", "app.channel.analytics_type_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var usersCount int64
var inactiveUsersCount int64
if teamID == "" {
g.Go(func() error {
var err error
if inactiveUsersCount, err = a.Srv().Store().User().AnalyticsGetInactiveUsersCount(); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_get_inactive_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
} else {
g.Go(func() error {
var err error
if usersCount, err = a.Srv().Store().User().Count(model.UserCountOptions{TeamId: teamID}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
var postsCount int64
if !skipIntensiveQueries {
g.Go(func() error {
var err error
if postsCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
var teamsCount int64
g.Go(func() error {
var err error
if teamsCount, err = a.Srv().Store().Team().AnalyticsTeamCount(nil); err != nil {
return model.NewAppError("GetAnalytics", "app.team.analytics_team_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var dailyActiveUsersCount int64
g.Go(func() error {
var err error
if dailyActiveUsersCount, err = a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_daily_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var monthlyActiveUsersCount int64
g.Go(func() error {
var err error
if monthlyActiveUsersCount, err = a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_daily_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
if err := g.Wait(); err != nil {
return nil, err.(*model.AppError)
}
rows[0].Value = float64(openChannelsCount)
rows[1].Value = float64(privateChannelsCount)
if skipIntensiveQueries {
rows[2].Value = -1
} else {
rows[2].Value = float64(postsCount)
}
if teamID == "" {
rows[3].Value = float64(systemUserCount)
rows[10].Value = float64(inactiveUsersCount)
} else {
rows[10].Value = -1
rows[3].Value = float64(usersCount)
}
rows[4].Value = float64(teamsCount)
// If in HA mode then aggregate all the stats
if a.Cluster() != nil && *a.Config().ClusterSettings.Enable {
stats, err2 := a.Cluster().GetClusterStats()
if err2 != nil {
return nil, err2
}
totalSockets := a.TotalWebsocketConnections()
totalMasterDb := a.Srv().Store().TotalMasterDbConnections()
totalReadDb := a.Srv().Store().TotalReadDbConnections()
for _, stat := range stats {
totalSockets = totalSockets + stat.TotalWebsocketConnections
totalMasterDb = totalMasterDb + stat.TotalMasterDbConnections
totalReadDb = totalReadDb + stat.TotalReadDbConnections
}
rows[5].Value = float64(totalSockets)
rows[6].Value = float64(totalMasterDb)
rows[7].Value = float64(totalReadDb)
} else {
rows[5].Value = float64(a.TotalWebsocketConnections())
rows[6].Value = float64(a.Srv().Store().TotalMasterDbConnections())
rows[7].Value = float64(a.Srv().Store().TotalReadDbConnections())
}
rows[8].Value = float64(dailyActiveUsersCount)
rows[9].Value = float64(monthlyActiveUsersCount)
return rows, nil
} else if name == "bot_post_counts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsPostCountsByDay(&model.AnalyticsPostCountsOptions{
TeamId: teamID,
BotsOnly: true,
YesterdayOnly: false,
})
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_posts_count_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "post_counts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsPostCountsByDay(&model.AnalyticsPostCountsOptions{
TeamId: teamID,
BotsOnly: false,
YesterdayOnly: false,
})
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_posts_count_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "user_counts_with_posts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsUserCountsWithPostsByDay(teamID)
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_user_counts_posts_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "extra_counts" {
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
rows[0] = &model.AnalyticsRow{Name: "file_post_count", Value: 0}
rows[1] = &model.AnalyticsRow{Name: "hashtag_post_count", Value: 0}
rows[2] = &model.AnalyticsRow{Name: "incoming_webhook_count", Value: 0}
rows[3] = &model.AnalyticsRow{Name: "outgoing_webhook_count", Value: 0}
rows[4] = &model.AnalyticsRow{Name: "command_count", Value: 0}
rows[5] = &model.AnalyticsRow{Name: "session_count", Value: 0}
var g2 errgroup.Group
var incomingWebhookCount int64
g2.Go(func() error {
var err error
if incomingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_incoming_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var outgoingWebhookCount int64
g2.Go(func() error {
var err error
if outgoingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsOutgoingCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_outgoing_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var commandsCount int64
g2.Go(func() error {
var err error
if commandsCount, err = a.Srv().Store().Command().AnalyticsCommandCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.analytics.getanalytics.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var sessionsCount int64
g2.Go(func() error {
var err error
if sessionsCount, err = a.Srv().Store().Session().AnalyticsSessionCount(); err != nil {
return model.NewAppError("GetAnalytics", "app.session.analytics_session_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var filesCount int64
var hashtagsCount int64
if !skipIntensiveQueries {
g2.Go(func() error {
var err error
if filesCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveFile: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
g2.Go(func() error {
var err error
if hashtagsCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveHashtag: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
if err := g2.Wait(); err != nil {
return nil, err.(*model.AppError)
}
if skipIntensiveQueries {
rows[0].Value = -1
rows[1].Value = -1
} else {
rows[0].Value = float64(filesCount)
rows[1].Value = float64(hashtagsCount)
}
rows[2].Value = float64(incomingWebhookCount)
rows[3].Value = float64(outgoingWebhookCount)
rows[4].Value = float64(commandsCount)
rows[5].Value = float64(sessionsCount)
return rows, nil
}
return nil, nil
}
func (a *App) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetRecentlyActiveUsersForTeam(teamID, 0, 100, nil)
if err != nil {
return nil, model.NewAppError("GetRecentlyActiveUsersForTeam", "app.user.get_recently_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userMap := make(map[string]*model.User)
for _, user := range users {
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetRecentlyActiveUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetRecentlyActiveUsersForTeam(teamID, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetRecentlyActiveUsersForTeamPage", "app.user.get_recently_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetNewUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetNewUsersForTeam(teamID, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetNewUsersForTeamPage", "app.user.get_new_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// App is a pure functional component that does not have any fields, except Server.
// It is a request-scoped struct constructed every time a request hits the server,
// and its only purpose is to provide business logic to Server via its methods.
type App struct {
ch *Channels
}
func New(options ...AppOption) *App {
app := &App{}
for _, option := range options {
option(app)
}
return app
}
func (a *App) TelemetryId() string {
return a.Srv().TelemetryId()
}
func (s *Server) TemplatesContainer() *templates.Container {
return s.htmlTemplateWatcher
}
func (a *App) Handle404(w http.ResponseWriter, r *http.Request) {
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
mlog.Debug("not found handler triggered", mlog.String("path", r.URL.Path), mlog.Int("code", 404), mlog.String("ip", ipAddress))
if *a.Config().ServiceSettings.WebserverMode == "disabled" {
http.NotFound(w, r)
return
}
utils.RenderWebAppError(a.Config(), w, r, model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound), a.AsymmetricSigningKey())
}
func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) {
systemData, err := s.Store().System().GetByName(model.SystemFirstServerRunTimestampKey)
if err != nil {
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
value, err := strconv.ParseInt(systemData.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system_install_date.parse_int.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return value, nil
}
func (a *App) Channels() *Channels {
return a.ch
}
func (a *App) Srv() *Server {
return a.ch.srv
}
func (a *App) Log() *mlog.Logger {
return a.ch.srv.Log()
}
func (a *App) NotificationsLog() *mlog.Logger {
return a.ch.srv.NotificationsLog()
}
func (a *App) AccountMigration() einterfaces.AccountMigrationInterface {
return a.ch.AccountMigration
}
func (a *App) Cluster() einterfaces.ClusterInterface {
return a.ch.srv.platform.Cluster()
}
func (a *App) Compliance() einterfaces.ComplianceInterface {
return a.ch.Compliance
}
func (a *App) DataRetention() einterfaces.DataRetentionInterface {
return a.ch.DataRetention
}
func (a *App) SearchEngine() *searchengine.Broker {
return a.ch.srv.platform.SearchEngine
}
func (a *App) Ldap() einterfaces.LdapInterface {
return a.ch.Ldap
}
func (a *App) MessageExport() einterfaces.MessageExportInterface {
return a.ch.MessageExport
}
func (a *App) Metrics() einterfaces.MetricsInterface {
return a.ch.srv.GetMetrics()
}
func (a *App) Notification() einterfaces.NotificationInterface {
return a.ch.Notification
}
func (a *App) Saml() einterfaces.SamlInterface {
return a.ch.Saml
}
func (a *App) Cloud() einterfaces.CloudInterface {
return a.ch.srv.Cloud
}
func (a *App) HTTPService() httpservice.HTTPService {
return a.ch.srv.httpService
}
func (a *App) ImageProxy() *imageproxy.ImageProxy {
return a.ch.imageProxy
}
func (a *App) Timezones() *timezones.Timezones {
return a.ch.srv.timezones
}
func (a *App) License() *model.License {
return a.Srv().License()
}
func (a *App) DBHealthCheckWrite() error {
currentTime := strconv.FormatInt(time.Now().Unix(), 10)
return a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: a.dbHealthCheckKey(),
Value: currentTime,
})
}
func (a *App) DBHealthCheckDelete() error {
_, err := a.Srv().Store().System().PermanentDeleteByName(a.dbHealthCheckKey())
return err
}
func (a *App) dbHealthCheckKey() string {
return fmt.Sprintf("health_check_%s", a.GetClusterId())
}
func (a *App) CheckIntegrity() <-chan model.IntegrityCheckResult {
return a.Srv().Store().CheckIntegrity()
}
func (a *App) SetChannels(ch *Channels) {
a.ch = ch
}
func (a *App) SetServer(srv *Server) {
a.ch.srv = srv
}
func (a *App) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
return a.Srv().Store().Status().UpdateExpiredDNDStatuses()
}
// Ensure system service adapter implements `product.SystemService`
var _ product.SystemService = (*systemServiceAdapter)(nil)
// systemServiceAdapter provides a collection of system APIs for use by products.
type systemServiceAdapter struct {
server *Server
}
func (ssa *systemServiceAdapter) GetDiagnosticId() string {
return ssa.server.TelemetryId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"os/user"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
LevelAPI = mlog.LvlAuditAPI
LevelContent = mlog.LvlAuditContent
LevelPerms = mlog.LvlAuditPerms
LevelCLI = mlog.LvlAuditCLI
)
func (a *App) GetAudits(userID string, limit int) (model.Audits, *model.AppError) {
audits, err := a.Srv().Store().Audit().Get(userID, 0, limit)
if err != nil {
var outErr *store.ErrOutOfBounds
switch {
case errors.As(err, &outErr):
return nil, model.NewAppError("GetAudits", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetAudits", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return audits, nil
}
func (a *App) GetAuditsPage(userID string, page int, perPage int) (model.Audits, *model.AppError) {
audits, err := a.Srv().Store().Audit().Get(userID, page*perPage, perPage)
if err != nil {
var outErr *store.ErrOutOfBounds
switch {
case errors.As(err, &outErr):
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return audits, nil
}
// LogAuditRec logs an audit record using default LvlAuditCLI.
func (a *App) LogAuditRec(rec *audit.Record, err error) {
a.LogAuditRecWithLevel(rec, mlog.LvlAuditCLI, err)
}
// LogAuditRecWithLevel logs an audit record using specified Level.
func (a *App) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level, err error) {
if rec == nil {
return
}
if err != nil {
appErr, ok := err.(*model.AppError)
if ok {
rec.AddErrorCode(appErr.StatusCode)
}
rec.AddErrorDesc(appErr.Error())
rec.Fail()
}
a.Srv().Audit.LogRecord(level, *rec)
}
// MakeAuditRecord creates a audit record pre-populated with defaults.
func (a *App) MakeAuditRecord(event string, initialStatus string) *audit.Record {
var userID string
user, err := user.Current()
if err == nil {
userID = fmt.Sprintf("%s:%s", user.Uid, user.Username)
}
rec := &audit.Record{
EventName: event,
Status: initialStatus,
Meta: map[string]interface{}{
audit.KeyAPIPath: "",
audit.KeyClusterID: a.GetClusterId(),
},
Actor: audit.EventActor{
UserId: userID,
SessionId: "",
Client: fmt.Sprintf("server %s-%s", model.BuildNumber, model.BuildHash),
IpAddress: "",
},
EventData: audit.EventData{
Parameters: map[string]interface{}{},
PriorState: map[string]interface{}{},
ResultState: map[string]interface{}{},
ObjectType: "",
},
}
return rec
}
func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) error {
adt.OnQueueFull = s.onAuditTargetQueueFull
adt.OnError = s.onAuditError
var logConfigSrc config.LogConfigSrc
dsn := *s.platform.Config().ExperimentalAuditSettings.AdvancedLoggingConfig
if bAllowAdvancedLogging && dsn != "" {
var err error
logConfigSrc, err = config.NewLogConfigSrc(dsn, s.platform.GetConfigStore())
if err != nil {
return fmt.Errorf("invalid config source for audit, %w", err)
}
mlog.Debug("Loaded audit configuration", mlog.String("source", dsn))
}
// ExperimentalAuditSettings provides basic file audit (E0, E10); logConfigSrc provides advanced config (E20).
cfg, err := config.MloggerConfigFromAuditConfig(s.platform.Config().ExperimentalAuditSettings, logConfigSrc)
if err != nil {
return fmt.Errorf("invalid config for audit, %w", err)
}
return adt.Configure(cfg)
}
func (s *Server) onAuditTargetQueueFull(qname string, maxQSize int) bool {
mlog.Error("Audit queue full, dropping record.", mlog.String("qname", qname), mlog.Int("queueSize", maxQSize))
return true // drop it
}
func (s *Server) onAuditError(err error) {
mlog.Error("Audit Error", mlog.Err(err))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
)
type TokenLocation int
const (
TokenLocationNotFound TokenLocation = iota
TokenLocationHeader
TokenLocationCookie
TokenLocationQueryString
TokenLocationCloudHeader
TokenLocationRemoteClusterHeader
)
func (tl TokenLocation) String() string {
switch tl {
case TokenLocationNotFound:
return "Not Found"
case TokenLocationHeader:
return "Header"
case TokenLocationCookie:
return "Cookie"
case TokenLocationQueryString:
return "QueryString"
case TokenLocationCloudHeader:
return "CloudHeader"
case TokenLocationRemoteClusterHeader:
return "RemoteClusterHeader"
default:
return "Unknown"
}
}
func (a *App) IsPasswordValid(password string) *model.AppError {
if err := users.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings); err != nil {
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("User.IsValid", invErr.Id(), map[string]any{"Min": *a.Config().PasswordSettings.MinimumLength}, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("User.IsValid", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
if err := users.CheckUserPassword(user, password); err != nil {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized).Wrap(err)
default:
return model.NewAppError("checkUserPassword", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.CheckUserMfa(user, mfaToken); err != nil {
// If the mfaToken is not set, we assume the client used this as a pre-flight request to query the server
// about the MFA state of the user in question
if mfaToken != "" {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
}
a.InvalidateCacheForUser(user.Id)
return err
}
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
return err
}
return nil
}
// This to be used for places we check the users password when they are already logged in
func (a *App) DoubleCheckPassword(user *model.User, password string) *model.AppError {
if err := checkUserLoginAttempts(user, *a.Config().ServiceSettings.MaximumLoginAttempts); err != nil {
return err
}
if err := users.CheckUserPassword(user, password); err != nil {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("DoubleCheckPassword", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("DoubleCheckPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized).Wrap(err)
default:
return model.NewAppError("DoubleCheckPassword", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0); passErr != nil {
return model.NewAppError("DoubleCheckPassword", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) checkLdapUserPasswordAndAllCriteria(c *request.Context, ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) {
if a.Ldap() == nil || ldapId == nil {
err := model.NewAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return nil, err
}
ldapUser, err := a.Ldap().DoLogin(c, *ldapId, password)
if err != nil {
err.StatusCode = http.StatusUnauthorized
return nil, err
}
if err := a.CheckUserMfa(ldapUser, mfaToken); err != nil {
return nil, err
}
if err := checkUserNotDisabled(ldapUser); err != nil {
return nil, err
}
// user successfully authenticated
return ldapUser, nil
}
func (a *App) CheckUserAllAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
return err
}
return nil
}
func (a *App) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := checkUserNotDisabled(user); err != nil {
return err
}
if err := checkUserNotBot(user); err != nil {
return err
}
if err := checkUserLoginAttempts(user, *a.Config().ServiceSettings.MaximumLoginAttempts); err != nil {
return err
}
return nil
}
func (a *App) CheckUserPostflightAuthenticationCriteria(user *model.User) *model.AppError {
if !user.EmailVerified && *a.Config().EmailSettings.RequireEmailVerification {
return model.NewAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func (a *App) CheckUserMfa(user *model.User, token string) *model.AppError {
if !user.MfaActive || !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return nil
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return model.NewAppError("CheckUserMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
ok, err := mfa.New(a.Srv().Store().User()).ValidateToken(user.MfaSecret, token)
if err != nil {
return model.NewAppError("CheckUserMfa", "mfa.validate_token.authenticate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if !ok {
return model.NewAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "", http.StatusUnauthorized)
}
return nil
}
func checkUserLoginAttempts(user *model.User, max int) *model.AppError {
if user.FailedAttempts >= max {
return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func checkUserNotDisabled(user *model.User) *model.AppError {
if user.DeleteAt > 0 {
return model.NewAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func checkUserNotBot(user *model.User) *model.AppError {
if user.IsBot {
return model.NewAppError("Login", "api.user.login.bot_login_forbidden.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func (a *App) authenticateUser(c *request.Context, user *model.User, password, mfaToken string) (*model.User, *model.AppError) {
license := a.Srv().License()
ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap() != nil && license != nil && *license.Features.LDAP
if user.AuthService == model.UserAuthServiceLdap {
if !ldapAvailable {
err := model.NewAppError("login", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return user, err
}
ldapUser, err := a.checkLdapUserPasswordAndAllCriteria(c, user.AuthData, password, mfaToken)
if err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
}
// slightly redundant to get the user again, but we need to get it from the LDAP server
return ldapUser, nil
}
if user.AuthService != "" {
authService := user.AuthService
if authService == model.UserAuthServiceSaml {
authService = strings.ToUpper(authService)
}
err := model.NewAppError("login", "api.user.login.use_auth_service.app_error", map[string]any{"AuthService": authService}, "", http.StatusBadRequest)
return user, err
}
if err := a.CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
}
return user, nil
}
func ParseAuthTokenFromRequest(r *http.Request) (token string, loc TokenLocation) {
defer func() {
// Stripping off tokens of large sizes
// to prevent logging a large string.
if len(token) > 50 {
token = token[:50]
}
}()
authHeader := r.Header.Get(model.HeaderAuth)
// Attempt to parse the token from the cookie
if cookie, err := r.Cookie(model.SessionCookieToken); err == nil {
return cookie.Value, TokenLocationCookie
}
// Parse the token from the header
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HeaderBearer {
// Default session token
return authHeader[7:], TokenLocationHeader
}
if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HeaderToken {
// OAuth token
return authHeader[6:], TokenLocationHeader
}
// Attempt to parse token out of the query string
if token := r.URL.Query().Get("access_token"); token != "" {
return token, TokenLocationQueryString
}
if token := r.Header.Get(model.HeaderCloudToken); token != "" {
return token, TokenLocationCloudHeader
}
if token := r.Header.Get(model.HeaderRemoteclusterToken); token != "" {
return token, TokenLocationRemoteClusterHeader
}
return "", TokenLocationNotFound
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql"
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError {
permissionsStr := "permission="
for _, permission := range permissions {
permissionsStr += permission.Id
permissionsStr += ","
}
return model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+s.UserId+", "+permissionsStr, http.StatusForbidden)
}
func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
if session.IsUnrestricted() {
return true
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool {
for _, perm := range permissions {
if a.SessionHasPermissionTo(session, perm) {
return true
}
}
return false
}
func (a *App) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool {
if teamID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
teamMember := session.GetTeamByTeamId(teamID)
if teamMember != nil {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
// SessionHasPermissionToTeams returns true only if user has access to all teams.
func (a *App) SessionHasPermissionToTeams(c request.CTX, session model.Session, teamIDs []string, permission *model.Permission) bool {
if len(teamIDs) == 0 {
return true
}
for _, teamID := range teamIDs {
if teamID == "" {
return false
}
}
if session.IsUnrestricted() {
return true
}
// Getting the list of unique roles from all teams.
var roles []string
uniqueRoles := make(map[string]bool)
for _, teamID := range teamIDs {
tm := session.GetTeamByTeamId(teamID)
if tm != nil {
for _, role := range tm.GetRoles() {
uniqueRoles[role] = true
}
}
}
for role := range uniqueRoles {
roles = append(roles, role)
}
if a.RolesGrantPermission(roles, permission.Id) {
return true
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToChannel(c request.CTX, session model.Session, channelID string, permission *model.Permission) bool {
if channelID == "" {
return false
}
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
if err == nil {
if roles, ok := ids[channelID]; ok {
channelRoles = strings.Fields(roles)
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
}
}
channel, appErr := a.GetChannel(c, channelID)
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
return false
}
if session.IsUnrestricted() {
return true
}
if appErr == nil && channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
return a.SessionHasPermissionTo(session, permission)
}
// SessionHasPermissionToChannels returns true only if user has access to all channels.
func (a *App) SessionHasPermissionToChannels(c request.CTX, session model.Session, channelIDs []string, permission *model.Permission) bool {
if len(channelIDs) == 0 {
return true
}
for _, channelID := range channelIDs {
if channelID == "" {
return false
}
}
if session.IsUnrestricted() {
return true
}
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
uniqueRoles := make(map[string]bool)
if err == nil {
for _, channelID := range channelIDs {
if roles, ok := ids[channelID]; ok {
for _, role := range strings.Fields(roles) {
uniqueRoles[role] = true
}
}
}
}
for role := range uniqueRoles {
channelRoles = append(channelRoles, role)
}
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
channels, appErr := a.GetChannels(c, channelIDs)
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
return false
}
// Get TeamIDs from channels
uniqueTeamIDs := make(map[string]bool)
for _, ch := range channels {
if ch.TeamId != "" {
uniqueTeamIDs[ch.TeamId] = true
}
}
var teamIDs []string
for teamID := range uniqueTeamIDs {
teamIDs = append(teamIDs, teamID)
}
if appErr == nil && len(teamIDs) > 0 {
return a.SessionHasPermissionToTeams(c, session, teamIDs, permission)
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool {
groupMember, err := a.Srv().Store().Group().GetMember(groupID, session.UserId)
// don't reject immediately on ErrNoRows error because there's further authz logic below for non-groupmembers
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false
}
// each member of a group is implicitly considered to have the 'custom_group_user' role in that group, so if the user is a member of the
// group and custom_group_user on their system has the requested permission then return true
if groupMember != nil && a.RolesGrantPermission([]string{model.CustomGroupUserRoleId}, permission.Id) {
return true
}
// Not implemented: group-override schemes.
// ...otherwise check their system roles to see if they have the requested permission system-wide
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, session.UserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store().Channel().GetForPost(postID); err == nil {
if channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToCategory(c request.CTX, session model.Session, userID, teamID, categoryId string) bool {
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
category, err := a.GetSidebarCategory(c, categoryId)
return err == nil && category != nil && category.UserId == session.UserId && category.UserId == userID && category.TeamId == teamID
}
func (a *App) SessionHasPermissionToUser(session model.Session, userID string) bool {
if userID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
if session.UserId == userID {
return true
}
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) SessionHasPermissionToUserOrBot(session model.Session, userID string) bool {
if session.IsUnrestricted() {
return true
}
if a.SessionHasPermissionToUser(session, userID) {
return true
}
if err := a.SessionHasPermissionToManageBot(session, userID); err == nil {
return true
}
return false
}
func (a *App) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
user, err := a.GetUser(askingUserId)
if err != nil {
return false
}
roles := user.GetRoles()
return a.RolesGrantPermission(roles, permission.Id)
}
func (a *App) HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool {
if teamID == "" || askingUserId == "" {
return false
}
teamMember, _ := a.GetTeamMember(teamID, askingUserId)
if teamMember != nil && teamMember.DeleteAt == 0 {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool {
if channelID == "" || askingUserId == "" {
return false
}
channelMember, err := a.GetChannelMember(c, channelID, askingUserId)
if err == nil {
roles := channelMember.GetRoles()
if a.RolesGrantPermission(roles, permission.Id) {
return true
}
}
var channel *model.Channel
channel, err = a.GetChannel(c, channelID)
if err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, askingUserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store().Channel().GetForPost(postID); err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToUser(askingUserId string, userID string) bool {
if askingUserId == userID {
return true
}
if a.HasPermissionTo(askingUserId, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool {
roles, err := a.GetRolesByNames(roleNames)
if err != nil {
// This should only happen if something is very broken. We can't realistically
// recover the situation, so deny permission and log an error.
mlog.Error("Failed to get roles from database with role names: "+strings.Join(roleNames, ",")+" ", mlog.Err(err))
return false
}
for _, role := range roles {
if role.DeleteAt != 0 {
continue
}
permissions := role.Permissions
for _, permission := range permissions {
if permission == permissionId {
return true
}
}
}
return false
}
// SessionHasPermissionToManageBot returns nil if the session has access to manage the given bot.
// This function deviates from other authorization checks in returning an error instead of just
// a boolean, allowing the permission failure to be exposed with more granularity.
func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError {
existingBot, err := a.GetBot(botUserId, true)
if err != nil {
return err
}
if session.IsUnrestricted() {
return nil
}
if existingBot.OwnerId == session.UserId {
if !a.SessionHasPermissionTo(session, model.PermissionManageBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadBots) {
// If the user doesn't have permission to read bots, pretend as if
// the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageBots})
}
} else {
if !a.SessionHasPermissionTo(session, model.PermissionManageOthersBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadOthersBots) {
// If the user doesn't have permission to read others' bots,
// pretend as if the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageOthersBots})
}
}
return nil
}
func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool {
return a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(userID, channel.TeamId, model.PermissionReadPublicChannel))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
)
// check if there is any auto_response type post in channel by the user in a calender day
func (a *App) checkIfRespondedToday(createdAt int64, channelId, userId string) (bool, error) {
y, m, d := model.GetTimeForMillis(createdAt).Date()
since := model.GetMillisForTime(time.Date(y, m, d, 0, 0, 0, 0, time.UTC))
return a.Srv().Store().Post().HasAutoResponsePostByUserSince(
model.GetPostsSinceOptions{ChannelId: channelId, Time: since},
userId,
)
}
func (a *App) SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError) {
if channel.Type != model.ChannelTypeDirect {
return false, nil
}
if sender.IsBot {
return false, nil
}
receiverId := channel.GetOtherUserIdForDM(sender.Id)
if receiverId == "" {
// User direct messaged themself, let them test their auto-responder.
receiverId = sender.Id
}
receiver, aErr := a.GetUser(receiverId)
if aErr != nil {
return false, aErr
}
autoResponded, err := a.checkIfRespondedToday(post.CreateAt, post.ChannelId, receiverId)
if err != nil {
return false, model.NewAppError("SendAutoResponseIfNecessary", "app.user.send_auto_response.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if autoResponded {
return false, nil
}
return a.SendAutoResponse(c, channel, receiver, post)
}
func (a *App) SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError) {
if receiver == nil || receiver.NotifyProps == nil {
return false, nil
}
active := receiver.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
message := receiver.NotifyProps[model.AutoResponderMessageNotifyProp]
if !active || message == "" {
return false, nil
}
rootID := post.Id
if post.RootId != "" {
rootID = post.RootId
}
autoResponderPost := &model.Post{
ChannelId: channel.Id,
Message: message,
RootId: rootID,
Type: model.PostTypeAutoResponder,
UserId: receiver.Id,
}
if _, err := a.CreatePost(c, autoResponderPost, channel, false, false); err != nil {
return false, err
}
return true, nil
}
func (a *App) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
active := user.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
oldActive := oldNotifyProps[model.AutoResponderActiveNotifyProp] == "true"
autoResponderEnabled := !oldActive && active
autoResponderDisabled := oldActive && !active
if autoResponderEnabled {
a.SetStatusOutOfOffice(user.Id)
} else if autoResponderDisabled {
a.SetStatusOnline(user.Id, true)
}
}
func (a *App) DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
active := user.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
if active {
patch := &model.UserPatch{}
patch.NotifyProps = user.NotifyProps
patch.NotifyProps[model.AutoResponderActiveNotifyProp] = "false"
_, err := a.PatchUser(c, userID, patch, asAdmin)
if err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
internalKeyPrefix = "mmi_"
botUserKey = internalKeyPrefix + "botid"
)
// Ensure bot service wrapper implements `product.BotService`
var _ product.BotService = (*botServiceWrapper)(nil)
// botServiceWrapper provides an implementation of `product.BotService` for use by products.
type botServiceWrapper struct {
app AppIface
}
func (w *botServiceWrapper) EnsureBot(c *request.Context, productID string, bot *model.Bot) (string, error) {
return w.app.EnsureBot(c, productID, bot)
}
// EnsureBot provides similar functionality with the plugin-api BotService. It doesn't accept
// any ensureBotOptions hence it is not required for now.
// TODO: Once the focalboard migration completed, we should add this logic to the app and
// let plugin-api use the same code
func (a *App) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
if bot == nil {
return "", errors.New("passed a nil bot")
}
if bot.Username == "" {
return "", errors.New("passed a bot with no username")
}
botIDBytes, err := a.GetPluginKey(productID, botUserKey)
if err != nil {
return "", err
}
// If the bot has already been created, use it
if botIDBytes != nil {
botID := string(botIDBytes)
// ensure existing bot is synced with what is being created
botPatch := &model.BotPatch{
Username: &bot.Username,
DisplayName: &bot.DisplayName,
Description: &bot.Description,
}
if _, err = a.PatchBot(botID, botPatch); err != nil {
return "", fmt.Errorf("failed to patch bot: %w", err)
}
return botID, nil
}
// Check for an existing bot user with that username. If one exists, then use that.
if user, appErr := a.GetUserByUsername(bot.Username); appErr == nil && user != nil {
if user.IsBot {
if appErr := a.SetPluginKey(productID, botUserKey, []byte(user.Id)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}
} else {
c.Logger().Error("Product attempted to use an account that already exists. Convert user to a bot "+
"account in the CLI by running 'mattermost user convert <username> --bot'. If the user is an "+
"existing user account you want to preserve, change its username and restart the Mattermost server, "+
"after which the plugin will create a bot account with that name. For more information about bot "+
"accounts, see https://mattermost.com/pl/default-bot-accounts", mlog.String("username",
bot.Username),
mlog.String("user_id",
user.Id),
)
}
return user.Id, nil
}
createdBot, err := a.CreateBot(c, bot)
if err != nil {
return "", fmt.Errorf("failed to create bot: %w", err)
}
if appErr := a.SetPluginKey(productID, botUserKey, []byte(createdBot.UserId)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}
return createdBot.UserId, nil
}
// CreateBot creates the given bot and corresponding user.
func (a *App) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
vErr := bot.IsValidCreate()
if vErr != nil {
return nil, vErr
}
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(bot))
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
code := ""
switch invErr.Field {
case "email":
code = "app.user.save.email_exists.app_error"
case "username":
code = "app.user.save.username_exists.app_error"
default:
code = "app.user.save.existing.app_error"
}
return nil, model.NewAppError("CreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
bot.UserId = user.Id
savedBot, nErr := a.Srv().Store().Bot().Save(bot)
if nErr != nil {
a.Srv().Store().User().PermanentDelete(bot.UserId)
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Get the owner of the bot, if one exists. If not, don't send a message
ownerUser, err := a.Srv().Store().User().Get(context.Background(), bot.OwnerId)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return nil, model.NewAppError("CreateBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if ownerUser != nil {
// Send a message to the bot's creator to inform them that the bot needs to be added
// to a team and channel after it's created
botOwner, err := a.GetUser(bot.OwnerId)
if err != nil {
return nil, err
}
channel, err := a.getOrCreateDirectChannelWithUser(c, user, botOwner)
if err != nil {
return nil, err
}
T := i18n.GetUserTranslations(ownerUser.Locale)
botAddPost := &model.Post{
Type: model.PostTypeAddBotTeamsChannels,
UserId: savedBot.UserId,
ChannelId: channel.Id,
Message: T("api.bot.teams_channels.add_message_mobile"),
}
if _, err := a.CreatePostAsUser(c, botAddPost, c.Session().Id, true); err != nil {
return nil, err
}
}
return savedBot, nil
}
func (a *App) GetWarnMetricsBot() (*model.Bot, *model.AppError) {
perPage := 1
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return nil, err
}
if len(sysAdminList) == 0 {
return nil, model.NewAppError("GetWarnMetricsBot", "app.bot.get_warn_metrics_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
warnMetricsBot := &model.Bot{
Username: model.BotWarnMetricBotUsername,
DisplayName: T("app.system.warn_metric.bot_displayname"),
Description: "",
OwnerId: sysAdminList[0].Id,
}
return a.getOrCreateBot(warnMetricsBot)
}
func (a *App) GetSystemBot() (*model.Bot, *model.AppError) {
perPage := 1
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return nil, err
}
if len(sysAdminList) == 0 {
return nil, model.NewAppError("GetSystemBot", "app.bot.get_system_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
systemBot := &model.Bot{
Username: model.BotSystemBotUsername,
DisplayName: T("app.system.system_bot.bot_displayname"),
Description: "",
OwnerId: sysAdminList[0].Id,
}
return a.getOrCreateBot(systemBot)
}
func (a *App) getOrCreateBot(botDef *model.Bot) (*model.Bot, *model.AppError) {
botUser, appErr := a.GetUserByUsername(botDef.Username)
if appErr != nil {
if appErr.StatusCode != http.StatusNotFound {
return nil, appErr
}
// cannot find this bot user, save the user
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(botDef))
if nErr != nil {
var appError *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appError):
return nil, appError
case errors.As(nErr, &invErr):
code := ""
switch invErr.Field {
case "email":
code = "app.user.save.email_exists.app_error"
case "username":
code = "app.user.save.username_exists.app_error"
default:
code = "app.user.save.existing.app_error"
}
return nil, model.NewAppError("getOrCreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("getOrCreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
botDef.UserId = user.Id
//save the bot
savedBot, nErr := a.Srv().Store().Bot().Save(botDef)
if nErr != nil {
a.Srv().Store().User().PermanentDelete(savedBot.UserId)
var nAppErr *model.AppError
switch {
case errors.As(nErr, &nAppErr): // in case we haven't converted to plain error.
return nil, nAppErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return savedBot, nil
}
if botUser == nil {
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError)
}
//return the bot for this user
savedBot, appErr := a.GetBot(botUser.Id, false)
if appErr != nil {
return nil, appErr
}
return savedBot, nil
}
// PatchBot applies the given patch to the bot and corresponding user.
func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
bot, err := a.GetBot(botUserId, true)
if err != nil {
return nil, err
}
if !bot.WouldPatch(botPatch) {
return bot, nil
}
bot.Patch(botPatch)
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
patchedUser := model.UserFromBot(bot)
user.Id = patchedUser.Id
user.Username = patchedUser.Username
user.Email = patchedUser.Email
user.FirstName = patchedUser.FirstName
userUpdate, nErr := a.Srv().Store().User().Update(user, true)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var conErr *store.ErrConflict
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
return nil, model.NewAppError("PatchBot", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &conErr):
if conErr.Resource == "Username" {
return nil, model.NewAppError("PatchBot", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
return nil, model.NewAppError("PatchBot", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.InvalidateCacheForUser(user.Id)
ruser := userUpdate.New
a.sendUpdatedUserEvent(*ruser)
bot, nErr = a.Srv().Store().Bot().Update(bot)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return bot, nil
}
// GetBot returns the given bot.
func (a *App) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Get(botUserId, includeDeleted)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("GetBot", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// GetBots returns the requested page of bots.
func (a *App) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) {
bots, err := a.Srv().Store().Bot().GetAll(options)
if err != nil {
return nil, model.NewAppError("GetBots", "app.bot.getbots.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bots, nil
}
// UpdateBotActive marks a bot as active or inactive, along with its corresponding user.
func (a *App) UpdateBotActive(c request.CTX, botUserId string, active bool) (*model.Bot, *model.AppError) {
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if _, err := a.UpdateActive(c, user, active); err != nil {
return nil, err
}
bot, nErr := a.Srv().Store().Bot().Get(botUserId, true)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("UpdateBotActive", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
changed := true
if active && bot.DeleteAt != 0 {
bot.DeleteAt = 0
} else if !active && bot.DeleteAt == 0 {
bot.DeleteAt = model.GetMillis()
} else {
changed = false
}
if changed {
bot, nErr = a.Srv().Store().Bot().Update(bot)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
return bot, nil
}
// PermanentDeleteBot permanently deletes a bot and its corresponding user.
func (a *App) PermanentDeleteBot(botUserId string) *model.AppError {
if err := a.Srv().Store().Bot().PermanentDelete(botUserId); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteBot", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("PatchBot", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().User().PermanentDelete(botUserId); err != nil {
return model.NewAppError("PermanentDeleteBot", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// UpdateBotOwner changes a bot's owner to the given value.
func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Get(botUserId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("UpdateBotOwner", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
bot.OwnerId = newOwnerId
bot, err = a.Srv().Store().Bot().Update(bot)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// disableUserBots disables all bots owned by the given user.
func (a *App) disableUserBots(c request.CTX, userID string) *model.AppError {
perPage := 20
for {
options := &model.BotGetOptions{
OwnerId: userID,
IncludeDeleted: false,
OnlyOrphaned: false,
Page: 0,
PerPage: perPage,
}
userBots, err := a.GetBots(options)
if err != nil {
return err
}
for _, bot := range userBots {
_, err := a.UpdateBotActive(c, bot.UserId, false)
if err != nil {
c.Logger().Warn("Unable to deactivate bot.", mlog.String("bot_user_id", bot.UserId), mlog.Err(err))
}
}
// Get next set of bots if we got the max number of bots
if len(userBots) == perPage {
options.Page += 1
continue
}
break
}
return nil
}
func (a *App) notifySysadminsBotOwnerDeactivated(c request.CTX, userID string) *model.AppError {
perPage := 25
botOptions := &model.BotGetOptions{
OwnerId: userID,
IncludeDeleted: false,
OnlyOrphaned: false,
Page: 0,
PerPage: perPage,
}
// get owner bots
var userBots []*model.Bot
for {
bots, err := a.GetBots(botOptions)
if err != nil {
return err
}
userBots = append(userBots, bots...)
if len(bots) < perPage {
break
}
botOptions.Page += 1
}
// user does not own bots
if len(userBots) == 0 {
return nil
}
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
// get sysadmins
var sysAdmins []*model.User
for {
sysAdminsList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return err
}
sysAdmins = append(sysAdmins, sysAdminsList...)
if len(sysAdminsList) < perPage {
break
}
userOptions.Page += 1
}
// user being disabled
user, err := a.GetUser(userID)
if err != nil {
return err
}
// for each sysadmin, notify user that owns bots was disabled
for _, sysAdmin := range sysAdmins {
channel, appErr := a.GetOrCreateDirectChannel(c, sysAdmin.Id, sysAdmin.Id)
if appErr != nil {
return appErr
}
post := &model.Post{
UserId: sysAdmin.Id,
ChannelId: channel.Id,
Message: a.getDisableBotSysadminMessage(user, userBots),
Type: model.PostTypeSystemGeneric,
}
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
return appErr
}
}
return nil
}
func (a *App) getDisableBotSysadminMessage(user *model.User, userBots model.BotList) string {
disableBotsSetting := *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated
var printAllBots = true
numBotsToPrint := len(userBots)
if numBotsToPrint > 10 {
numBotsToPrint = 10
printAllBots = false
}
var message, botList string
for _, bot := range userBots[:numBotsToPrint] {
botList += fmt.Sprintf("* %v\n", bot.Username)
}
T := i18n.GetUserTranslations(user.Locale)
message = T("app.bot.get_disable_bot_sysadmin_message",
map[string]any{
"UserName": user.Username,
"NumBots": len(userBots),
"BotNames": botList,
"disableBotsSetting": disableBotsSetting,
"printAllBots": printAllBots,
})
return message
}
// ConvertUserToBot converts a user to bot.
func (a *App) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Save(model.BotFromUser(user))
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"mime/multipart"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
BrandFilePath = "brand/"
BrandFileName = "image.png"
)
func (a *App) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("SaveBrandImage", "api.admin.upload_brand_image.storage.app_error", nil, "", http.StatusNotImplemented)
}
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
if err = checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.check_image_limits.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
img, _, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
t := time.Now()
a.MoveFile(BrandFilePath+BrandFileName, BrandFilePath+t.Format("2006-01-02T15:04:05")+".png")
if _, err := a.WriteFile(buf, BrandFilePath+BrandFileName); err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.save_image.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetBrandImage() ([]byte, *model.AppError) {
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "", http.StatusNotImplemented)
}
img, err := a.ReadFile(BrandFilePath + BrandFileName)
if err != nil {
return nil, err
}
return img, nil
}
func (a *App) DeleteBrandImage() *model.AppError {
filePath := BrandFilePath + BrandFileName
fileExists, err := a.FileExists(filePath)
if err != nil {
return err
}
if !fileExists {
return model.NewAppError("DeleteBrandImage", "api.admin.delete_brand_image.storage.not_found", nil, "", http.StatusNotFound)
}
return a.RemoveFile(filePath)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
const (
TimestampFormat = "Mon Jan 2 15:04:05 -0700 MST 2006"
)
// Busy represents the busy state of the server. A server marked busy
// will have non-critical services disabled. If a Cluster is provided
// any changes will be propagated to each node.
type Busy struct {
busy int32 // protected via atomic for fast IsBusy calls
mux sync.RWMutex
timer *time.Timer
expires time.Time
cluster einterfaces.ClusterInterface
}
// NewBusy creates a new Busy instance with optional cluster which will
// be notified of busy state changes.
func NewBusy(cluster einterfaces.ClusterInterface) *Busy {
return &Busy{cluster: cluster}
}
// IsBusy returns true if the server has been marked as busy.
func (b *Busy) IsBusy() bool {
if b == nil {
return false
}
return atomic.LoadInt32(&b.busy) != 0
}
// Set marks the server as busy for dur duration and notifies cluster nodes.
func (b *Busy) Set(dur time.Duration) {
b.mux.Lock()
defer b.mux.Unlock()
// minimum 1 second
if dur < (time.Second * 1) {
dur = time.Second * 1
}
b.setWithoutNotify(dur)
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: true, Expires: b.expires.Unix(), ExpiresTS: b.expires.UTC().Format(TimestampFormat)}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) setWithoutNotify(dur time.Duration) {
b.clearWithoutNotify()
atomic.StoreInt32(&b.busy, 1)
b.expires = time.Now().Add(dur)
b.timer = time.AfterFunc(dur, func() {
b.mux.Lock()
b.clearWithoutNotify()
b.mux.Unlock()
})
}
// ClearBusy marks the server as not busy and notifies cluster nodes.
func (b *Busy) Clear() {
b.mux.Lock()
defer b.mux.Unlock()
b.clearWithoutNotify()
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: false, Expires: time.Time{}.Unix(), ExpiresTS: ""}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) clearWithoutNotify() {
if b.timer != nil {
b.timer.Stop() // don't drain timer.C channel for AfterFunc timers.
}
b.timer = nil
b.expires = time.Time{}
atomic.StoreInt32(&b.busy, 0)
}
// Expires returns the expected time that the server
// will be marked not busy. This expiry can be extended
// via additional calls to SetBusy.
func (b *Busy) Expires() time.Time {
b.mux.RLock()
defer b.mux.RUnlock()
return b.expires
}
// notifyServerBusyChange informs all cluster members of a server busy state change.
func (b *Busy) notifyServerBusyChange(sbs *model.ServerBusyState) {
if b.cluster == nil {
return
}
buf, _ := json.Marshal(sbs)
msg := &model.ClusterMessage{
Event: model.ClusterEventBusyStateChanged,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
b.cluster.SendClusterMessage(msg)
}
// ClusterEventChanged is called when a CLUSTER_EVENT_BUSY_STATE_CHANGED is received.
func (b *Busy) ClusterEventChanged(sbs *model.ServerBusyState) {
b.mux.Lock()
defer b.mux.Unlock()
if sbs.Busy {
expires := time.Unix(sbs.Expires, 0)
dur := time.Until(expires)
if dur > 0 {
b.setWithoutNotify(dur)
}
} else {
b.clearWithoutNotify()
}
}
func (b *Busy) ToJSON() ([]byte, error) {
b.mux.RLock()
defer b.mux.RUnlock()
sbs := &model.ServerBusyState{
Busy: atomic.LoadInt32(&b.busy) != 0,
Expires: b.expires.Unix(),
ExpiresTS: b.expires.UTC().Format(TimestampFormat),
}
sbsJSON, jsonErr := json.Marshal(sbs)
if jsonErr != nil {
return []byte{}, fmt.Errorf("failed to encode server busy state to JSON: %w", jsonErr)
}
return sbsJSON, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// channelsWrapper provides an implementation of `product.ChannelService` to be used by products.
type channelsWrapper struct {
app *App
}
func (s *channelsWrapper) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.getDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// GetChannelByID gets a Channel by its ID.
func (s *channelsWrapper) GetChannelByID(channelID string) (*model.Channel, *model.AppError) {
return s.app.GetChannel(request.EmptyContext(s.app.Log()), channelID)
}
// GetChannelMember gets a channel member by userID.
func (s *channelsWrapper) GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError) {
return s.app.GetChannelMember(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
return s.app.GetChannelsForTeamForUser(request.EmptyContext(s.app.Log()), teamID, userID, opts)
}
func (s *channelsWrapper) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return s.app.GetSidebarCategoriesForTeamForUser(request.EmptyContext(s.app.Log()), userID, teamID)
}
func (s *channelsWrapper) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return s.app.GetChannelMembersPage(request.EmptyContext(s.app.Log()), channelID, page, perPage)
}
func (s *channelsWrapper) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.CreateSidebarCategory(request.EmptyContext(s.app.Log()), userID, teamID, newCategory)
}
func (s *channelsWrapper) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.UpdateSidebarCategories(request.EmptyContext(s.app.Log()), userID, teamID, categories)
}
func (s *channelsWrapper) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return s.app.CreateChannel(request.EmptyContext(s.app.Log()), channel, false)
}
func (s *channelsWrapper) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
ctx := request.EmptyContext(s.app.Log())
channel, err := s.app.GetChannel(ctx, channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (s *channelsWrapper) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return s.app.UpdateChannelMemberRoles(request.EmptyContext(s.app.Log()), channelID, userID, newRoles)
}
func (s *channelsWrapper) DeleteChannelMember(channelID, userID string) *model.AppError {
return s.app.LeaveChannel(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := s.GetChannelByID(channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(request.EmptyContext(s.app.Log()), userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (s *channelsWrapper) GetDirectChannelOrCreate(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.GetOrCreateDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// Ensure the wrapper implements the product service.
var _ product.ChannelService = (*channelsWrapper)(nil)
// DefaultChannelNames returns the list of system-wide default channel names.
//
// By default the list will be (not necessarily in this order):
//
// ['town-square', 'off-topic']
//
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
//
// ['town-square', 'game-of-thrones', 'wow']
func (a *App) DefaultChannelNames(c request.CTX) []string {
names := []string{"town-square"}
if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 {
names = append(names, "off-topic")
} else {
seenChannels := map[string]bool{"town-square": true}
for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels {
if !seenChannels[channelName] {
names = append(names, channelName)
seenChannels[channelName] = true
}
}
}
return names
}
func (a *App) JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
var requestor *model.User
var nErr error
if userRequestorId != "" {
requestor, nErr = a.Srv().Store().User().Get(context.Background(), userRequestorId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("JoinDefaultChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("JoinDefaultChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, channelName := range a.DefaultChannelNames(c) {
channel, channelErr := a.Srv().Store().Channel().GetByName(teamID, channelName, true)
if channelErr != nil {
c.Logger().Warn("No default channel with this name", mlog.String("channelName", channelName), mlog.String("teamID", teamID), mlog.Err(channelErr))
continue
}
if channel.Type != model.ChannelTypeOpen {
continue
}
cm := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: shouldBeAdmin,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = a.Srv().Store().Channel().SaveMember(cm)
if histErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); histErr != nil {
return model.NewAppError("JoinDefaultChannels", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(histErr)
}
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
if aErr := a.postJoinMessageForDefaultChannel(c, user, requestor, channel); aErr != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(aErr))
}
}
a.invalidateCacheForChannelMembers(channel.Id)
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "")
message.Add("user_id", user.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
// A/B Test on the welcome post
if a.Config().FeatureFlags.SendWelcomePost && channelName == model.DefaultChannelName {
nbTeams, err := a.Srv().Store().Team().AnalyticsTeamCount(&model.TeamSearch{
IncludeDeleted: model.NewBool(true),
})
if err != nil {
c.Logger().Warn("unable to get number of teams", logr.Err(err))
return nil
}
if nbTeams == 1 && a.IsFirstAdmin(user) {
// Post the welcome message
if _, err := a.CreatePost(c, &model.Post{
ChannelId: channel.Id,
Type: model.PostTypeWelcomePost,
UserId: user.Id,
}, channel, false, false); err != nil {
c.Logger().Warn("unable to post welcome message", logr.Err(err))
return nil
}
ts := a.Srv().GetTelemetryService()
if ts != nil {
ts.SendTelemetry("welcome-message-sent", map[string]any{
"category": "growth",
})
}
}
}
}
if nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
if cErr.Resource == "ChannelMembers" {
return model.NewAppError("JoinDefaultChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("JoinDefaultChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (a *App) postJoinMessageForDefaultChannel(c request.CTX, user *model.User, requestor *model.User, channel *model.Channel) *model.AppError {
if channel.Name == model.DefaultChannelName {
if requestor == nil {
if err := a.postJoinTeamMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.postAddToTeamMessage(c, requestor, user, channel, ""); err != nil {
return err
}
}
} else {
if requestor == nil {
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.PostAddToChannelMessage(c, requestor, user, channel, ""); err != nil {
return err
}
}
}
return nil
}
func (a *App) CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
if channel.IsGroupOrDirect() {
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest)
}
if channel.TeamId == "" {
return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.no_team_id.app_error", nil, "", http.StatusBadRequest)
}
// Get total number of channels on current team
count, err := a.GetNumberOfChannelsOnTeam(c, channel.TeamId)
if err != nil {
return nil, err
}
if int64(count+1) > *a.Config().TeamSettings.MaxChannelsPerTeam {
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.max_channel_limit.app_error", map[string]any{"MaxChannelsPerTeam": *a.Config().TeamSettings.MaxChannelsPerTeam}, "", http.StatusBadRequest)
}
channel.CreatorId = userID
rchannel, err := a.CreateChannel(c, channel, true)
if err != nil {
return nil, err
}
var user *model.User
if user, err = a.GetUser(userID); err != nil {
return nil, err
}
a.postJoinChannelMessage(c, user, channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", userID, nil, "")
message.Add("channel_id", channel.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
return rchannel, nil
}
// RenameChannel is used to rename the channel Name and the DisplayName fields
func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
if channel.Type == model.ChannelTypeDirect {
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_direct_messages.app_error", nil, "", http.StatusBadRequest)
}
if channel.Type == model.ChannelTypeGroup {
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest)
}
channel.Name = newChannelName
if newDisplayName != "" {
channel.DisplayName = newDisplayName
}
newChannel, err := a.UpdateChannel(c, channel)
if err != nil {
return nil, err
}
return newChannel, nil
}
func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
channel.DisplayName = strings.TrimSpace(channel.DisplayName)
sc, nErr := a.Srv().Store().Channel().Save(channel, *a.Config().TeamSettings.MaxChannelsPerTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
return sc, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, <Err):
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if addMember {
user, nErr := a.Srv().Store().User().Get(context.Background(), channel.CreatorId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
cm := &model.ChannelMember{
ChannelId: sc.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: true,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
if _, nErr := a.Srv().Store().Channel().SaveMember(cm); nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return nil, model.NewAppError("CreateChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(channel.CreatorId, sc.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("CreateChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(channel.CreatorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ChannelHasBeenCreated(pluginContext, sc)
return true
}, plugin.ChannelHasBeenCreatedID)
})
return sc, nil
}
func (a *App) GetOrCreateDirectChannel(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
channel, nErr := a.getDirectChannel(c, userID, otherUserID)
if nErr != nil {
return nil, nErr
}
if channel != nil {
return channel, nil
}
if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam &&
!a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem) {
users, err := a.GetUsersByIds([]string{userID, otherUserID}, &store.UserGetByIdsOpts{})
if err != nil {
return nil, err
}
var isBot bool
for _, user := range users {
if user.IsBot {
isBot = true
break
}
}
// if one of the users is a bot, don't restrict to team members
if !isBot {
commonTeamIDs, err := a.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
return nil, err
}
if len(commonTeamIDs) == 0 {
return nil, model.NewAppError("createDirectChannel", "api.channel.create_channel.direct_channel.team_restricted_error", nil, "", http.StatusForbidden)
}
}
}
channel, err := a.createDirectChannel(c, userID, otherUserID, channelOptions...)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
a.handleCreationEvent(c, userID, otherUserID, channel)
return channel, nil
}
func (a *App) getOrCreateDirectChannelWithUser(c request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) {
channel, nErr := a.getDirectChannel(c, user.Id, otherUser.Id)
if nErr != nil {
return nil, nErr
}
if channel != nil {
return channel, nil
}
channel, err := a.createDirectChannelWithUser(c, user, otherUser)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
a.handleCreationEvent(c, user.Id, otherUser.Id, channel)
return channel, nil
}
func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, channel *model.Channel) {
a.InvalidateCacheForUser(userID)
a.InvalidateCacheForUser(otherUserID)
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ChannelHasBeenCreated(pluginContext, channel)
return true
}, plugin.ChannelHasBeenCreatedID)
})
message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "")
message.Add("creator_id", userID)
message.Add("teammate_id", otherUserID)
a.Publish(message)
}
func (a *App) createDirectChannel(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
users, err := a.Srv().Store().User().GetMany(context.Background(), []string{userID, otherUserID})
if err != nil {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(users) == 0 {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
}
// We are doing this because we allow a user to create a direct channel with themselves
if userID == otherUserID {
users = append(users, users[0])
}
// After we counted for direct channels with the same user, if we do not have two users then we failed to find one
if len(users) != 2 {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
}
// The potential swap dance below is necessary in order to guarantee determinism when creating a direct channel.
// When we query the database for some given user ids, the database result is not deterministic, meaning we can get
// the same results but in different order. In order to conform the contract of Channel.CreateDirectChannel method
// below we need to identify which user is who.
user := users[0]
otherUser := users[1]
if user.Id != userID {
user = users[1]
otherUser = users[0]
}
return a.createDirectChannelWithUser(c, user, otherUser, channelOptions...)
}
func (a *App) createDirectChannelWithUser(c request.CTX, user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
channel, nErr := a.Srv().Store().Channel().CreateDirectChannel(user, otherUser, channelOptions...)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_direct_channel.not_direct.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "Channel":
return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case "ChannelMembers":
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, <Err):
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if user.Id != otherUser.Id {
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(otherUser.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// When the newly created channel is shared and the creator is local
// create a local shared channel record
if channel.IsShared() && !user.IsRemote() {
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: true,
ReadOnly: false,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: user.Id,
Type: channel.Type,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
return nil, model.NewAppError("CreateDirectChannel", "app.sharedchannel.dm_channel_creation.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) {
channel, err := a.createGroupChannel(c, userIDs)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
for _, userID := range userIDs {
a.InvalidateCacheForUser(userID)
}
message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, "", nil, "")
message.Add("teammate_ids", model.ArrayToJSON(userIDs))
a.Publish(message)
return channel, nil
}
func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, true)
if err != nil {
return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) != len(userIDs) {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIDs),
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
Type: model.ChannelTypeGroup,
}
channel, nErr := a.Srv().Store().Channel().Save(group, *a.Config().TeamSettings.MaxChannelsPerTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, <Err):
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
for _, user := range users {
cm := &model.ChannelMember{
UserId: user.Id,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
if _, nErr = a.Srv().Store().Channel().SaveMember(cm); nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return nil, model.NewAppError("createGroupChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createGroupChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, true)
if err != nil {
return nil, model.NewAppError("GetGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) != len(userIDs) {
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
}
channel, appErr := a.GetChannelByName(c, model.GetGroupNameFromUserIds(userIDs), "", true)
if appErr != nil {
return nil, appErr
}
return channel, nil
}
// UpdateChannel updates a given channel by its Id. It also publishes the CHANNEL_UPDATED event.
func (a *App) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
_, err := a.Srv().Store().Channel().Update(channel)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, "", channel.Id, "", nil, "")
channelJSON, jsonErr := json.Marshal(channel)
if jsonErr != nil {
return nil, model.NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("channel", string(channelJSON))
a.Publish(messageWs)
return channel, nil
}
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
func (a *App) CreateChannelScheme(c request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
scheme, err := a.CreateScheme(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Scope: model.SchemeScopeChannel,
})
if err != nil {
return nil, err
}
channel.SchemeId = &scheme.Id
if _, err := a.UpdateChannelScheme(c, channel); err != nil {
return nil, err
}
return scheme, nil
}
// DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil.
func (a *App) DeleteChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
if channel.SchemeId != nil && *channel.SchemeId != "" {
if _, err := a.DeleteScheme(*channel.SchemeId); err != nil {
return nil, err
}
}
channel.SchemeId = nil
return a.UpdateChannelScheme(c, channel)
}
// UpdateChannelScheme saves the new SchemeId of the channel passed.
func (a *App) UpdateChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
var oldChannel *model.Channel
var err *model.AppError
if oldChannel, err = a.GetChannel(c, channel.Id); err != nil {
return nil, err
}
oldChannel.SchemeId = channel.SchemeId
return a.UpdateChannel(c, oldChannel)
}
func (a *App) UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
channel, err := a.UpdateChannel(c, oldChannel)
if err != nil {
return channel, err
}
if err := a.postChannelPrivacyMessage(c, user, channel); err != nil {
if channel.Type == model.ChannelTypeOpen {
channel.Type = model.ChannelTypePrivate
} else {
channel.Type = model.ChannelTypeOpen
}
// revert to previous channel privacy
a.UpdateChannel(c, channel)
return channel, err
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", channel.Id)
a.Publish(messageWs)
return channel, nil
}
func (a *App) postChannelPrivacyMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
var authorId string
var authorUsername string
if user != nil {
authorId = user.Id
authorUsername = user.Username
} else {
systemBot, err := a.GetSystemBot()
if err != nil {
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
authorId = systemBot.UserId
authorUsername = systemBot.Username
}
message := (map[model.ChannelType]string{
model.ChannelTypeOpen: i18n.T("api.channel.change_channel_privacy.private_to_public"),
model.ChannelTypePrivate: i18n.T("api.channel.change_channel_privacy.public_to_private"),
})[channel.Type]
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeChangeChannelPrivacy,
UserId: authorId,
Props: model.StringInterface{
"username": authorUsername,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
if channel.DeleteAt == 0 {
return nil, model.NewAppError("restoreChannel", "api.channel.restore_channel.restored.app_error", nil, "", http.StatusBadRequest)
}
if err := a.Srv().Store().Channel().Restore(channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("RestoreChannel", "app.channel.restore.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channel.DeleteAt = 0
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelRestored, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
a.Publish(message)
var user *model.User
if userID != "" {
var nErr error
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("RestoreChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("RestoreChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
if user != nil {
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: T("api.channel.restore_channel.unarchived", map[string]any{"Username": user.Username}),
Type: model.PostTypeChannelRestored,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post unarchive message", mlog.Err(err))
}
} else {
a.Srv().Go(func() {
systemBot, err := a.GetSystemBot()
if err != nil {
c.Logger().Error("Failed to post unarchive message", mlog.Err(err))
return
}
post := &model.Post{
ChannelId: channel.Id,
Message: i18n.T("api.channel.restore_channel.unarchived", map[string]any{"Username": systemBot.Username}),
Type: model.PostTypeChannelRestored,
UserId: systemBot.UserId,
Props: model.StringInterface{
"username": systemBot.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Error("Failed to post unarchive message", mlog.Err(err))
}
})
}
return channel, nil
}
func (a *App) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
oldChannelDisplayName := channel.DisplayName
oldChannelHeader := channel.Header
oldChannelPurpose := channel.Purpose
channel.Patch(patch)
channel, err := a.UpdateChannel(c, channel)
if err != nil {
return nil, err
}
if oldChannelDisplayName != channel.DisplayName {
if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
c.Logger().Warn(err.Error())
}
}
if channel.Header != oldChannelHeader {
if err = a.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, channel.Header); err != nil {
c.Logger().Warn(err.Error())
}
}
if channel.Purpose != oldChannelPurpose {
if err = a.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, channel.Purpose); err != nil {
c.Logger().Warn(err.Error())
}
}
return channel, nil
}
// GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles.
func (a *App) GetSchemeRolesForChannel(c request.CTX, channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
channel, err := a.GetChannel(c, channelID)
if err != nil {
return
}
if channel.SchemeId != nil && *channel.SchemeId != "" {
var scheme *model.Scheme
scheme, err = a.GetScheme(*channel.SchemeId)
if err != nil {
return
}
guestRoleName = scheme.DefaultChannelGuestRole
userRoleName = scheme.DefaultChannelUserRole
adminRoleName = scheme.DefaultChannelAdminRole
return
}
return a.GetTeamSchemeChannelRoles(c, channel.TeamId)
}
// GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names.
func (a *App) GetTeamSchemeChannelRoles(c request.CTX, teamID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return
}
if team.SchemeId != nil && *team.SchemeId != "" {
var scheme *model.Scheme
scheme, err = a.GetScheme(*team.SchemeId)
if err != nil {
return
}
guestRoleName = scheme.DefaultChannelGuestRole
userRoleName = scheme.DefaultChannelUserRole
adminRoleName = scheme.DefaultChannelAdminRole
} else {
guestRoleName = model.ChannelGuestRoleId
userRoleName = model.ChannelUserRoleId
adminRoleName = model.ChannelAdminRoleId
}
return
}
// GetChannelModerationsForChannel Gets a channels ChannelModerations from either the higherScoped roles or from the channel scheme roles.
func (a *App) GetChannelModerationsForChannel(c request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(c, channel.Id)
if err != nil {
return nil, err
}
memberRole, err := a.GetRoleByName(context.Background(), memberRoleName)
if err != nil {
return nil, err
}
var guestRole *model.Role
if guestRoleName != "" {
guestRole, err = a.GetRoleByName(context.Background(), guestRoleName)
if err != nil {
return nil, err
}
}
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId)
if err != nil {
return nil, err
}
higherScopedMemberRole, err := a.GetRoleByName(context.Background(), higherScopedMemberRoleName)
if err != nil {
return nil, err
}
var higherScopedGuestRole *model.Role
if higherScopedGuestRoleName != "" {
higherScopedGuestRole, err = a.GetRoleByName(context.Background(), higherScopedGuestRoleName)
if err != nil {
return nil, err
}
}
return buildChannelModerations(c, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
}
// PatchChannelModerationsForChannel Updates a channels scheme roles based on a given ChannelModerationPatch, if the permissions match the higher scoped role the scheme is deleted.
func (a *App) PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId)
if err != nil {
return nil, err
}
ctx := sqlstore.WithMaster(context.Background())
higherScopedMemberRole, err := a.GetRoleByName(ctx, higherScopedMemberRoleName)
if err != nil {
return nil, err
}
var higherScopedGuestRole *model.Role
if higherScopedGuestRoleName != "" {
higherScopedGuestRole, err = a.GetRoleByName(ctx, higherScopedGuestRoleName)
if err != nil {
return nil, err
}
}
higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type)
var higherScopedGuestPermissions map[string]bool
if higherScopedGuestRole != nil {
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type)
}
for _, moderationPatch := range channelModerationsPatch {
if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] {
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
}
if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] {
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
}
}
var scheme *model.Scheme
// Channel has no scheme so create one
if channel.SchemeId == nil || *channel.SchemeId == "" {
scheme, err = a.CreateChannelScheme(c, channel)
if err != nil {
return nil, err
}
// Send a websocket event about this new role. The other new roles—member and guest—get emitted when they're updated.
var adminRole *model.Role
adminRole, err = a.GetRoleByName(ctx, scheme.DefaultChannelAdminRole)
if err != nil {
return nil, err
}
if appErr := a.sendUpdatedRoleEvent(adminRole); appErr != nil {
return nil, appErr
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
a.Publish(message)
c.Logger().Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
scheme, err = a.GetScheme(*channel.SchemeId)
if err != nil {
return nil, err
}
}
guestRoleName := scheme.DefaultChannelGuestRole
memberRoleName := scheme.DefaultChannelUserRole
memberRole, err := a.GetRoleByName(ctx, memberRoleName)
if err != nil {
return nil, err
}
var guestRole *model.Role
if guestRoleName != "" {
guestRole, err = a.GetRoleByName(ctx, guestRoleName)
if err != nil {
return nil, err
}
}
memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members")
var guestRolePatch *model.RolePatch
if guestRole != nil {
guestRolePatch = guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests")
}
for _, channelModerationPatch := range channelModerationsPatch {
permissionModified := *channelModerationPatch.Name
if channelModerationPatch.Roles.Guests != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch)) {
if *channelModerationPatch.Roles.Guests {
c.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
c.Logger().Info("Permission disabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
}
}
if channelModerationPatch.Roles.Members != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch)) {
if *channelModerationPatch.Roles.Members {
c.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
c.Logger().Info("Permission disabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
}
}
}
memberRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedMemberRole, memberRolePatch)) == 0
guestRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedGuestRole, guestRolePatch)) == 0
if memberRolePermissionsUnmodified && guestRolePermissionsUnmodified {
// The channel scheme matches the permissions of its higherScoped scheme so delete the scheme
if _, err = a.DeleteChannelScheme(c, channel); err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
a.Publish(message)
memberRole = higherScopedMemberRole
guestRole = higherScopedGuestRole
c.Logger().Info("Permission scheme deleted.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
memberRole, err = a.PatchRole(memberRole, memberRolePatch)
if err != nil {
return nil, err
}
guestRole, err = a.PatchRole(guestRole, guestRolePatch)
if err != nil {
return nil, err
}
}
cErr := a.forEachChannelMember(c, channel.Id, func(channelMember model.ChannelMember) error {
a.Srv().Store().Channel().InvalidateAllChannelMembersForUser(channelMember.UserId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(channelMember)
if jsonErr != nil {
return jsonErr
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return nil
})
if cErr != nil {
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations.cache_invalidation.error", nil, "", http.StatusInternalServerError).Wrap(cErr)
}
return buildChannelModerations(c, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
}
func buildChannelModerations(c request.CTX, channelType model.ChannelType, memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration {
var memberPermissions, guestPermissions, higherScopedMemberPermissions, higherScopedGuestPermissions map[string]bool
if memberRole != nil {
memberPermissions = memberRole.GetChannelModeratedPermissions(channelType)
}
if guestRole != nil {
guestPermissions = guestRole.GetChannelModeratedPermissions(channelType)
}
if higherScopedMemberRole != nil {
higherScopedMemberPermissions = higherScopedMemberRole.GetChannelModeratedPermissions(channelType)
}
if higherScopedGuestRole != nil {
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channelType)
}
var channelModerations []*model.ChannelModeration
for _, permissionKey := range model.ChannelModeratedPermissions {
roles := &model.ChannelModeratedRoles{}
roles.Members = &model.ChannelModeratedRole{
Value: memberPermissions[permissionKey],
Enabled: higherScopedMemberPermissions[permissionKey],
}
if permissionKey == "manage_members" {
roles.Guests = nil
} else {
roles.Guests = &model.ChannelModeratedRole{
Value: guestPermissions[permissionKey],
Enabled: higherScopedGuestPermissions[permissionKey],
}
}
moderation := &model.ChannelModeration{
Name: permissionKey,
Roles: roles,
}
channelModerations = append(channelModerations, moderation)
}
return channelModerations
}
func (a *App) UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
if member, err = a.GetChannelMember(c, channelID, userID); err != nil {
return nil, err
}
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(c, channelID)
if err != nil {
return nil, err
}
prevSchemeGuestValue := member.SchemeGuest
var newExplicitRoles []string
member.SchemeGuest = false
member.SchemeUser = false
member.SchemeAdmin = false
for _, roleName := range strings.Fields(newRoles) {
var role *model.Role
role, err = a.GetRoleByName(context.Background(), roleName)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
if !role.SchemeManaged {
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
newExplicitRoles = append(newExplicitRoles, roleName)
} else {
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
switch roleName {
case schemeAdminRole:
member.SchemeAdmin = true
case schemeUserRole:
member.SchemeUser = true
case schemeGuestRole:
member.SchemeGuest = true
default:
// If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
}
}
}
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
if prevSchemeGuestValue != member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
}
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
return a.updateChannelMember(c, member)
}
func (a *App) UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
member, err := a.GetChannelMember(c, channelID, userID)
if err != nil {
return nil, err
}
member.SchemeAdmin = isSchemeAdmin
member.SchemeUser = isSchemeUser
member.SchemeGuest = isSchemeGuest
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
// If the migration is not completed, we also need to check the default channel_admin/channel_user roles are not present in the roles field.
if err = a.IsPhase2MigrationCompleted(); err != nil {
member.ExplicitRoles = RemoveRoles([]string{model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId}, member.ExplicitRoles)
}
return a.updateChannelMember(c, member)
}
func (a *App) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
filteredProps := make(map[string]string)
// update whichever notify properties have been provided, but don't change the others
if markUnread, exists := data[model.MarkUnreadNotifyProp]; exists {
filteredProps[model.MarkUnreadNotifyProp] = markUnread
}
if desktop, exists := data[model.DesktopNotifyProp]; exists {
filteredProps[model.DesktopNotifyProp] = desktop
}
if desktop_threads, exists := data[model.DesktopThreadsNotifyProp]; exists {
filteredProps[model.DesktopThreadsNotifyProp] = desktop_threads
}
if email, exists := data[model.EmailNotifyProp]; exists {
filteredProps[model.EmailNotifyProp] = email
}
if push, exists := data[model.PushNotifyProp]; exists {
filteredProps[model.PushNotifyProp] = push
}
if push_threads, exists := data[model.PushThreadsNotifyProp]; exists {
filteredProps[model.PushThreadsNotifyProp] = push_threads
}
if ignoreChannelMentions, exists := data[model.IgnoreChannelMentionsNotifyProp]; exists {
filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions
}
member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("updateMemberNotifyProps", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(member.UserId)
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
// Notify the clients that the member notify props changed
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("UpdateChannelMemberNotifyProps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return member, nil
}
func (a *App) updateChannelMember(c request.CTX, member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
member, err := a.Srv().Store().Channel().UpdateMember(member)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("updateChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(member.UserId)
// Notify the clients that the member notify props changed
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("updateChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return member, nil
}
func (a *App) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
ihc := make(chan store.StoreResult, 1)
ohc := make(chan store.StoreResult, 1)
go func() {
webhooks, err := a.Srv().Store().Webhook().GetIncomingByChannel(channel.Id)
ihc <- store.StoreResult{Data: webhooks, NErr: err}
close(ihc)
}()
go func() {
outgoingHooks, err := a.Srv().Store().Webhook().GetOutgoingByChannel(channel.Id, -1, -1)
ohc <- store.StoreResult{Data: outgoingHooks, NErr: err}
close(ohc)
}()
var user *model.User
if userID != "" {
var nErr error
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("DeleteChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("DeleteChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
ihcresult := <-ihc
if ihcresult.NErr != nil {
return model.NewAppError("DeleteChannel", "app.webhooks.get_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ihcresult.NErr)
}
ohcresult := <-ohc
if ohcresult.NErr != nil {
return model.NewAppError("DeleteChannel", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ohcresult.NErr)
}
incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
if channel.DeleteAt > 0 {
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "", http.StatusBadRequest)
return err
}
if channel.Name == model.DefaultChannelName {
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return err
}
if user != nil {
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
Type: model.PostTypeChannelDeleted,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
}
} else {
systemBot, err := a.GetSystemBot()
if err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
} else {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.channel.delete_channel.archived"), systemBot.Username),
Type: model.PostTypeChannelDeleted,
UserId: systemBot.UserId,
Props: model.StringInterface{
"username": systemBot.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
}
}
}
now := model.GetMillis()
for _, hook := range incomingHooks {
if err := a.Srv().Store().Webhook().DeleteIncoming(hook.Id, now); err != nil {
c.Logger().Warn("Encountered error deleting incoming webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
}
a.Srv().Platform().InvalidateCacheForWebhook(hook.Id)
}
for _, hook := range outgoingHooks {
if err := a.Srv().Store().Webhook().DeleteOutgoing(hook.Id, now); err != nil {
c.Logger().Warn("Encountered error deleting outgoing webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
}
}
deleteAt := model.GetMillis()
if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
message.Add("delete_at", deleteAt)
a.Publish(message)
return nil
}
func (a *App) addUserToChannel(c request.CTX, user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
if channel.Type != model.ChannelTypeOpen && channel.Type != model.ChannelTypePrivate {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
}
channelMember, nErr := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, user.Id)
if nErr != nil {
var nfErr *store.ErrNotFound
if !errors.As(nErr, &nfErr) {
return nil, model.NewAppError("AddUserToChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
} else {
return channelMember, nil
}
if channel.IsGroupConstrained() {
nonMembers, err := a.FilterNonGroupChannelMembers([]string{user.Id}, channel)
if err != nil {
return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError)
}
if len(nonMembers) > 0 {
return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
}
}
newMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel)
if appErr != nil {
return nil, appErr
}
newMember.SchemeAdmin = userShouldBeAdmin
}
newMember, nErr = a.Srv().Store().Channel().SaveMember(newMember)
if nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to add member: %v, user_id: %s, channel_id: %s", nErr, user.Id, channel.Id), http.StatusInternalServerError)
}
if nErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForChannelMembers(channel.Id)
return newMember, nil
}
// AddUserToChannel adds a user to a given channel.
func (a *App) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
if !skipTeamMemberIntegrityCheck {
teamMember, nErr := a.Srv().Store().Team().GetMember(context.Background(), channel.TeamId, user.Id)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if teamMember.DeleteAt > 0 {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "", http.StatusBadRequest)
}
}
newMember, err := a.addUserToChannel(c, user, channel)
if err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "")
message.Add("user_id", user.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
return newMember, nil
}
type ChannelMemberOpts struct {
UserRequestorID string
PostRootID string
// SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked
// that a user has already been removed from that team or not.
// This is useful to avoid in scenarios when we just added the team member,
// and thereby know that there is no need to check this.
SkipTeamMemberIntegrityCheck bool
}
// AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel.
func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
if member, err := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, userID); err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return nil, model.NewAppError("AddChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
} else {
return member, nil
}
var user *model.User
var err *model.AppError
if user, err = a.GetUser(userID); err != nil {
return nil, err
}
var userRequestor *model.User
if opts.UserRequestorID != "" {
if userRequestor, err = a.GetUser(opts.UserRequestorID); err != nil {
return nil, err
}
}
cm, err := a.AddUserToChannel(c, user, channel, opts.SkipTeamMemberIntegrityCheck)
if err != nil {
return nil, err
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
return true
}, plugin.UserHasJoinedChannelID)
})
if opts.UserRequestorID == "" || userID == opts.UserRequestorID {
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return nil, err
}
} else {
a.Srv().Go(func() {
a.PostAddToChannelMessage(c, userRequestor, user, channel, opts.PostRootID)
})
}
return cm, nil
}
func (a *App) AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError {
var profiles []*model.User
options := &model.UserGetOptions{InTeamId: teamID, Page: 0, PerPage: 100}
profiles, err := a.Srv().Store().User().GetProfiles(options)
if err != nil {
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
}
var preferences model.Preferences
for _, profile := range profiles {
if profile.Id == user.Id {
continue
}
preference := model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: profile.Id,
Value: "true",
}
preferences = append(preferences, preference)
if len(preferences) >= 10 {
break
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
}
return nil
}
func (a *App) PostUpdateChannelHeaderMessage(c request.CTX, userID string, channel *model.Channel, oldChannelHeader, newChannelHeader string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
var message string
if oldChannelHeader == "" {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
} else if newChannelHeader == "" {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
} else {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeHeaderChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_header": oldChannelHeader,
"new_header": newChannelHeader,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
var message string
if oldChannelPurpose == "" {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
} else if newChannelPurpose == "" {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
} else {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypePurposeChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_purpose": oldChannelPurpose,
"new_purpose": newChannelPurpose,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostUpdateChannelDisplayNameMessage(c request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
message := fmt.Sprintf(i18n.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeDisplaynameChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_displayname": oldChannelDisplayName,
"new_displayname": newChannelDisplayName,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
return a.Srv().getChannel(c, channelID)
}
func (s *Server) getChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
channel, err := s.Store().Channel().Get(channelID, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetChannels(c request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetMany(channelIDs, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channels, nil
}
func (a *App) GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
var channel *model.Channel
var err error
if includeDeleted {
channel, err = a.Srv().Store().Channel().GetByNameIncludeDeleted(teamID, channelName, false)
} else {
channel, err = a.Srv().Store().Channel().GetByName(teamID, channelName, false)
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetChannelsByNames(c request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetByNames(teamID, channelNames, true)
if err != nil {
return nil, model.NewAppError("GetChannelsByNames", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetChannelByNameForTeamName(c request.CTX, channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) {
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamName)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
}
var result *model.Channel
var nErr error
if includeDeleted {
result, nErr = a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, channelName, false)
} else {
result, nErr = a.Srv().Store().Channel().GetByName(team.Id, channelName, false)
}
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return result, nil
}
func (s *Server) getChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
list, err := s.Store().Channel().GetChannels(teamID, userID, opts)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
return a.Srv().getChannelsForTeamForUser(c, teamID, userID, opts)
}
func (a *App) GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetChannelsWithCursor(teamID, userID, opts, afterChannelID)
if err != nil {
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetAllChannels(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
IncludePolicyID: opts.IncludePolicyID,
}
channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts)
if err != nil {
return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
}
count, err := a.Srv().Store().Channel().GetAllChannelsCount(storeOpts)
if err != nil {
return 0, model.NewAppError("GetAllChannelsCount", "app.channel.get_all_channels_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetDeleted(teamID, offset, limit, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetMoreChannels(teamID, userID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetChannelsUserNotIn", "app.channel.get_more_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPublicChannelsByIdsForTeam(teamID, channelIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetPublicChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPublicChannelsForTeam(teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetPublicChannelsForTeam", "app.channel.get_public_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPrivateChannelsForTeam(teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetPrivateChannelsForTeam", "app.channel.get_private_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
return a.Srv().getChannelMember(c, channelID, userID)
}
func (s *Server) getChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
channelMember, err := s.Store().Channel().GetMember(c.Context(), channelID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channelMember, nil
}
func (a *App) GetChannelMembersPage(c request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetChannelMembersTimezones(c request.CTX, channelID string) ([]string, *model.AppError) {
membersTimezones, err := a.Srv().Store().Channel().GetChannelMembersTimezones(channelID)
if err != nil {
return nil, model.NewAppError("GetChannelMembersTimezones", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var timezones []string
for _, membersTimezone := range membersTimezones {
if membersTimezone["automaticTimezone"] == "" && membersTimezone["manualTimezone"] == "" {
continue
}
timezones = append(timezones, model.GetPreferredTimezone(membersTimezone))
}
return model.RemoveDuplicateStrings(timezones), nil
}
func (a *App) GetChannelMembersByIds(c request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
members, err := a.Srv().Store().Channel().GetMembersByIds(channelID, userIDs)
if err != nil {
return nil, model.NewAppError("GetChannelMembersByIds", "app.channel.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return members, nil
}
func (a *App) GetChannelMembersForUser(c request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) {
channelMembers, err := a.Srv().Store().Channel().GetMembersForUser(teamID, userID)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUser", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetChannelMembersForUserWithPagination(c request.CTX, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
members := make([]*model.ChannelMember, 0, len(m))
for _, member := range m {
member := member
members = append(members, &member.ChannelMember)
}
return members, nil
}
func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(c request.CTX, userID string, page, perPage int) (model.ChannelMembersWithTeamData, *model.AppError) {
m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return m, nil
}
func (a *App) GetChannelMemberCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetMemberCount(channelID, true)
if err != nil {
return 0, model.NewAppError("GetChannelMemberCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelFileCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetFileCount(channelID)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetFileCount", "app.channel.get_file_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelGuestCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetGuestCount(channelID, true)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelPinnedPostCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetPinnedPostCount(channelID, true)
if err != nil {
return 0, model.NewAppError("GetChannelPinnedPostCount", "app.channel.get_pinnedpost_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelCounts(c request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) {
counts, err := a.Srv().Store().Channel().GetChannelCounts(teamID, userID)
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelCounts", "app.channel.get_channel_counts.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return counts, nil
}
func (a *App) GetChannelUnread(c request.CTX, channelID, userID string) (*model.ChannelUnread, *model.AppError) {
channelUnread, err := a.Srv().Store().Channel().GetChannelUnread(channelID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if channelUnread.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
channelUnread.MsgCount = 0
channelUnread.MsgCountRoot = 0
}
return channelUnread, nil
}
func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
userChan := make(chan store.StoreResult, 1)
memberChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
go func() {
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, userID)
memberChan <- store.StoreResult{Data: member, NErr: err}
close(memberChan)
}()
uresult := <-userChan
if uresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(uresult.NErr, &nfErr):
return model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
default:
return model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
}
}
mresult := <-memberChan
if mresult.NErr == nil && mresult.Data != nil {
// user is already in the channel
return nil
}
user := uresult.Data.(*model.User)
if channel.Type != model.ChannelTypeOpen {
return model.NewAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "", http.StatusBadRequest)
}
cm, err := a.AddUserToChannel(c, user, channel, false)
if err != nil {
return err
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
return true
}, plugin.UserHasJoinedChannelID)
})
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return err
}
return nil
}
func (a *App) postJoinChannelMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
message := fmt.Sprintf(i18n.T("api.channel.join_channel.post_and_forget"), user.Username)
postType := model.PostTypeJoinChannel
if user.IsGuest() {
message = fmt.Sprintf(i18n.T("api.channel.guest_join_channel.post_and_forget"), user.Username)
postType = model.PostTypeGuestJoinChannel
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: postType,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postJoinChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postJoinTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.join_team.post_and_forget"), user.Username),
Type: model.PostTypeJoinTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postJoinTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
sc := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().Get(channelID, true)
sc <- store.StoreResult{Data: channel, NErr: err}
close(sc)
}()
uc := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uc <- store.StoreResult{Data: user, NErr: err}
close(uc)
}()
mcc := make(chan store.StoreResult, 1)
go func() {
count, err := a.Srv().Store().Channel().GetMemberCount(channelID, false)
mcc <- store.StoreResult{Data: count, NErr: err}
close(mcc)
}()
cresult := <-sc
if cresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(cresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(cresult.NErr)
}
}
uresult := <-uc
if uresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(uresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
}
}
ccresult := <-mcc
if ccresult.NErr != nil {
return model.NewAppError("LeaveChannel", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(ccresult.NErr)
}
channel := cresult.Data.(*model.Channel)
user := uresult.Data.(*model.User)
membersCount := ccresult.Data.(int64)
if channel.IsGroupOrDirect() {
err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest)
return err
}
if channel.Type == model.ChannelTypePrivate && membersCount == 1 {
err := model.NewAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
return err
}
if err := a.removeUserFromChannel(c, userID, userID, channel); err != nil {
return err
}
if channel.Name == model.DefaultChannelName && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
return nil
}
a.Srv().Go(func() {
a.postLeaveChannelMessage(c, user, channel)
})
return nil
}
func (a *App) postLeaveChannelMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
// Message here embeds `@username`, not just `username`, to ensure that mentions
// treat this as a username mention even though the user has now left the channel.
// The client renders its own system message, ignoring this value altogether.
Message: fmt.Sprintf(i18n.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)),
Type: model.PostTypeLeaveChannel,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postLeaveChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
message := fmt.Sprintf(i18n.T("api.channel.add_member.added"), addedUser.Username, user.Username)
postType := model.PostTypeAddToChannel
if addedUser.IsGuest() {
message = fmt.Sprintf(i18n.T("api.channel.add_guest.added"), addedUser.Username, user.Username)
postType = model.PostTypeAddGuestToChannel
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: postType,
UserId: user.Id,
RootId: postRootId,
Props: model.StringInterface{
"userId": user.Id,
"username": user.Username,
model.PostPropsAddedUserId: addedUser.Id,
"addedUsername": addedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postAddToChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postAddToTeamMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.add_user_to_team.added"), addedUser.Username, user.Username),
Type: model.PostTypeAddToTeam,
UserId: user.Id,
RootId: postRootId,
Props: model.StringInterface{
"userId": user.Id,
"username": user.Username,
model.PostPropsAddedUserId: addedUser.Id,
"addedUsername": addedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postAddToTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postRemoveFromChannelMessage(c request.CTX, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError {
messageUserId := removerUserId
if messageUserId == "" {
systemBot, err := a.GetSystemBot()
if err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageUserId = systemBot.UserId
}
post := &model.Post{
ChannelId: channel.Id,
// Message here embeds `@username`, not just `username`, to ensure that mentions
// treat this as a username mention even though the user has now left the channel.
// The client renders its own system message, ignoring this value altogether.
Message: fmt.Sprintf(i18n.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)),
Type: model.PostTypeRemoveFromChannel,
UserId: messageUserId,
Props: model.StringInterface{
"removedUserId": removedUser.Id,
"removedUsername": removedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
user, nErr := a.Srv().Store().User().Get(context.Background(), userIDToRemove)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("removeUserFromChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("removeUserFromChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
isGuest := user.IsGuest()
if channel.Name == model.DefaultChannelName {
if !isGuest {
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
}
}
if channel.IsGroupConstrained() && userIDToRemove != removerUserId && !user.IsBot {
nonMembers, err := a.FilterNonGroupChannelMembers([]string{userIDToRemove}, channel)
if err != nil {
return model.NewAppError("removeUserFromChannel", "api.channel.remove_user_from_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(nonMembers) == 0 {
return model.NewAppError("removeUserFromChannel", "api.channel.remove_members.denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
}
}
cm, err := a.GetChannelMember(c, channel.Id, userIDToRemove)
if err != nil {
return err
}
if err := a.Srv().Store().Channel().RemoveMember(channel.Id, userIDToRemove); err != nil {
return model.NewAppError("removeUserFromChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if isGuest {
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
if err != nil {
return err
}
if len(currentMembers) == 0 {
teamMember, err := a.GetTeamMember(channel.TeamId, userIDToRemove)
if err != nil {
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.ch.srv.teamService.RemoveTeamMember(teamMember); err != nil {
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = a.postProcessTeamMemberLeave(c, teamMember, removerUserId); err != nil {
return err
}
}
}
a.InvalidateCacheForUser(userIDToRemove)
a.invalidateCacheForChannelMembers(channel.Id)
var actorUser *model.User
if removerUserId != "" {
actorUser, _ = a.GetUser(removerUserId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
return true
}, plugin.UserHasLeftChannelID)
})
message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil, "")
message.Add("user_id", userIDToRemove)
message.Add("remover_id", removerUserId)
a.Publish(message)
// because the removed user no longer belongs to the channel we need to send a separate websocket event
userMsg := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", "", userIDToRemove, nil, "")
userMsg.Add("channel_id", channel.Id)
userMsg.Add("remover_id", removerUserId)
a.Publish(userMsg)
return nil
}
func (a *App) RemoveUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
var err *model.AppError
if err = a.removeUserFromChannel(c, userIDToRemove, removerUserId, channel); err != nil {
return err
}
var user *model.User
if user, err = a.GetUser(userIDToRemove); err != nil {
return err
}
if userIDToRemove == removerUserId {
if err := a.postLeaveChannelMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.postRemoveFromChannelMessage(c, removerUserId, user, channel); err != nil {
c.Logger().Error("Failed to post user removal message", mlog.Err(err))
}
}
return nil
}
func (a *App) GetNumberOfChannelsOnTeam(c request.CTX, teamID string) (int, *model.AppError) {
// Get total number of channels on current team
list, err := a.Srv().Store().Channel().GetTeamChannels(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return len(list), nil
}
func (a *App) SetActiveChannel(c request.CTX, userID string, channelID string) *model.AppError {
status, err := a.Srv().Platform().GetStatus(userID)
oldStatus := model.StatusOffline
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: channelID}
} else {
oldStatus = status.Status
status.ActiveChannel = channelID
if !status.Manual && channelID != "" {
status.Status = model.StatusOnline
}
status.LastActivityAt = model.GetMillis()
}
a.Srv().Platform().AddStatusCache(status)
if status.Status != oldStatus {
a.Srv().Platform().BroadcastStatus(status)
}
return nil
}
func (a *App) IsCRTEnabledForUser(c request.CTX, userID string) bool {
appCRT := *a.Config().ServiceSettings.CollapsedThreads
if appCRT == model.CollapsedThreadsDisabled {
return false
}
if appCRT == model.CollapsedThreadsAlwaysOn {
return true
}
threadsEnabled := appCRT == model.CollapsedThreadsDefaultOn
// check if a participant has overridden collapsed threads settings
if preference, err := a.Srv().Store().Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); err == nil {
threadsEnabled = preference.Value == "on"
}
return threadsEnabled
}
// ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list.
func (a *App) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
var allowedChannelIds []string
for _, channelId := range channelIds {
channel, err := a.GetChannel(c, channelId)
if err != nil {
mlog.Info("Invite users to team - couldn't get channel " + channelId)
continue
}
if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePrivateChannelMembers) {
allowedChannelIds = append(allowedChannelIds, channelId)
} else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePublicChannelMembers) {
allowedChannelIds = append(allowedChannelIds, channelId)
} else {
mlog.Info("Invite users to team - no permission to add members to that channel. UserId: " + userId + " ChannelId: " + channelId)
}
}
return allowedChannelIds
}
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
func (a *App) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID) {
return a.markChannelAsUnreadFromPostCRTUnsupported(c, postID, userID)
}
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(c, user, post)
if err != nil {
return nil, err
}
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, false)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) {
post, appErr := a.GetSinglePost(postID, false)
if appErr != nil {
return nil, appErr
}
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
threadId := post.RootId
if post.RootId == "" {
threadId = post.Id
}
unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(c, user, post)
if appErr != nil {
return nil, appErr
}
// if root post,
// In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked.
// In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
if post.RootId == "" {
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, true)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
// if reply post, autofollow thread and
// In CRT Supported Client: Mark the specific thread as unread but not the channel where the thread exists.
// If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge.
// In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post.
// Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
rootPost, appErr := a.GetSinglePost(post.RootId, false)
if appErr != nil {
return nil, appErr
}
channel, nErr := a.Srv().Store().Channel().Get(post.ChannelId, true)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if *a.Config().ServiceSettings.ThreadAutoFollow {
threadMembership, mErr := a.Srv().Store().Thread().GetMembershipForUser(user.Id, threadId)
var errNotFound *store.ErrNotFound
if mErr != nil && !errors.As(mErr, &errNotFound) {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
// Follow thread if we're not already following it
if threadMembership == nil {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
threadMembership, mErr = a.Srv().Store().Thread().MaintainMembership(user.Id, threadId, opts)
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
}
// If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again.
threadMembership.Following = true
threadMembership.LastViewed = post.CreateAt - 1
threadMembership.UnreadMentions, appErr = a.countThreadMentions(c, user, rootPost, channel.TeamId, post.CreateAt-1)
if appErr != nil {
return nil, appErr
}
threadMembership, mErr = a.Srv().Store().Thread().UpdateMembership(threadMembership)
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
if a.IsCRTEnabledForUser(c, userID) {
payload, jsonErr := json.Marshal(thread)
if jsonErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, channel.TeamId, "", userID, nil, "")
message.Add("thread", string(payload))
a.Publish(message)
}
}
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, false)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
func (a *App) sendWebSocketPostUnreadEvent(c request.CTX, channelUnread *model.ChannelUnreadAt, postID string, withMsgCountRoot bool) {
message := model.NewWebSocketEvent(model.WebsocketEventPostUnread, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil, "")
message.Add("msg_count", channelUnread.MsgCount)
if withMsgCountRoot {
message.Add("msg_count_root", channelUnread.MsgCountRoot)
}
message.Add("mention_count", channelUnread.MentionCount)
message.Add("mention_count_root", channelUnread.MentionCountRoot)
message.Add("urgent_mention_count", channelUnread.UrgentMentionCount)
message.Add("last_viewed_at", channelUnread.LastViewedAt)
message.Add("post_id", postID)
a.Publish(message)
}
func (a *App) AutocompleteChannels(c request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
channelList, err := a.Srv().Store().Channel().Autocomplete(userID, term, includeDeleted, user.IsGuest())
if err != nil {
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) AutocompleteChannelsForTeam(c request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
channelList, err := a.Srv().Store().Channel().AutocompleteInTeam(teamID, userID, term, includeDeleted, user.IsGuest())
if err != nil {
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) AutocompleteChannelsForSearch(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("AutocompleteChannelsForSearch", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
// SearchAllChannels returns a list of channels, the total count of the results of the search (if the paginate search option is true), and an error.
func (a *App) SearchAllChannels(c request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
Deleted: opts.Deleted,
TeamIds: opts.TeamIds,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
PolicyID: opts.PolicyID,
IncludePolicyID: opts.IncludePolicyID,
IncludeSearchById: opts.IncludeSearchById,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
Public: opts.Public,
Private: opts.Private,
Page: opts.Page,
PerPage: opts.PerPage,
}
term = strings.TrimSpace(term)
channelList, totalCount, err := a.Srv().Store().Channel().SearchAllChannels(term, storeOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, totalCount, nil
}
func (a *App) SearchChannels(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchInTeam(teamID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("SearchChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchArchivedChannels(c request.CTX, teamID string, term string, userID string) (model.ChannelList, *model.AppError) {
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchArchivedInTeam(teamID, term, userID)
if err != nil {
return nil, model.NewAppError("SearchArchivedChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchChannelsForUser(c request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("SearchChannelsForUser", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchGroupChannels(c request.CTX, userID, term string) (model.ChannelList, *model.AppError) {
if term == "" {
return model.ChannelList{}, nil
}
channelList, err := a.Srv().Store().Channel().SearchGroupChannels(userID, term)
if err != nil {
return nil, model.NewAppError("SearchGroupChannels", "app.channel.search_group_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchChannelsUserNotIn(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchMore(userID, teamID, term)
if err != nil {
return nil, model.NewAppError("SearchChannelsUserNotIn", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
channelsToClearPushNotifications := []string{}
if a.canSendPushNotifications() {
for _, channelID := range channelIDs {
channel, errCh := a.Srv().Store().Channel().Get(channelID, true)
if errCh != nil {
c.Logger().Warn("Failed to get channel", mlog.Err(errCh))
continue
}
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
if err != nil {
c.Logger().Warn("Failed to get membership", mlog.Err(err))
continue
}
notify := member.NotifyProps[model.PushNotifyProp]
if notify == model.ChannelNotifyDefault {
user, err := a.GetUser(userID)
if err != nil {
c.Logger().Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err))
continue
}
notify = user.NotifyProps[model.PushNotifyProp]
}
if notify == model.UserNotifyAll {
if count, err := a.Srv().Store().User().GetAnyUnreadPostCountForChannel(userID, channelID); err == nil {
if count > 0 {
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
}
}
} else if notify == model.UserNotifyMention || channel.Type == model.ChannelTypeDirect {
if count, err := a.Srv().Store().User().GetUnreadCountForChannel(userID, channelID); err == nil {
if count > 0 {
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
}
}
}
}
}
var err error
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID))
if updateThreads {
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelIDs)
if err != nil {
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
times, err := a.Srv().Store().Channel().UpdateLastViewedAt(channelIDs, userID)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
for _, channelID := range channelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil, "")
message.Add("channel_id", channelID)
a.Publish(message)
}
}
for _, channelID := range channelsToClearPushNotifications {
a.clearPushNotification(currentSessionId, userID, channelID, "")
}
if updateThreads && a.IsCRTEnabledForUser(c, userID) {
timestamp := model.GetMillis()
for _, channelID := range channelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
message.Add("timestamp", timestamp)
a.Publish(message)
}
}
return times, nil
}
func (a *App) ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
if err := a.SetActiveChannel(c, userID, view.ChannelId); err != nil {
return nil, err
}
channelIDs := []string{}
if view.ChannelId != "" {
channelIDs = append(channelIDs, view.ChannelId)
}
if view.PrevChannelId != "" {
channelIDs = append(channelIDs, view.PrevChannelId)
}
if len(channelIDs) == 0 {
return map[string]int64{}, nil
}
return a.MarkChannelsAsViewed(c, channelIDs, userID, currentSessionId, collapsedThreadsSupported)
}
func (a *App) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError {
if err := a.Srv().Store().Post().PermanentDeleteByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.post.permanent_delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().PermanentDeleteMembersByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if nErr := a.Srv().Store().Channel().PermanentDelete(channel.Id); nErr != nil {
return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
message.Add("delete_at", deleteAt)
a.Publish(message)
return nil
}
func (a *App) RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError {
err := a.Srv().Store().Channel().RemoveAllDeactivatedMembers(channel.Id)
if err != nil {
return model.NewAppError("RemoveAllDeactivatedMembersFromChannel", "app.channel.remove_all_deactivated_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is relatively small.
func (a *App) MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
// Check that all channel members are in the destination team.
channelMembers, err := a.GetChannelMembersPage(c, channel.Id, 0, 10000000)
if err != nil {
return err
}
channelMemberIds := []string{}
for _, channelMember := range channelMembers {
channelMemberIds = append(channelMemberIds, channelMember.UserId)
}
if len(channelMemberIds) > 0 {
teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
if err2 != nil {
return err2
}
if len(teamMembers) != len(channelMembers) {
teamMembersMap := make(map[string]*model.TeamMember, len(teamMembers))
for _, teamMember := range teamMembers {
teamMembersMap[teamMember.UserId] = teamMember
}
for _, channelMember := range channelMembers {
if _, ok := teamMembersMap[channelMember.UserId]; !ok {
c.Logger().Warn("Not member of the target team", mlog.String("userId", channelMember.UserId))
}
}
return model.NewAppError("MoveChannel", "app.channel.move_channel.members_do_not_match.error", nil, "", http.StatusInternalServerError)
}
}
// keep instance of the previous team
previousTeam, nErr := a.Srv().Store().Team().Get(channel.TeamId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("MoveChannel", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("MoveChannel", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if nErr := a.Srv().Store().Channel().UpdateSidebarChannelCategoryOnMove(channel, team.Id); nErr != nil {
return model.NewAppError("MoveChannel", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
channel.TeamId = team.Id
if _, err := a.Srv().Store().Channel().Update(channel); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("MoveChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
c.Logger().Warn("Failed to get incoming webhooks", mlog.Err(err))
} else {
for _, webhook := range incomingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store().Webhook().UpdateIncoming(webhook); err != nil {
c.Logger().Warn("Failed to move incoming webhook to new team", mlog.String("webhook id", webhook.Id))
}
}
}
}
if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
c.Logger().Warn("Failed to get outgoing webhooks", mlog.Err(err))
} else {
for _, webhook := range outgoingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store().Webhook().UpdateOutgoing(webhook); err != nil {
c.Logger().Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook id", webhook.Id))
}
}
}
}
if err := a.RemoveUsersFromChannelNotMemberOfTeam(c, user, channel, team); err != nil {
c.Logger().Warn("error while removing non-team member users", mlog.Err(err))
}
if user != nil {
if err := a.postChannelMoveMessage(c, user, channel, previousTeam); err != nil {
c.Logger().Warn("error while posting move channel message", mlog.Err(err))
}
}
return nil
}
func (a *App) postChannelMoveMessage(c request.CTX, user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.move_channel.success"), previousTeam.Name),
Type: model.PostTypeMoveChannel,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveUsersFromChannelNotMemberOfTeam(c request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
channelMembers, err := a.GetChannelMembersPage(c, channel.Id, 0, 10000000)
if err != nil {
return err
}
channelMemberIds := []string{}
channelMemberMap := make(map[string]struct{})
for _, channelMember := range channelMembers {
channelMemberMap[channelMember.UserId] = struct{}{}
channelMemberIds = append(channelMemberIds, channelMember.UserId)
}
if len(channelMemberIds) > 0 {
teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
if err != nil {
return err
}
if len(teamMembers) != len(channelMembers) {
for _, teamMember := range teamMembers {
delete(channelMemberMap, teamMember.UserId)
}
var removerId string
if remover != nil {
removerId = remover.Id
}
for userID := range channelMemberMap {
if err := a.removeUserFromChannel(c, userID, removerId, channel); err != nil {
return err
}
}
}
}
return nil
}
func (a *App) GetPinnedPosts(c request.CTX, channelID string) (*model.PostList, *model.AppError) {
posts, err := a.Srv().Store().Channel().GetPinnedPosts(channelID)
if err != nil {
return nil, model.NewAppError("GetPinnedPosts", "app.channel.pinned_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return posts, nil
}
func (a *App) ToggleMuteChannel(c request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError) {
member, nErr := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
if nErr != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("ToggleMuteChannel", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
member.SetChannelMuted(!member.IsChannelMuted())
member, err := a.updateChannelMember(c, member)
if err != nil {
return nil, err
}
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
return member, nil
}
func (a *App) setChannelsMuted(c request.CTX, channelIDs []string, userID string, muted bool) ([]*model.ChannelMember, *model.AppError) {
members, err := a.Srv().Store().Channel().GetMembersByChannelIds(channelIDs, userID)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
var membersToUpdate []*model.ChannelMember
for _, member := range members {
if muted == member.IsChannelMuted() {
continue
}
updatedMember := member
updatedMember.SetChannelMuted(muted)
membersToUpdate = append(membersToUpdate, &updatedMember)
}
if len(membersToUpdate) == 0 {
return nil, nil
}
updated, err := a.Srv().Store().Channel().UpdateMultipleMembers(membersToUpdate)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("setChannelsMuted", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, member := range updated {
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("setChannelsMuted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
return updated, nil
}
func (a *App) FillInChannelProps(c request.CTX, channel *model.Channel) *model.AppError {
return a.FillInChannelsProps(c, model.ChannelList{channel})
}
func (a *App) FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError {
// Group the channels by team and call GetChannelsByNames just once per team.
channelsByTeam := make(map[string]model.ChannelList)
for _, channel := range channelList {
channelsByTeam[channel.TeamId] = append(channelsByTeam[channel.TeamId], channel)
}
for teamID, channelList := range channelsByTeam {
allChannelMentions := make(map[string]bool)
channelMentions := make(map[*model.Channel][]string, len(channelList))
// Collect mentions across the channels so as to query just once for this team.
for _, channel := range channelList {
channelMentions[channel] = model.ChannelMentions(channel.Header)
for _, channelMention := range channelMentions[channel] {
allChannelMentions[channelMention] = true
}
}
allChannelMentionNames := make([]string, 0, len(allChannelMentions))
for channelName := range allChannelMentions {
allChannelMentionNames = append(allChannelMentionNames, channelName)
}
if len(allChannelMentionNames) > 0 {
mentionedChannels, err := a.GetChannelsByNames(c, allChannelMentionNames, teamID)
if err != nil {
return err
}
mentionedChannelsByName := make(map[string]*model.Channel)
for _, channel := range mentionedChannels {
mentionedChannelsByName[channel.Name] = channel
}
for _, channel := range channelList {
channelMentionsProp := make(map[string]any, len(channelMentions[channel]))
for _, channelMention := range channelMentions[channel] {
if mentioned, ok := mentionedChannelsByName[channelMention]; ok {
if mentioned.Type == model.ChannelTypeOpen {
channelMentionsProp[mentioned.Name] = map[string]any{
"display_name": mentioned.DisplayName,
}
}
}
}
if len(channelMentionsProp) > 0 {
channel.AddProp("channel_mentions", channelMentionsProp)
} else if channel.Props != nil {
delete(channel.Props, "channel_mentions")
}
}
}
}
return nil
}
func (a *App) forEachChannelMember(c request.CTX, channelID string, f func(model.ChannelMember) error) error {
perPage := 100
page := 0
for {
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
if err != nil {
return err
}
for _, channelMember := range channelMembers {
if err = f(channelMember); err != nil {
return err
}
}
length := len(channelMembers)
if length < perPage {
break
}
page++
}
return nil
}
func (a *App) ClearChannelMembersCache(c request.CTX, channelID string) error {
clearSessionCache := func(channelMember model.ChannelMember) error {
a.ClearSessionCacheForUser(channelMember.UserId)
message := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(channelMember)
if jsonErr != nil {
return jsonErr
}
message.Add("channelMember", string(memberJSON))
a.Publish(message)
return nil
}
if err := a.forEachChannelMember(c, channelID, clearSessionCache); err != nil {
return fmt.Errorf("error clearing cache for channel members: channel_id: %s, error: %w", channelID, err)
}
return nil
}
func (a *App) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
channelMemberCounts, err := a.Srv().Store().Channel().GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err != nil {
return nil, model.NewAppError("GetMemberCountsByGroup", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMemberCounts, nil
}
func (a *App) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
return a.Srv().getDirectChannel(c, userID, otherUserID)
}
func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true)
if nErr != nil {
var nfErr *store.ErrNotFound
if errors.As(nErr, &nfErr) {
return nil, nil
}
return nil, model.NewAppError("GetOrCreateDirectChannel", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return channel, nil
}
func (a *App) GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopChannelsForUserSince(c request.CTX, userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
// PostCountsByDuration returns the post counts for the given channels, grouped by day, starting at the given time.
// Unless one is specifically itending to omit results from part of the calendar day, it will typically makes the most sense to
// use a sinceUnixMillis parameter value as returned by model.GetStartOfDayMillis.
//
// WARNING: PostCountsByDuration PERFORMS NO AUTHORIZATION CHECKS ON THE GIVEN CHANNELS.
func (a *App) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("PostCountsByDuration", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
postCountByDay, err := a.Srv().Store().Channel().PostCountsByDuration(channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if err != nil {
return nil, model.NewAppError("PostCountsByDuration", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return postCountByDay, nil
}
func (a *App) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) createInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
categories, nErr := a.Srv().Store().Channel().CreateInitialSidebarCategories(userID, opts)
if nErr != nil {
return nil, model.NewAppError("createInitialSidebarCategories", "app.channel.create_initial_sidebar_categories.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return categories, nil
}
func (a *App) GetSidebarCategoriesForTeamForUser(c request.CTX, userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
var appErr *model.AppError
categories, err := a.Srv().Store().Channel().GetSidebarCategoriesForTeamForUser(userID, teamID)
if err == nil && len(categories.Categories) == 0 {
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
categories, appErr = a.createInitialSidebarCategories(userID, &store.SidebarCategorySearchOpts{
TeamID: teamID,
ExcludeTeam: false,
})
if appErr != nil {
return nil, appErr
}
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
var appErr *model.AppError
categories, err := a.Srv().Store().Channel().GetSidebarCategories(userID, opts)
if err == nil && len(categories.Categories) == 0 {
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
categories, appErr = a.createInitialSidebarCategories(userID, opts)
if appErr != nil {
return nil, appErr
}
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategoryOrder(c request.CTX, userID, teamID string) ([]string, *model.AppError) {
categories, err := a.Srv().Store().Channel().GetSidebarCategoryOrder(userID, teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
category, err := a.Srv().Store().Channel().GetSidebarCategory(categoryId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return category, nil
}
func (a *App) CreateSidebarCategory(c request.CTX, userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
category, err := a.Srv().Store().Channel().CreateSidebarCategory(userID, teamID, newCategory)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryCreated, teamID, "", userID, nil, "")
message.Add("category_id", category.Id)
a.Publish(message)
return category, nil
}
func (a *App) UpdateSidebarCategoryOrder(c request.CTX, userID, teamID string, categoryOrder []string) *model.AppError {
err := a.Srv().Store().Channel().UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &nfErr):
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryOrderUpdated, teamID, "", userID, nil, "")
message.Add("order", categoryOrder)
a.Publish(message)
return nil
}
func (a *App) UpdateSidebarCategories(c request.CTX, userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
updatedCategories, originalCategories, err := a.Srv().Store().Channel().UpdateSidebarCategories(userID, teamID, categories)
if err != nil {
return nil, model.NewAppError("UpdateSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, teamID, "", userID, nil, "")
updatedCategoriesJSON, jsonErr := json.Marshal(updatedCategories)
if jsonErr != nil {
return nil, model.NewAppError("UpdateSidebarCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("updatedCategories", string(updatedCategoriesJSON))
a.Publish(message)
a.muteChannelsForUpdatedCategories(c, userID, updatedCategories, originalCategories)
return updatedCategories, nil
}
func (a *App) muteChannelsForUpdatedCategories(c request.CTX, userID string, updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) {
var channelsToMute []string
var channelsToUnmute []string
// Mute or unmute all channels in categories that were muted or unmuted
for i, updatedCategory := range updatedCategories {
if i > len(originalCategories)-1 {
// The two slices should be the same length, but double check that to be safe
continue
}
originalCategory := originalCategories[i]
if updatedCategory.Muted && !originalCategory.Muted {
channelsToMute = append(channelsToMute, updatedCategory.Channels...)
} else if !updatedCategory.Muted && originalCategory.Muted {
channelsToUnmute = append(channelsToUnmute, updatedCategory.Channels...)
}
}
// Mute any channels moved from an unmuted category into a muted one and vice versa
channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories)
if len(channelsDiff) != 0 {
makeCategoryMap := func(categories []*model.SidebarCategoryWithChannels) map[string]*model.SidebarCategoryWithChannels {
result := make(map[string]*model.SidebarCategoryWithChannels)
for _, category := range categories {
result[category.Id] = category
}
return result
}
updatedCategoriesById := makeCategoryMap(updatedCategories)
originalCategoriesById := makeCategoryMap(originalCategories)
for channelID, diff := range channelsDiff {
fromCategory := originalCategoriesById[diff.fromCategoryId]
toCategory := updatedCategoriesById[diff.toCategoryId]
if toCategory.Muted && !fromCategory.Muted {
channelsToMute = append(channelsToMute, channelID)
} else if !toCategory.Muted && fromCategory.Muted {
channelsToUnmute = append(channelsToUnmute, channelID)
}
}
}
if len(channelsToMute) > 0 {
_, err := a.setChannelsMuted(c, channelsToMute, userID, true)
if err != nil {
c.Logger().Error(
"Failed to mute channels to match category",
mlog.String("user_id", userID),
mlog.Err(err),
)
}
}
if len(channelsToUnmute) > 0 {
_, err := a.setChannelsMuted(c, channelsToUnmute, userID, false)
if err != nil {
c.Logger().Error(
"Failed to unmute channels to match category",
mlog.String("user_id", userID),
mlog.Err(err),
)
}
}
}
type categoryChannelDiff struct {
fromCategoryId string
toCategoryId string
}
func diffChannelsBetweenCategories(updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) map[string]*categoryChannelDiff {
// mapChannelIdsToCategories returns a map of channel IDs to the IDs of the categories that they're a member of.
mapChannelIdsToCategories := func(categories []*model.SidebarCategoryWithChannels) map[string]string {
result := make(map[string]string)
for _, category := range categories {
for _, channelID := range category.Channels {
result[channelID] = category.Id
}
}
return result
}
updatedChannelIdsMap := mapChannelIdsToCategories(updatedCategories)
originalChannelIdsMap := mapChannelIdsToCategories(originalCategories)
// Check for any channels that have changed categories. Note that we don't worry about any channels that have moved
// outside of these categories since that heavily complicates things and doesn't currently happen in our apps.
channelsDiff := make(map[string]*categoryChannelDiff)
for channelID, originalCategoryId := range originalChannelIdsMap {
updatedCategoryId := updatedChannelIdsMap[channelID]
if originalCategoryId != updatedCategoryId && updatedCategoryId != "" {
channelsDiff[channelID] = &categoryChannelDiff{originalCategoryId, updatedCategoryId}
}
}
return channelsDiff
}
func (a *App) DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId string) *model.AppError {
err := a.Srv().Store().Channel().DeleteSidebarCategory(categoryId)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryDeleted, teamID, "", userID, nil, "")
message.Add("category_id", categoryId)
a.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"runtime"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const ServerKey product.ServiceKey = "server"
// licenseSvc is added to act as a starting point for future integrated products.
// It has the same signature and functionality with the license related APIs of the plugin-api.
type licenseSvc interface {
GetLicense() *model.License
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
}
// Channels contains all channels related state.
type Channels struct {
srv *Server
cfgSvc product.ConfigService
filestore filestore.FileBackend
licenseSvc licenseSvc
routerSvc *routerService
postActionCookieSecret []byte
pluginCommandsLock sync.RWMutex
pluginCommands []*PluginCommand
pluginsLock sync.RWMutex
pluginsEnvironment *plugin.Environment
pluginConfigListenerID string
productCommandsLock sync.RWMutex
productCommands []*ProductCommand
imageProxy *imageproxy.ImageProxy
// cached counts that are used during notice condition validation
cachedPostCount int64
cachedUserCount int64
cachedDBMSVersion string
// previously fetched notices
cachedNotices model.ProductNotices
AccountMigration einterfaces.AccountMigrationInterface
Compliance einterfaces.ComplianceInterface
DataRetention einterfaces.DataRetentionInterface
MessageExport einterfaces.MessageExportInterface
Saml einterfaces.SamlInterface
Notification einterfaces.NotificationInterface
Ldap einterfaces.LdapInterface
// These are used to prevent concurrent upload requests
// for a given upload session which could cause inconsistencies
// and data corruption.
uploadLockMapMut sync.Mutex
uploadLockMap map[string]bool
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
dndTaskMut sync.Mutex
dndTask *model.ScheduledTask
postReminderMut sync.Mutex
postReminderTask *model.ScheduledTask
// collectionTypes maps from collection types to the registering plugin id
collectionTypes map[string]string
// topicTypes maps from topic types to collection types
topicTypes map[string]string
collectionAndTopicTypesMut sync.Mutex
}
func init() {
product.RegisterProduct("channels", product.Manifest{
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
return NewChannels(services)
},
Dependencies: map[product.ServiceKey]struct{}{
ServerKey: {},
product.ConfigKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
},
})
}
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
s, ok := services[ServerKey].(*Server)
if !ok {
return nil, errors.New("server not passed")
}
ch := &Channels{
srv: s,
imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()),
uploadLockMap: map[string]bool{},
collectionTypes: map[string]string{},
topicTypes: map[string]string{},
}
// To get another service:
// 1. Prepare the service interface
// 2. Add the field to *Channels
// 3. Add the service key to the slice.
// 4. Add a new case in the switch statement.
requiredServices := []product.ServiceKey{
product.ConfigKey,
product.LicenseKey,
product.FilestoreKey,
}
for _, svcKey := range requiredServices {
svc, ok := services[svcKey]
if !ok {
return nil, fmt.Errorf("Service %s not passed", svcKey)
}
switch svcKey {
// Keep adding more services here
case product.ConfigKey:
cfgSvc, ok := svc.(product.ConfigService)
if !ok {
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
}
ch.cfgSvc = cfgSvc
case product.FilestoreKey:
filestore, ok := svc.(filestore.FileBackend)
if !ok {
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
}
ch.filestore = filestore
case product.LicenseKey:
svc, ok := svc.(licenseSvc)
if !ok {
return nil, errors.New("License service did not satisfy licenseSvc interface")
}
ch.licenseSvc = svc
}
}
// We are passing a partially filled Channels struct so that the enterprise
// methods can have access to app methods.
// Otherwise, passing server would mean it has to call s.Channels(),
// which would be nil at this point.
if complianceInterface != nil {
ch.Compliance = complianceInterface(New(ServerConnector(ch)))
}
if messageExportInterface != nil {
ch.MessageExport = messageExportInterface(New(ServerConnector(ch)))
}
if dataRetentionInterface != nil {
ch.DataRetention = dataRetentionInterface(New(ServerConnector(ch)))
}
if accountMigrationInterface != nil {
ch.AccountMigration = accountMigrationInterface(New(ServerConnector(ch)))
}
if ldapInterface != nil {
ch.Ldap = ldapInterface(New(ServerConnector(ch)))
}
if notificationInterface != nil {
ch.Notification = notificationInterface(New(ServerConnector(ch)))
}
if samlInterfaceNew != nil {
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
if err := ch.Saml.ConfigureSP(); err != nil {
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
}
ch.AddConfigListener(func(_, _ *model.Config) {
if err := ch.Saml.ConfigureSP(); err != nil {
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
}
})
}
var imgErr error
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)
if decoderConcurrency == -1 {
decoderConcurrency = runtime.NumCPU()
}
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
ConcurrencyLevel: decoderConcurrency,
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image decoder")
}
ch.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
ConcurrencyLevel: runtime.NumCPU(),
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image encoder")
}
ch.routerSvc = newRouterService()
services[product.RouterKey] = ch.routerSvc
// Setup routes.
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
services[product.ChannelKey] = &channelsWrapper{
app: &App{ch: ch},
}
services[product.PostKey] = &postServiceWrapper{
app: &App{ch: ch},
}
services[product.PermissionsKey] = &permissionsServiceWrapper{
app: &App{ch: ch},
}
services[product.TeamKey] = &teamServiceWrapper{
app: &App{ch: ch},
}
services[product.BotKey] = &botServiceWrapper{
app: &App{ch: ch},
}
services[product.HooksKey] = &hooksService{
ch: ch,
}
services[product.UserKey] = &App{ch: ch}
services[product.PreferencesKey] = &preferencesServiceWrapper{
app: &App{ch: ch},
}
services[product.CommandKey] = &App{ch: ch}
services[product.ThreadsKey] = &App{ch: ch}
return ch, nil
}
func (ch *Channels) Start() error {
// Start plugins
ctx := request.EmptyContext(ch.srv.Log())
ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
ch.AddConfigListener(func(prevCfg, cfg *model.Config) {
// We compute the difference between configs
// to ensure we don't re-init plugins unnecessarily.
diffs, err := config.Diff(prevCfg, cfg)
if err != nil {
ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err))
return
}
hasDiff := false
// TODO: This could be a method on ConfigDiffs itself
for _, diff := range diffs {
if strings.HasPrefix(diff.Path, "PluginSettings.") {
hasDiff = true
break
}
}
// Do only if some plugin related settings has changed.
if hasDiff {
if *cfg.PluginSettings.Enable {
ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
} else {
ch.ShutDownPlugins()
}
}
})
// TODO: This should be moved to the platform service.
if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil {
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
}
if err := ch.ensurePostActionCookieSecret(); err != nil {
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
}
return nil
}
func (ch *Channels) Stop() error {
ch.ShutDownPlugins()
ch.dndTaskMut.Lock()
if ch.dndTask != nil {
ch.dndTask.Cancel()
}
ch.dndTaskMut.Unlock()
return nil
}
func (ch *Channels) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return ch.cfgSvc.AddConfigListener(listener)
}
func (ch *Channels) RemoveConfigListener(id string) {
ch.cfgSvc.RemoveConfigListener(id)
}
func (ch *Channels) License() *model.License {
return ch.licenseSvc.GetLicense()
}
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
receiveEmailsAccepted)
}
func (a *App) HooksManager() *product.HooksManager {
return a.ch.srv.hooksManager
}
// Ensure hooksService implements `product.HooksService`
var _ product.HooksService = (*hooksService)(nil)
type hooksService struct {
ch *Channels
}
func (s *hooksService) RegisterHooks(productID string, hooks any) error {
return s.ch.srv.hooksManager.AddProduct(productID, hooks)
}
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
if env := ch.GetPluginsEnvironment(); env != nil {
env.RunMultiPluginHook(hookRunnerFunc, hookId)
}
// run hook for the products
ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId)
}
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
var hooks plugin.Hooks
if env := ch.GetPluginsEnvironment(); env != nil {
// we intentionally ignore the error here, because the id can be a product id
// we are going to check if we have the hooks or not
hooks, _ = env.HooksForPlugin(id)
if hooks != nil {
return hooks, nil
}
}
hooks = ch.srv.hooksManager.HooksForProduct(id)
if hooks != nil {
return hooks, nil
}
return nil, fmt.Errorf("could not find hooks for id %s", id)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Ensure cloud service wrapper implements `product.CloudService`
var _ product.CloudService = (*cloudWrapper)(nil)
// cloudWrapper provides an implementation of `product.CloudService` for use by products.
type cloudWrapper struct {
cloud einterfaces.CloudInterface
}
func (c *cloudWrapper) GetCloudLimits() (*model.ProductLimits, error) {
if c.cloud != nil {
return c.cloud.GetCloudLimits("")
}
return &model.ProductLimits{}, nil
}
func (a *App) getSysAdminsEmailRecipients() ([]*model.User, *model.AppError) {
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: 100,
Role: model.SystemAdminRoleId,
Inactive: false,
}
return a.GetUsersFromProfiles(userOptions)
}
func getCurrentPlanName(a *App) (string, *model.AppError) {
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if subscription == nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
}
products, err := a.Cloud().GetCloudProducts("", false)
if err != nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if products == nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, "", http.StatusInternalServerError)
}
planName := getCurrentProduct(subscription.ProductID, products).Name
return planName, nil
}
func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError {
sysAdmins, err := a.getSysAdminsEmailRecipients()
if err != nil {
return err
}
planName, err := getCurrentPlanName(a)
if err != nil {
return model.NewAppError("SendPaymentFailedEmail", "app.cloud.get_current_plan_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, admin := range sysAdmins {
_, err := a.Srv().EmailService.SendPaymentFailedEmail(admin.Email, admin.Locale, failedPayment, planName, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending payment failed email", mlog.Err(err))
}
}
return nil
}
func getCurrentProduct(subscriptionProductID string, products []*model.Product) *model.Product {
for _, product := range products {
if product.ID == subscriptionProductID {
return product
}
}
return nil
}
func (a *App) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
sysAdmins, aErr := a.getSysAdminsEmailRecipients()
if aErr != nil {
return aErr
}
planName, aErr := getCurrentPlanName(a)
if aErr != nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_current_plan_name.app_error", nil, aErr.Error(), http.StatusInternalServerError)
}
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if subscription == nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
}
if subscription.DelinquentSince == nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription_delinquency_date.app_error", nil, "", http.StatusInternalServerError)
}
delinquentSince := time.Unix(*subscription.DelinquentSince, 0)
delinquencyDate := delinquentSince.Format("01/02/2006")
for _, admin := range sysAdmins {
switch emailToSend {
case model.DelinquencyEmail7:
err := a.Srv().EmailService.SendDelinquencyEmail7(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 7", mlog.Err(err))
}
case model.DelinquencyEmail14:
err := a.Srv().EmailService.SendDelinquencyEmail14(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 14", mlog.Err(err))
}
case model.DelinquencyEmail30:
err := a.Srv().EmailService.SendDelinquencyEmail30(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 30", mlog.Err(err))
}
case model.DelinquencyEmail45:
err := a.Srv().EmailService.SendDelinquencyEmail45(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
if err != nil {
a.Log().Error("Error sending delinquency email 45", mlog.Err(err))
}
case model.DelinquencyEmail60:
err := a.Srv().EmailService.SendDelinquencyEmail60(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending delinquency email 60", mlog.Err(err))
}
case model.DelinquencyEmail75:
err := a.Srv().EmailService.SendDelinquencyEmail75(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
if err != nil {
a.Log().Error("Error sending delinquency email 75", mlog.Err(err))
}
case model.DelinquencyEmail90:
err := a.Srv().EmailService.SendDelinquencyEmail90(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending delinquency email 90", mlog.Err(err))
}
}
}
return nil
}
func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 {
err := a.AdjustTeamsFromProductLimits(limits.Teams)
if err != nil {
return err
}
}
return nil
}
func getNextBillingDateString() string {
now := time.Now()
t := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, time.UTC)
return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year())
}
func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
sysAdmins, e := a.getSysAdminsEmailRecipients()
if e != nil {
return e
}
if len(sysAdmins) == 0 {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
billingDate := getNextBillingDateString()
// we want to at least have one email sent out to an admin
countNotOks := 0
embeddedFiles := make(map[string]io.Reader)
if isYearly {
pdf, filename, pdfErr := a.Cloud().GetInvoicePDF("", subscription.LastInvoice.ID)
if pdfErr != nil {
a.Log().Error("Error retrieving the invoice for subscription id", mlog.String("subscription", subscription.ID), mlog.Err(pdfErr))
} else {
embeddedFiles = map[string]io.Reader{
filename: bytes.NewReader(pdf),
}
}
}
for _, admin := range sysAdmins {
name := admin.FirstName
if name == "" {
name = admin.Username
}
err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly, embeddedFiles)
if err != nil {
a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err))
countNotOks++
}
}
// if not even one admin got an email, we consider that this operation errored
if countNotOks == len(sysAdmins) {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
return nil
}
// SendNoCardPaymentFailedEmail
func (a *App) SendNoCardPaymentFailedEmail() *model.AppError {
sysAdmins, err := a.getSysAdminsEmailRecipients()
if err != nil {
return err
}
for _, admin := range sysAdmins {
err := a.Srv().EmailService.SendNoCardPaymentFailedEmail(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending payment failed email", mlog.Err(err))
}
}
return nil
}
// Create/ Update a subscription history event
func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
license := a.Srv().License()
// No need to create a Subscription History Event if the license isn't cloud
if !license.IsCloud() {
return nil, nil
}
// Get user count
userCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, err
}
return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
// Registers a given function to be called when the cluster leader may have changed. Returns a unique ID for the
// listener which can later be used to remove it. If clustering is not enabled in this build, the callback will never
// be called.
func (s *Server) AddClusterLeaderChangedListener(listener func()) string {
return s.platform.AddClusterLeaderChangedListener(listener)
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (s *Server) RemoveClusterLeaderChangedListener(id string) {
s.platform.RemoveClusterLeaderChangedListener(id)
}
func (s *Server) InvokeClusterLeaderChangedListeners() {
s.platform.InvokeClusterLeaderChangedListeners()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
func (s *Server) IsLeader() bool {
return s.platform.IsLeader()
}
func (a *App) IsLeader() bool {
return a.Srv().IsLeader()
}
func (a *App) GetClusterId() string {
return a.Srv().Platform().GetClusterId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) {
var data model.PluginEventData
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
s.Channels().installPluginFromData(data)
}
func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
var data model.PluginEventData
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
s.Channels().removePluginFromData(data)
}
func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
if msg.Props == nil {
mlog.Warn("ClusterMessage.Props for plugin event should not be nil")
return
}
pluginID := msg.Props["PluginID"]
// if the plugin key is empty, the message might be coming from a product.
if pluginID == "" {
pluginID = msg.Props["ProductID"]
}
eventID := msg.Props["EventID"]
if pluginID == "" || eventID == "" {
mlog.Warn("Invalid ClusterMessage.Props values for plugin event",
mlog.String("plugin_id", pluginID), mlog.String("event_id", eventID))
return
}
channels, ok := s.products["channels"].(*Channels)
if !ok {
return
}
hooks, err := channels.HooksForPluginOrProduct(pluginID)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
return
}
hooks.OnPluginClusterEvent(&plugin.Context{}, model.PluginClusterEvent{
Id: eventID,
Data: msg.Data,
})
}
// registerClusterHandlers registers the cluster message handlers that are handled by the server.
//
// The cluster event handlers are spread across this function and NewLocalCacheLayer.
// Be careful to not have duplicated handlers here and there.
func (s *Server) registerClusterHandlers() {
s.platform.RegisterClusterMessageHandler(model.ClusterEventInstallPlugin, s.clusterInstallPluginHandler)
s.platform.RegisterClusterMessageHandler(model.ClusterEventRemovePlugin, s.clusterRemovePluginHandler)
s.platform.RegisterClusterMessageHandler(model.ClusterEventPluginEvent, s.clusterPluginEventHandler)
s.platform.RegisterClusterHandlers()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) RegisterCollectionAndTopic(pluginID, collectionType, topicType string) error {
// we have a race condition due to multiple plugins calling this method
a.ch.collectionAndTopicTypesMut.Lock()
defer a.ch.collectionAndTopicTypesMut.Unlock()
// check if collectionType was already registered by other plugin
existingPluginID, ok := a.ch.collectionTypes[collectionType]
if ok && existingPluginID != pluginID {
return model.NewAppError("registerCollectionAndTopic", "app.collection.add_collection.exists.app_error", nil, "", http.StatusBadRequest)
}
// check if topicType was already registered to other collection
existingCollectionType, ok := a.ch.topicTypes[topicType]
if ok && existingCollectionType != collectionType {
return model.NewAppError("registerCollectionAndTopic", "app.collection.add_topic.exists.app_error", nil, "", http.StatusBadRequest)
}
a.ch.collectionTypes[collectionType] = pluginID
a.ch.topicTypes[topicType] = collectionType
a.ch.srv.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"unicode"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
CmdCustomStatusTrigger = "status"
usernameSpecialChars = ".-_"
maxTriggerLen = 512
)
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
type CommandProvider interface {
GetTrigger() string
GetCommand(a *App, T i18n.TranslateFunc) *model.Command
DoCommand(a *App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse
}
var commandProviders = make(map[string]CommandProvider)
func RegisterCommandProvider(newProvider CommandProvider) {
commandProviders[newProvider.GetTrigger()] = newProvider
}
func GetCommandProvider(name string) CommandProvider {
provider, ok := commandProviders[name]
if ok {
return provider
}
return nil
}
// @openTracingParams teamID, skipSlackParsing
func (a *App) CreateCommandPost(c request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
if skipSlackParsing {
post.Message = response.Text
} else {
post.Message = model.ParseSlackLinksToMarkdown(response.Text)
}
post.CreateAt = model.GetMillis()
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if response.Attachments != nil {
model.ParseSlackAttachment(post, response.Attachments)
}
if response.ResponseType == model.CommandResponseTypeInChannel {
return a.CreatePostMissingChannel(c, post, true, true)
}
if (response.ResponseType == "" || response.ResponseType == model.CommandResponseTypeEphemeral) && (response.Text != "" || response.Attachments != nil) {
a.SendEphemeralPost(c, post.UserId, post)
}
return post, nil
}
// @openTracingParams teamID
// previous ListCommands now ListAutocompleteCommands
func (a *App) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
// Disable custom status slash command if the feature or the setting is off
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
seen[CmdCustomStatusTrigger] = true
}
for _, cmd := range a.CommandsForTeam(teamID) {
if cmd.AutoComplete && !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAutocompleteCommands", "app.command.listautocompletecommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, cmd := range teamCmds {
if cmd.AutoComplete && !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
return commands, nil
}
func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamCmds, nil
}
func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
for _, cmd := range a.CommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAllCommands", "app.command.listallcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, cmd := range teamCmds {
if !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
return commands, nil
}
// @openTracingParams args
func (a *App) ExecuteCommand(c request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
trigger := ""
message := ""
index := strings.IndexFunc(args.Command, unicode.IsSpace)
if index != -1 {
trigger = args.Command[:index]
message = args.Command[index+1:]
} else {
trigger = args.Command
}
trigger = strings.ToLower(trigger)
if !strings.HasPrefix(trigger, "/") {
return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]any{"Trigger": trigger}, "", http.StatusBadRequest)
}
trigger = strings.TrimPrefix(trigger, "/")
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
if appErr != nil {
c.Logger().Warn("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
}
args.TriggerId = triggerId
// Plugins can override built in, custom, and product commands
cmd, response, appErr := a.tryExecutePluginCommand(c, args)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, true)
}
// Products can override built in and custom commands
cmd, response, appErr = a.tryExecuteProductCommand(c, args)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, true)
}
// Custom commands can override built ins
cmd, response, appErr = a.tryExecuteCustomCommand(c, args, trigger, message)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, false)
}
cmd, response = a.tryExecuteBuiltInCommand(c, args, trigger, message)
if cmd != nil && response != nil {
return a.HandleCommandResponse(c, cmd, args, response, true)
}
if len(trigger) > maxTriggerLen {
trigger = trigger[:maxTriggerLen]
trigger += "..."
}
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]any{"Trigger": trigger}, "", http.StatusNotFound)
}
// MentionsToTeamMembers returns all the @ mentions found in message that
// belong to users in the specified team, linking them to their users
func (a *App) MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap {
type mentionMapItem struct {
Name string
Id string
}
possibleMentions := possibleAtMentions(message)
mentionChan := make(chan *mentionMapItem, len(possibleMentions))
var wg sync.WaitGroup
for _, mention := range possibleMentions {
wg.Add(1)
go func(mention string) {
defer wg.Done()
user, nErr := a.Srv().Store().User().GetByUsername(mention)
var nfErr *store.ErrNotFound
if nErr != nil && !errors.As(nErr, &nfErr) {
c.Logger().Warn("Failed to retrieve user @"+mention, mlog.Err(nErr))
return
}
// If it's a http.StatusNotFound error, check for usernames in substrings
// without trailing punctuation
if nErr != nil {
trimmed, ok := trimUsernameSpecialChar(mention)
for ; ok; trimmed, ok = trimUsernameSpecialChar(trimmed) {
userFromTrimmed, nErr := a.Srv().Store().User().GetByUsername(trimmed)
if nErr != nil && !errors.As(nErr, &nfErr) {
return
}
if nErr != nil {
continue
}
_, err := a.GetTeamMember(teamID, userFromTrimmed.Id)
if err != nil {
// The user is not in the team, so we should ignore it
return
}
mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
return
}
return
}
_, err := a.GetTeamMember(teamID, user.Id)
if err != nil {
// The user is not in the team, so we should ignore it
return
}
mentionChan <- &mentionMapItem{mention, user.Id}
}(mention)
}
wg.Wait()
close(mentionChan)
atMentionMap := make(model.UserMentionMap)
for mention := range mentionChan {
atMentionMap[mention.Name] = mention.Id
}
return atMentionMap
}
// MentionsToPublicChannels returns all the mentions to public channels,
// linking them to their channels
func (a *App) MentionsToPublicChannels(c request.CTX, message, teamID string) model.ChannelMentionMap {
type mentionMapItem struct {
Name string
Id string
}
channelMentions := model.ChannelMentions(message)
mentionChan := make(chan *mentionMapItem, len(channelMentions))
var wg sync.WaitGroup
for _, channelName := range channelMentions {
wg.Add(1)
go func(channelName string) {
defer wg.Done()
channel, err := a.GetChannelByName(c, channelName, teamID, false)
if err != nil {
return
}
if !channel.IsOpen() {
return
}
mentionChan <- &mentionMapItem{channelName, channel.Id}
}(channelName)
}
wg.Wait()
close(mentionChan)
channelMentionMap := make(model.ChannelMentionMap)
for mention := range mentionChan {
channelMentionMap[mention.Name] = mention.Id
}
return channelMentionMap
}
// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteBuiltInCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
provider := GetCommandProvider(trigger)
if provider == nil {
return nil, nil
}
cmd := provider.GetCommand(a, args.T)
if cmd == nil {
return nil, nil
}
return cmd, provider.DoCommand(a, c, args, message)
}
// tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteCustomCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
// Handle custom commands
if !*a.Config().ServiceSettings.EnableCommands {
return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
chanChan := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().Get(args.ChannelId, true)
chanChan <- store.StoreResult{Data: channel, NErr: err}
close(chanChan)
}()
teamChan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(args.TeamId)
teamChan <- store.StoreResult{Data: team, NErr: err}
close(teamChan)
}()
userChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), args.UserId)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
teamCmds, err := a.Srv().Store().Command().GetByTeam(args.TeamId)
if err != nil {
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.command.tryexecutecustomcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
tr := <-teamChan
if tr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(tr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
}
}
team := tr.Data.(*model.Team)
ur := <-userChan
if ur.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(ur.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
}
}
user := ur.Data.(*model.User)
cr := <-chanChan
if cr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(cr.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(cr.NErr)
}
}
channel := cr.Data.(*model.Channel)
var cmd *model.Command
for _, teamCmd := range teamCmds {
if trigger == teamCmd.Trigger {
cmd = teamCmd
}
}
if cmd == nil {
return nil, nil, nil
}
c.Logger().Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
p := url.Values{}
p.Set("token", cmd.Token)
p.Set("team_id", cmd.TeamId)
p.Set("team_domain", team.Name)
p.Set("channel_id", args.ChannelId)
p.Set("channel_name", channel.Name)
p.Set("user_id", args.UserId)
p.Set("user_name", user.Username)
p.Set("command", "/"+trigger)
p.Set("text", message)
p.Set("trigger_id", args.TriggerId)
userMentionMap := a.MentionsToTeamMembers(c, message, team.Id)
for key, values := range userMentionMap.ToURLValues() {
p[key] = values
}
channelMentionMap := a.MentionsToPublicChannels(c, message, team.Id)
for key, values := range channelMentionMap.ToURLValues() {
p[key] = values
}
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
if appErr != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError).Wrap(appErr)
}
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
return a.DoCommandRequest(cmd, p)
}
func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
// Prepare the request
var req *http.Request
var err error
if cmd.Method == model.CommandMethodGet {
req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
} else {
req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
}
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
}
if cmd.Method == model.CommandMethodGet {
if req.URL.RawQuery != "" {
req.URL.RawQuery += "&"
}
req.URL.RawQuery += p.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Method == model.CommandMethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
// Send the request
resp, err := a.HTTPService().MakeClient(false).Do(req)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
}
defer resp.Body.Close()
// Handle the response
body := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
if resp.StatusCode != http.StatusOK {
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := io.ReadAll(body)
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]any{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError)
}
response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
} else if response == nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
}
return cmd, response, nil
}
func (a *App) HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
trigger := ""
if args.Command != "" {
parts := strings.Split(args.Command, " ")
trigger = parts[0][1:]
trigger = strings.ToLower(trigger)
}
var lastError *model.AppError
_, err := a.HandleCommandResponsePost(c, command, args, response, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
if response.ExtraResponses != nil {
for _, resp := range response.ExtraResponses {
_, err := a.HandleCommandResponsePost(c, command, args, resp, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
}
}
if lastError != nil {
return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError)
}
return response, nil
}
func (a *App) HandleCommandResponsePost(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.UserId = args.UserId
post.Type = response.Type
post.SetProps(response.Props)
if response.ChannelId != "" {
_, err := a.GetChannelMember(c, response.ChannelId, args.UserId)
if err != nil {
err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, "", http.StatusForbidden).Wrap(err)
return nil, err
}
post.ChannelId = response.ChannelId
}
isBotPost := !builtIn
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
if command.Username != "" {
post.AddProp("override_username", command.Username)
isBotPost = true
} else if response.Username != "" {
post.AddProp("override_username", response.Username)
isBotPost = true
}
}
if *a.Config().ServiceSettings.EnablePostIconOverride {
if command.IconURL != "" {
post.AddProp("override_icon_url", command.IconURL)
isBotPost = true
} else if response.IconURL != "" {
post.AddProp("override_icon_url", response.IconURL)
isBotPost = true
} else {
post.AddProp("override_icon_url", "")
}
}
if isBotPost {
post.AddProp("from_webhook", "true")
}
// Process Slack text replacements if the response does not contain "skip_slack_parsing": true.
if !response.SkipSlackParsing {
response.Text = a.ProcessSlackText(response.Text)
response.Attachments = a.ProcessSlackAttachments(response.Attachments)
}
if _, err := a.CreateCommandPost(c, post, args.TeamId, response, response.SkipSlackParsing); err != nil {
return post, err
}
return post, nil
}
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
return a.createCommand(cmd)
}
func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) {
cmd.Trigger = strings.ToLower(cmd.Trigger)
teamCmds, err := a.Srv().Store().Command().GetByTeam(cmd.TeamId)
if err != nil {
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingCommand := range teamCmds {
if cmd.Trigger == existingCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
for _, builtInProvider := range commandProviders {
builtInCommand := builtInProvider.GetCommand(a, i18n.T)
if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
command, nErr := a.Srv().Store().Command().Save(cmd)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return command, nil
}
func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
command, err := a.Srv().Store().Command().Get(commandID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Get", "store.sql_command.get.missing.app_error", map[string]any{"command_id": commandID}, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetCommand", "app.command.getcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.CreatorId = oldCmd.CreatorId
updatedCmd.PluginId = oldCmd.PluginId
updatedCmd.TeamId = oldCmd.TeamId
command, err := a.Srv().Store().Command().Update(updatedCmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": updatedCmd.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateCommand", "app.command.updatecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
command.TeamId = team.Id
_, err := a.Srv().Store().Command().Update(command)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": command.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("MoveCommand", "app.command.movecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
cmd.Token = model.NewId()
command, err := a.Srv().Store().Command().Update(cmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": cmd.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("RegenCommandToken", "app.command.regencommandtoken.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) DeleteCommand(commandID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableCommands {
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
err := a.Srv().Store().Command().Delete(commandID, model.GetMillis())
if err != nil {
return model.NewAppError("DeleteCommand", "app.command.deletecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// possibleAtMentions returns all substrings in message that look like valid @
// mentions.
func possibleAtMentions(message string) []string {
var names []string
if !strings.Contains(message, "@") {
return names
}
alreadyMentioned := make(map[string]bool)
for _, match := range atMentionRegexp.FindAllString(message, -1) {
name := model.NormalizeUsername(match[1:])
if !alreadyMentioned[name] && model.IsValidUsernameAllowRemote(name) {
names = append(names, name)
alreadyMentioned[name] = true
}
}
return names
}
// trimUsernameSpecialChar tries to remove the last character from word if it
// is a special character for usernames (dot, dash or underscore). If not, it
// returns the same string.
func trimUsernameSpecialChar(word string) (string, bool) {
len := len(word)
if len > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (len-1) {
return word[:len-1], true
}
return word, false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// AutocompleteDynamicArgProvider dynamically provides auto-completion args for built-in commands.
type AutocompleteDynamicArgProvider interface {
GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error)
}
// GetSuggestions returns suggestions for user input.
func (a *App) GetSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion {
sort.Slice(commands, func(i, j int) bool {
return strings.Compare(strings.ToLower(commands[i].Trigger), strings.ToLower(commands[j].Trigger)) < 0
})
autocompleteData := []*model.AutocompleteData{}
for _, command := range commands {
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
}
autocompleteData = append(autocompleteData, command.AutocompleteData)
}
userInput := commandArgs.Command
suggestions := a.getSuggestions(c, commandArgs, autocompleteData, "", userInput, roleID)
for i, suggestion := range suggestions {
for _, command := range commands {
if strings.HasPrefix(suggestion.Complete, command.Trigger) {
suggestions[i].IconData = command.AutocompleteIconData
break
}
}
}
return suggestions
}
func (a *App) getSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.AutocompleteData, inputParsed, inputToBeParsed, roleID string) []model.AutocompleteSuggestion {
suggestions := []model.AutocompleteSuggestion{}
index := strings.Index(inputToBeParsed, " ")
if index == -1 { // no space in input
for _, command := range commands {
if strings.HasPrefix(command.Trigger, strings.ToLower(inputToBeParsed)) && (command.RoleID == roleID || roleID == model.SystemAdminRoleId || roleID == "") {
s := model.AutocompleteSuggestion{
Complete: inputParsed + command.Trigger,
Suggestion: command.Trigger,
Description: command.HelpText,
Hint: command.Hint,
}
suggestions = append(suggestions, s)
}
}
return suggestions
}
for _, command := range commands {
if command.Trigger != strings.ToLower(inputToBeParsed[:index]) {
continue
}
if roleID != "" && roleID != model.SystemAdminRoleId && roleID != command.RoleID {
continue
}
toBeParsed := inputToBeParsed[index+1:]
parsed := inputParsed + inputToBeParsed[:index+1]
if len(command.Arguments) == 0 {
// Seek recursively in subcommands
subSuggestions := a.getSuggestions(c, commandArgs, command.SubCommands, parsed, toBeParsed, roleID)
suggestions = append(suggestions, subSuggestions...)
continue
}
found, _, _, suggestion := a.parseArguments(c, commandArgs, command.Arguments, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion...)
}
}
return suggestions
}
func (a *App) parseArguments(c *request.Context, commandArgs *model.CommandArgs, args []*model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
if len(args) == 0 {
return false, parsed, toBeParsed, suggestions
}
if args[0].Required {
found, changedParsed, changedToBeParsed, suggestion := a.parseArgument(c, commandArgs, args[0], parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion...)
return true, changedParsed, changedToBeParsed, suggestions
}
return a.parseArguments(c, commandArgs, args[1:], changedParsed, changedToBeParsed)
}
// Handling optional arguments. Optional argument can be inputted or not,
// so we have to pase both cases recursively and output combined suggestions.
foundWithOptional, changedParsedWithOptional, changedToBeParsedWithOptional, suggestionsWithOptional := a.parseArgument(c, commandArgs, args[0], parsed, toBeParsed)
if foundWithOptional {
suggestions = append(suggestions, suggestionsWithOptional...)
} else {
foundWithOptionalRest, changedParsedWithOptionalRest, changedToBeParsedWithOptionalRest, suggestionsWithOptionalRest := a.parseArguments(c, commandArgs, args[1:], changedParsedWithOptional, changedToBeParsedWithOptional)
if foundWithOptionalRest {
suggestions = append(suggestions, suggestionsWithOptionalRest...)
}
foundWithOptional = foundWithOptionalRest
changedParsedWithOptional = changedParsedWithOptionalRest
changedToBeParsedWithOptional = changedToBeParsedWithOptionalRest
}
foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestionsWithoutOptional := a.parseArguments(c, commandArgs, args[1:], parsed, toBeParsed)
if foundWithoutOptional {
suggestions = append(suggestions, suggestionsWithoutOptional...)
}
// if suggestions were found we can return them
if foundWithOptional || foundWithoutOptional {
return true, parsed + toBeParsed, "", suggestions
}
// no suggestions found yet, check if optional argument was inputted
if changedParsedWithOptional != parsed && changedToBeParsedWithOptional != toBeParsed {
return false, changedParsedWithOptional, changedToBeParsedWithOptional, suggestions
}
// no suggestions and optional argument was not inputted
return foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestions
}
func (a *App) parseArgument(c *request.Context, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
if arg.Name != "" { //Parse the --name first
found, changedParsed, changedToBeParsed, suggestion := parseNamedArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion)
return true, changedParsed, changedToBeParsed, suggestions
}
if changedToBeParsed == "" {
return true, changedParsed, changedToBeParsed, suggestions
}
if changedToBeParsed == " " {
changedToBeParsed = ""
}
parsed = changedParsed
toBeParsed = changedToBeParsed
}
if arg.Type == model.AutocompleteArgTypeText {
found, changedParsed, changedToBeParsed, suggestion := parseInputTextArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
} else if arg.Type == model.AutocompleteArgTypeStaticList {
found, changedParsed, changedToBeParsed, staticListSuggestions := parseStaticListArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, staticListSuggestions...)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
} else if arg.Type == model.AutocompleteArgTypeDynamicList {
found, changedParsed, changedToBeParsed, dynamicListSuggestions := a.getDynamicListArgument(c, commandArgs, arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, dynamicListSuggestions...)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
}
return false, parsed, toBeParsed, suggestions
}
func parseNamedArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
namedArg := "--" + arg.Name
if in == "" { //The user has not started typing the argument.
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
}
if strings.HasPrefix(strings.ToLower(namedArg), strings.ToLower(in)) {
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg[len(in):] + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
}
if !strings.HasPrefix(strings.ToLower(in), strings.ToLower(namedArg)+" ") {
return false, parsed + toBeParsed, "", model.AutocompleteSuggestion{}
}
if strings.ToLower(in) == strings.ToLower(namedArg)+" " {
return false, parsed + namedArg + " ", " ", model.AutocompleteSuggestion{}
}
return false, parsed + namedArg + " ", in[len(namedArg)+1:], model.AutocompleteSuggestion{}
}
func parseInputTextArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
a := arg.Data.(*model.AutocompleteTextArg)
if in == "" { //The user has not started typing the argument.
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
if in[0] == '"' { //input with multiple words
indexOfSecondQuote := strings.Index(in[1:], `"`)
if indexOfSecondQuote == -1 { //typing of the multiple word argument is not finished
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
// this argument is typed already
offset := 2
if len(in) > indexOfSecondQuote+2 && in[indexOfSecondQuote+2] == ' ' {
offset++
}
return false, parsed + in[:indexOfSecondQuote+offset], in[indexOfSecondQuote+offset:], model.AutocompleteSuggestion{}
}
// input with a single word
index := strings.Index(in, " ")
if index == -1 { // typing of the single word argument is not finished
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
// single word argument already typed
return false, parsed + in[:index+1], in[index+1:], model.AutocompleteSuggestion{}
}
func parseStaticListArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
a := arg.Data.(*model.AutocompleteStaticListArg)
return parseListItems(a.PossibleArguments, parsed, toBeParsed)
}
func (a *App) getDynamicListArgument(c *request.Context, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
if strings.HasPrefix(dynamicArg.FetchURL, "builtin:") {
listItems, err := a.getBuiltinDynamicListArgument(commandArgs, arg, parsed, toBeParsed)
if err != nil {
a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
}
return parseListItems(listItems, parsed, toBeParsed)
}
params := url.Values{}
params.Add("user_input", parsed+toBeParsed)
params.Add("parsed", parsed)
// Encode the information normally provided to a plugin slash command handler into the request parameters
// Encode PluginContext:
pluginContext := pluginContext(c)
params.Add("request_id", pluginContext.RequestId)
params.Add("session_id", pluginContext.SessionId)
params.Add("ip_address", pluginContext.IPAddress)
params.Add("accept_language", pluginContext.AcceptLanguage)
params.Add("user_agent", pluginContext.UserAgent)
// Encode CommandArgs:
params.Add("channel_id", commandArgs.ChannelId)
params.Add("team_id", commandArgs.TeamId)
params.Add("root_id", commandArgs.RootId)
params.Add("user_id", commandArgs.UserId)
params.Add("site_url", commandArgs.SiteURL)
resp, err := a.doPluginRequest(c, "GET", dynamicArg.FetchURL, params, nil)
if err != nil {
a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
}
var listItems []model.AutocompleteListItem
if jsonErr := json.NewDecoder(resp.Body).Decode(&listItems); jsonErr != nil {
c.Logger().Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
return parseListItems(listItems, parsed, toBeParsed)
}
func parseListItems(items []model.AutocompleteListItem, parsed, toBeParsed string) (bool, string, string, []model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
suggestions := []model.AutocompleteSuggestion{}
maxPrefix := ""
for _, arg := range items {
if strings.HasPrefix(strings.ToLower(in), strings.ToLower(arg.Item)+" ") && len(maxPrefix) < len(arg.Item)+1 {
maxPrefix = arg.Item + " "
}
}
if maxPrefix != "" { //typing of an argument finished
return false, parsed + in[:len(maxPrefix)], in[len(maxPrefix):], []model.AutocompleteSuggestion{}
}
// user has not finished typing static argument
for _, arg := range items {
if strings.HasPrefix(strings.ToLower(arg.Item), strings.ToLower(in)) {
suggestions = append(suggestions, model.AutocompleteSuggestion{Complete: parsed + arg.Item, Suggestion: arg.Item, Hint: arg.Hint, Description: arg.HelpText})
}
}
return true, parsed + toBeParsed, "", suggestions
}
func (a *App) getBuiltinDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
arr := strings.Split(dynamicArg.FetchURL, ":")
if len(arr) < 2 {
return nil, errors.New("dynamic list URL missing built-in command name")
}
cmdName := arr[1]
provider := GetCommandProvider(cmdName)
if provider == nil {
return nil, fmt.Errorf("no command provider for %s", cmdName)
}
dp, ok := provider.(AutocompleteDynamicArgProvider)
if !ok {
return nil, fmt.Errorf("auto-completion not available for built-in command %s", cmdName)
}
return dp.GetAutoCompleteListItems(a, commandArgs, arg, parsed, toBeParsed)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"os"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance {
return nil, model.NewAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
compliances, err := a.Srv().Store().Compliance().GetAll(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetComplianceReports", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return compliances, nil
}
func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
return nil, model.NewAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
job.Type = model.ComplianceTypeAdhoc
job, err := a.Srv().Store().Compliance().Save(job)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveComplianceReport", "app.compliance.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
jCopy := job.DeepCopy()
a.Srv().Go(func() {
err := a.Compliance().RunComplianceJob(jCopy)
if err != nil {
mlog.Warn("Error running compliance job", mlog.Err(err))
}
})
return job, nil
}
func (a *App) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
return nil, model.NewAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
compliance, err := a.Srv().Store().Compliance().Get(reportId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return compliance, nil
}
func (a *App) GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
f, err := os.ReadFile(*a.Config().ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip")
if err != nil {
return nil, model.NewAppError("readFile", "api.file.read_file.reading_local.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
return f, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/json"
"net/url"
"reflect"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
ErrorTermsOfServiceNoRowsFound = "app.terms_of_service.get.no_rows.app_error"
)
func (s *Server) Config() *model.Config {
return s.platform.Config()
}
func (a *App) Config() *model.Config {
return a.ch.cfgSvc.Config()
}
func (a *App) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
return a.Srv().platform.GetEnvironmentOverridesWithFilter(filter)
}
func (a *App) UpdateConfig(f func(*model.Config)) {
a.Srv().platform.UpdateConfig(f)
}
func (a *App) ReloadConfig() error {
return a.Srv().platform.ReloadConfig()
}
func (a *App) ClientConfig() map[string]string {
return a.ch.srv.platform.ClientConfig()
}
func (a *App) ClientConfigHash() string {
return a.ch.ClientConfigHash()
}
func (a *App) LimitedClientConfig() map[string]string {
return a.ch.srv.platform.LimitedClientConfig()
}
func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return a.Srv().platform.AddConfigListener(listener)
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (a *App) RemoveConfigListener(id string) {
a.Srv().platform.RemoveConfigListener(id)
}
// ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists
// and future calls to PostActionCookieSecret will always return a valid key, same on all
// servers in the cluster
func (ch *Channels) ensurePostActionCookieSecret() error {
if ch.postActionCookieSecret != nil {
return nil
}
var secret *model.SystemPostActionCookieSecret
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if secret == nil {
newSecret := &model.SystemPostActionCookieSecret{
Secret: make([]byte, 32),
}
_, err := rand.Reader.Read(newSecret.Secret)
if err != nil {
return err
}
system := &model.System{
Name: model.SystemPostActionCookieSecretKey,
}
v, err := json.Marshal(newSecret)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = ch.srv.Store().System().Save(system); err != nil {
mlog.Warn("Failed to save PostActionCookieSecret", mlog.Err(err))
} else {
secret = newSecret
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if secret == nil {
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
ch.postActionCookieSecret = secret.Secret
return nil
}
func (s *Server) ensureInstallationDate() error {
_, appErr := s.platform.GetSystemInstallDate()
if appErr == nil {
return nil
}
installDate, nErr := s.Store().User().InferSystemInstallDate()
var installationDate int64
if nErr == nil && installDate > 0 {
installationDate = installDate
} else {
installationDate = utils.MillisFromTime(time.Now())
}
if err := s.Store().System().SaveOrUpdate(&model.System{
Name: model.SystemInstallationDateKey,
Value: strconv.FormatInt(installationDate, 10),
}); err != nil {
return err
}
return nil
}
func (s *Server) ensureFirstServerRunTimestamp() error {
_, appErr := s.getFirstServerRunTimestamp()
if appErr == nil {
return nil
}
if err := s.Store().System().SaveOrUpdate(&model.System{
Name: model.SystemFirstServerRunTimestampKey,
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
}); err != nil {
return err
}
return nil
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (ch *Channels) AsymmetricSigningKey() *ecdsa.PrivateKey {
return ch.srv.platform.AsymmetricSigningKey()
}
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
return a.ch.AsymmetricSigningKey()
}
func (ch *Channels) PostActionCookieSecret() []byte {
return ch.postActionCookieSecret
}
func (a *App) PostActionCookieSecret() []byte {
return a.ch.PostActionCookieSecret()
}
func (a *App) GetCookieDomain() string {
if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
return siteURL.Hostname()
}
}
return ""
}
func (a *App) GetSiteURL() string {
return *a.Config().ServiceSettings.SiteURL
}
// GetConfigFile proxies access to the given configuration file to the underlying config store.
func (a *App) GetConfigFile(name string) ([]byte, error) {
data, err := a.Srv().platform.GetConfigFile(name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get config file %s", name)
}
return data, nil
}
// GetSanitizedConfig gets the configuration for a system admin without any secrets.
func (a *App) GetSanitizedConfig() *model.Config {
cfg := a.Config().Clone()
cfg.Sanitize()
return cfg
}
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
// If filter is not nil and returns false for a struct field, that field will be omitted.
func (a *App) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
return a.EnvironmentConfig(filter)
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
return a.Srv().platform.SaveConfig(newCfg, sendConfigChangeClusterMessage)
}
func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
// If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an
// appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file
// directly and not through the System Console UI.
if *cfg.MessageExportSettings.EnableExport != *appCfg.MessageExportSettings.EnableExport {
if *cfg.MessageExportSettings.EnableExport && *cfg.MessageExportSettings.ExportFromTimestamp == int64(0) {
// When the feature is toggled on, use the current timestamp as the start time for future exports.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis())
} else if !*cfg.MessageExportSettings.EnableExport {
// When the feature is disabled, reset the timestamp so that the timestamp will be set if
// the feature is re-enabled from the System Console in future.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(0)
}
}
}
func (s *Server) MailServiceConfig() *mail.SMTPConfig {
emailSettings := s.platform.Config().EmailSettings
hostname := utils.GetHostnameFromSiteURL(*s.platform.Config().ServiceSettings.SiteURL)
cfg := mail.SMTPConfig{
Hostname: hostname,
ConnectionSecurity: *emailSettings.ConnectionSecurity,
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
ServerName: *emailSettings.SMTPServer,
Server: *emailSettings.SMTPServer,
Port: *emailSettings.SMTPPort,
ServerTimeout: *emailSettings.SMTPServerTimeout,
Username: *emailSettings.SMTPUsername,
Password: *emailSettings.SMTPPassword,
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
SendEmailNotifications: *emailSettings.SendEmailNotifications,
FeedbackName: *emailSettings.FeedbackName,
FeedbackEmail: *emailSettings.FeedbackEmail,
ReplyToAddress: *emailSettings.ReplyToAddress,
}
return &cfg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
// WithMaster adds the context value that master DB should be selected for this request.
func WithMaster(ctx context.Context) context.Context {
return sqlstore.WithMaster(ctx)
}
func pluginContext(c request.CTX) *plugin.Context {
context := &plugin.Context{
RequestId: c.RequestId(),
SessionId: c.Session().Id,
IPAddress: c.IPAddress(),
AcceptLanguage: c.AcceptLanguage(),
UserAgent: c.UserAgent(),
}
return context
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetGlobalRetentionPolicy")
}
return a.DataRetention().GetGlobalPolicy()
}
func (a *App) GetRetentionPolicies(offset, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetRetentionPolicies")
}
return a.DataRetention().GetPolicies(offset, limit)
}
func (a *App) GetRetentionPoliciesCount() (int64, *model.AppError) {
if a.DataRetention() == nil {
return 0, newLicenseError("GetRetentionPoliciesCount")
}
return a.DataRetention().GetPoliciesCount()
}
func (a *App) GetRetentionPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetRetentionPolicy")
}
return a.DataRetention().GetPolicy(policyID)
}
func (a *App) CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("CreateRetentionPolicy")
}
return a.DataRetention().CreatePolicy(policy)
}
func (a *App) PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("PatchRetentionPolicy")
}
return a.DataRetention().PatchPolicy(patch)
}
func (a *App) DeleteRetentionPolicy(policyID string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("DeleteRetentionPolicy")
}
return a.DataRetention().DeletePolicy(policyID)
}
func (a *App) GetTeamsForRetentionPolicy(policyID string, offset, limit int) (*model.TeamsWithCount, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetTeamsForRetentionPolicy")
}
return a.DataRetention().GetTeamsForPolicy(policyID, offset, limit)
}
func (a *App) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("AddTeamsToRetentionPolicy")
}
return a.DataRetention().AddTeamsToPolicy(policyID, teamIDs)
}
func (a *App) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("RemoveTeamsFromRetentionPolicy")
}
return a.DataRetention().RemoveTeamsFromPolicy(policyID, teamIDs)
}
func (a *App) GetChannelsForRetentionPolicy(policyID string, offset, limit int) (*model.ChannelsWithCount, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetChannelsForRetentionPolicy")
}
return a.DataRetention().GetChannelsForPolicy(policyID, offset, limit)
}
func (a *App) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("AddChannelsToRetentionPolicies")
}
return a.DataRetention().AddChannelsToPolicy(policyID, channelIDs)
}
func (a *App) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("RemoveChannelsFromRetentionPolicy")
}
return a.DataRetention().RemoveChannelsFromPolicy(policyID, channelIDs)
}
func (a *App) GetTeamPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetTeamPoliciesForUser")
}
return a.DataRetention().GetTeamPoliciesForUser(userID, offset, limit)
}
func (a *App) GetChannelPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetChannelPoliciesForUser")
}
return a.DataRetention().GetChannelPoliciesForUser(userID, offset, limit)
}
func newLicenseError(methodName string) *model.AppError {
return model.NewAppError("App."+methodName, "ent.data_retention.generic.license.error",
nil, "", http.StatusNotImplemented)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"io"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
// HTTPRequestTimeout defines a high timeout for downloading large files
// from an external URL to avoid slow connections from failing to install.
HTTPRequestTimeout = 1 * time.Hour
)
func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) {
return a.Srv().downloadFromURL(downloadURL)
}
func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) {
if !model.IsValidHTTPURL(downloadURL) {
return nil, errors.Errorf("invalid url %s", downloadURL)
}
u, err := url.ParseRequestURI(downloadURL)
if err != nil {
return nil, errors.Errorf("failed to parse url %s", downloadURL)
}
if !*s.platform.Config().PluginSettings.AllowInsecureDownloadURL && u.Scheme != "https" {
return nil, errors.Errorf("insecure url not allowed %s", downloadURL)
}
client := s.HTTPService().MakeClient(true)
client.Timeout = HTTPRequestTimeout
var resp *http.Response
err = utils.ProgressiveRetry(func() error {
resp, err = client.Get(downloadURL)
if err != nil {
return errors.Wrapf(err, "failed to fetch from %s", downloadURL)
}
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return errors.Errorf("failed to fetch from %s", downloadURL)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "download failed after multiple retries.")
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetDraft(userID, channelID, rootID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("GetDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
draft, err := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return draft, nil
}
func (a *App) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
dt, dErr := a.Srv().Store().Draft().Get(draft.UserId, draft.ChannelId, draft.RootId, true)
var notFoundErr *store.ErrNotFound
if dErr != nil && !errors.As(dErr, ¬FoundErr) {
return nil, model.NewAppError("UpsertDraft", "app.select_error", nil, dErr.Error(), http.StatusInternalServerError)
}
var err *model.AppError
if dt == nil {
dt, err = a.CreateDraft(c, draft, connectionID)
if err != nil {
return nil, err
}
} else {
dt, err = a.UpdateDraft(c, draft, connectionID)
if err != nil {
return nil, err
}
}
return dt, nil
}
func (a *App) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("CreateDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
// Check that channel exists and has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
if errCh != nil {
err := model.NewAppError("CreateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("CreateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
if nErr != nil {
return nil, model.NewAppError("CreateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt, nErr := a.Srv().Store().Draft().Save(draft)
if nErr != nil {
return nil, model.NewAppError("CreateDraft", "app.draft.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
message := model.NewWebSocketEvent(model.WebsocketEventDraftCreated, "", dt.ChannelId, dt.UserId, nil, connectionID)
draftJSON, jsonErr := json.Marshal(dt)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
}
message.Add("draft", string(draftJSON))
a.Publish(message)
return dt, nil
}
func (a *App) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts {
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
// Check that channel exists and has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
if errCh != nil {
err := model.NewAppError("UpdateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("UpdateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
if nErr != nil {
return nil, model.NewAppError("UpdateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt, nErr := a.Srv().Store().Draft().Update(draft)
if nErr != nil {
return nil, model.NewAppError("UpdateDraft", "app.draft.update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
message := model.NewWebSocketEvent(model.WebsocketEventDraftUpdated, "", draft.ChannelId, draft.UserId, nil, connectionID)
draftJSON, jsonErr := json.Marshal(dt)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
}
message.Add("draft", string(draftJSON))
a.Publish(message)
return dt, nil
}
func (a *App) GetDraftsForUser(userID, teamID string) ([]*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("GetDraftsForUser", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
drafts, err := a.Srv().Store().Draft().GetDraftsForUser(userID, teamID)
if err != nil {
return nil, model.NewAppError("GetDraftsForUser", "app.draft.get_drafts.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, draft := range drafts {
a.prepareDraftWithFileInfos(userID, draft)
}
return drafts, nil
}
func (a *App) prepareDraftWithFileInfos(userID string, draft *model.Draft) *model.Draft {
if fileInfos, err := a.getFileInfosForDraft(draft); err != nil {
mlog.Error("Failed to get files for a user's drafts", mlog.String("user_id", userID), mlog.Err(err))
} else {
draft.Metadata = &model.PostMetadata{}
draft.Metadata.Files = fileInfos
}
return draft
}
func (a *App) getFileInfosForDraft(draft *model.Draft) ([]*model.FileInfo, *model.AppError) {
if len(draft.FileIds) == 0 {
return nil, nil
}
fileInfos, err := a.Srv().Store().FileInfo().GetByIds(draft.FileIds)
if err != nil {
return nil, model.NewAppError("GetFileInfosForDraft", "app.draft.get_for_draft.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) DeleteDraft(userID, channelID, rootID, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("DeleteDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
draft, nErr := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
if nErr != nil {
return nil, model.NewAppError("DeleteDraft", "app.draft.get.app_error", nil, nErr.Error(), http.StatusBadRequest)
}
if err := a.Srv().Store().Draft().Delete(userID, channelID, rootID); err != nil {
return nil, model.NewAppError("DeleteDraft", "app.draft.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
draftJSON, jsonErr := json.Marshal(draft)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON")
}
message := model.NewWebSocketEvent(model.WebsocketEventDraftDeleted, "", draft.ChannelId, draft.UserId, nil, connectionID)
message.Add("draft", string(draftJSON))
a.Publish(message)
return draft, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
"github.com/microcosm-cc/bluemonday"
)
// Returns category if enabled is true (default false)
// If "" is returned when enabled is false, the category headers aren't attached to the email
func getSendGridCategory(category string, enabled bool) string {
if enabled {
return category
}
return ""
}
func (es *Service) SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.username_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.username_change_body.title")
data.Props["Info"] = T("api.templates.username_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "NewUsername": newUsername})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("email_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "ChangeUsernameEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(newUserEmail))
subject := T("api.templates.email_change_verify_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.email_change_verify_body.title")
data.Props["Info"] = T("api.templates.email_change_verify_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName})
data.Props["VerifyUrl"] = link
data.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["EmailInfo1"] = T("api.templates.email_us_anytime_at")
data.Props["SupportEmail"] = "feedback@mattermost.com"
data.Props["FooterV2"] = T("api.templates.email_footer_v2")
body, err := es.templatesContainer.RenderToString("email_change_verify_body", data)
if err != nil {
return err
}
if err := es.sendMail(newUserEmail, subject, body, "EmailChangeVerifyEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.email_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.email_change_body.title")
data.Props["Info"] = T("api.templates.email_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "NewEmail": newEmail})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("email_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(oldEmail, subject, body, "EmailChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(userEmail))
if redirect != "" {
link += fmt.Sprintf("&redirect_to=%s", redirect)
}
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.verify_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.verify_body.title")
data.Props["SubTitle1"] = T("api.templates.verify_body.subTitle1")
data.Props["ServerURL"] = T("api.templates.verify_body.serverURL", map[string]any{"ServerURL": serverURL})
data.Props["SubTitle2"] = T("api.templates.verify_body.subTitle2")
data.Props["ButtonURL"] = link
data.Props["Button"] = T("api.templates.verify_body.button")
data.Props["Info"] = T("api.templates.verify_body.info")
data.Props["Info1"] = T("api.templates.verify_body.info1")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("verify_body", data)
if err != nil {
return err
}
if err := es.sendMail(userEmail, subject, body, "VerifyEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendSignInChangeEmail(email, method, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.signin_change_email.subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.signin_change_email.body.title")
data.Props["Info"] = T("api.templates.signin_change_email.body.info",
map[string]any{"SiteName": es.config().TeamSettings.SiteName, "Method": method})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("signin_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "SignInChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error {
if disableWelcomeEmail {
return nil
}
if !*es.config().EmailSettings.SendEmailNotifications && !*es.config().EmailSettings.RequireEmailVerification {
return errors.New("send email notifications and require email verification is disabled in the system console")
}
T := i18n.GetUserTranslations(locale)
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.welcome_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"ServerURL": serverURL})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.welcome_body.title")
data.Props["SubTitle1"] = T("api.templates.welcome_body.subTitle1")
data.Props["ServerURL"] = T("api.templates.welcome_body.serverURL", map[string]any{"ServerURL": serverURL})
data.Props["SubTitle2"] = T("api.templates.welcome_body.subTitle2")
data.Props["Button"] = T("api.templates.welcome_body.button")
data.Props["Info"] = T("api.templates.welcome_body.info")
data.Props["Info1"] = T("api.templates.welcome_body.info1")
data.Props["SiteURL"] = siteURL
if *es.config().NativeAppSettings.AppDownloadLink != "" {
data.Props["AppDownloadTitle"] = T("api.templates.welcome_body.app_download_title")
data.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info")
data.Props["AppDownloadButton"] = T("api.templates.welcome_body.app_download_button")
data.Props["AppDownloadLink"] = *es.config().NativeAppSettings.AppDownloadLink
}
if !verified && *es.config().EmailSettings.RequireEmailVerification {
token, err := es.CreateVerifyEmailToken(userID, email)
if err != nil {
return err
}
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token.Token, url.QueryEscape(email))
if redirect != "" {
link += fmt.Sprintf("&redirect_to=%s", redirect)
}
data.Props["ButtonURL"] = link
}
body, err := es.templatesContainer.RenderToString("welcome_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "WelcomeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.cloud_upgrade_confirmation.subject")
data := es.NewEmailTemplateData(locale)
data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title")
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_monthly.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date})
data.Props["SiteURL"] = siteURL
data.Props["ButtonURL"] = siteURL
data.Props["Button"] = T("api.templates.cloud_welcome_email.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
if isYearly {
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_yearly.subtitle", map[string]any{"WorkspaceName": workspaceName})
data.Props["ButtonURL"] = siteURL + "/admin_console/billing/billing_history"
data.Props["Button"] = T("api.templates.cloud_welcome_email.yearly_plan_button")
}
body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data)
if err != nil {
return err
}
if isYearly {
if err := es.SendMailWithEmbeddedFilesAndCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, embeddedFiles, "CloudUpgradeConfirmationEmail"); err != nil {
return err
}
} else {
if err := es.sendEmailWithCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, "CloudUpgradeConfirmationEmail"); err != nil {
return err
}
}
return nil
}
// SendCloudWelcomeEmail sends the cloud version of the welcome email
func (es *Service) SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.cloud_welcome_email.subject")
data := es.NewEmailTemplateData(locale)
data.Props["Title"] = T("api.templates.cloud_welcome_email.title")
data.Props["SubTitle"] = T("api.templates.cloud_welcome_email.subtitle")
data.Props["SubTitleInfo"] = T("api.templates.cloud_welcome_email.subtitle_info")
data.Props["Info"] = T("api.templates.cloud_welcome_email.info")
data.Props["Info2"] = T("api.templates.cloud_welcome_email.info2")
data.Props["WorkSpacePath"] = siteURL
data.Props["DNS"] = dns
data.Props["InviteInfo"] = T("api.templates.cloud_welcome_email.invite_info")
data.Props["InviteSubInfo"] = T("api.templates.cloud_welcome_email.invite_sub_info", map[string]any{"WorkSpace": workSpaceName})
data.Props["InviteSubInfoLink"] = fmt.Sprintf("%s/signup_user_complete/?id=%s", siteURL, teamInviteID)
data.Props["AddAppsInfo"] = T("api.templates.cloud_welcome_email.add_apps_info")
data.Props["AddAppsSubInfo"] = T("api.templates.cloud_welcome_email.add_apps_sub_info")
data.Props["AppMarketPlace"] = T("api.templates.cloud_welcome_email.app_market_place")
data.Props["AppMarketPlaceLink"] = "https://integrations.mattermost.com/"
data.Props["DownloadMMInfo"] = T("api.templates.cloud_welcome_email.download_mm_info")
data.Props["SignInSubInfo"] = T("api.templates.cloud_welcome_email.signin_sub_info")
data.Props["MMApps"] = T("api.templates.cloud_welcome_email.mm_apps")
data.Props["SignInSubInfo2"] = T("api.templates.cloud_welcome_email.signin_sub_info2")
data.Props["DownloadMMAppsLink"] = "https://mattermost.com/download/"
data.Props["Button"] = T("api.templates.cloud_welcome_email.button")
data.Props["GettingStartedQuestions"] = T("api.templates.cloud_welcome_email.start_questions")
body, err := es.templatesContainer.RenderToString("cloud_welcome_email", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, "CloudWelcomeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendPasswordChangeEmail(email, method, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.password_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.password_change_body.title")
data.Props["Info"] = T("api.templates.password_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "TeamURL": siteURL, "Method": method})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("password_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "PasswordChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendUserAccessTokenAddedEmail(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.user_access_token_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.user_access_token_body.title")
data.Props["Info"] = T("api.templates.user_access_token_body.info",
map[string]any{"SiteName": es.config().TeamSettings.SiteName, "SiteURL": siteURL})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("password_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "UserAccessTokenAddedEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error) {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/reset_password_complete?token=%s", siteURL, url.QueryEscape(token.Token))
subject := T("api.templates.reset_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.reset_body.title")
data.Props["SubTitle"] = T("api.templates.reset_body.subTitle")
data.Props["Info"] = T("api.templates.reset_body.info")
data.Props["ButtonURL"] = link
data.Props["Button"] = T("api.templates.reset_body.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("reset_body", data)
if err != nil {
return false, err
}
if err := es.sendMail(email, subject, body, "PasswordResetEmail"); err != nil {
return false, err
}
return true, nil
}
func (es *Service) SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.mfa_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
if activated {
data.Props["Info"] = T("api.templates.mfa_activated_body.info", map[string]any{"SiteURL": siteURL})
data.Props["Title"] = T("api.templates.mfa_activated_body.title")
} else {
data.Props["Info"] = T("api.templates.mfa_deactivated_body.info", map[string]any{"SiteURL": siteURL})
data.Props["Title"] = T("api.templates.mfa_deactivated_body.title")
}
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("mfa_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "MfaChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendInviteEmails(
team *model.Team,
senderName string,
senderUserId string,
invites []string,
siteURL string,
reminderData *model.TeamInviteReminderData,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return RateLimitExceededError
}
for _, invite := range invites {
if invite != "" {
subject := i18n.T("api.templates.invite_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
token := model.NewToken(
TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": team.Id, "email": invite}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
title := i18n.T("api.templates.invite_body.title", map[string]any{"SenderName": senderName, "TeamDisplayName": team.DisplayName})
if reminderData != nil {
reminder := i18n.T("api.templates.invite_body.title.reminder")
title = fmt.Sprintf("%s: %s", reminder, title)
tokenProps["reminder_interval"] = reminderData.Interval
}
data.Props["Title"] = title
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
queryString := url.Values{}
queryString.Add("d", tokenData)
queryString.Add("t", token.Token)
queryString.Add("md", "email")
queryString.Add("sbr", es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?%s", siteURL, queryString.Encode())
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
}
if err := es.sendMail(invite, subject, body, "InviteEmail"); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
if errorWhenNotSent {
return SendMailError
}
}
}
}
return nil
}
func (es *Service) SendGuestInviteEmails(
team *model.Team,
channels []*model.Channel,
senderName string,
senderUserId string,
senderProfileImage []byte,
invites []string,
siteURL string,
message string,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return RateLimitExceededError
}
for _, invite := range invites {
if invite != "" {
subject := i18n.T("api.templates.invite_guest_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]any{"SenderName": senderName, "TeamDisplayName": team.DisplayName})
data.Props["SubTitle"] = i18n.T("api.templates.invite_body_guest.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
if message != "" {
message = bluemonday.NewPolicy().Sanitize(message)
}
data.Props["Message"] = message
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
channelIDs := []string{}
for _, channel := range channels {
channelIDs = append(channelIDs, channel.Id)
}
token := model.NewToken(
TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{
"teamId": team.Id,
"channels": strings.Join(channelIDs, " "),
"email": invite,
"guest": "true",
"senderId": senderUserId,
}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
if !*es.config().EmailSettings.SendEmailNotifications {
mlog.Info("sending invitation ", mlog.String("to", invite), mlog.String("link", data.Props["ButtonURL"].(string)))
}
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if message != "" {
if senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
}
pData := postData{
SenderName: senderName,
Message: template.HTML(message),
SenderPhoto: senderPhoto,
}
data.Props["Posts"] = []postData{pData}
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(err))
}
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles, "", "", "", "InviteEmail"); nErr != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
if errorWhenNotSent {
return SendMailError
}
}
}
}
return nil
}
func (es *Service) SendInviteEmailsToTeamAndChannels(
team *model.Team,
channels []*model.Channel,
senderName string,
senderUserId string,
senderProfileImage []byte,
invites []string,
siteURL string,
reminderData *model.TeamInviteReminderData,
message string,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) ([]*model.EmailInviteWithError, error) {
if es.perHourEmailRateLimiter == nil {
return nil, NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return nil, SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return nil, RateLimitExceededError
}
channelsLen := len(channels)
subject := i18n.T("api.templates.invite_team_and_channels_subject", map[string]any{
"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"ChannelsLen": channelsLen,
"SiteName": es.config().TeamSettings.SiteName})
title := i18n.T("api.templates.invite_team_and_channels_body.title", map[string]any{
"SenderName": senderName,
"ChannelsLen": channelsLen,
"TeamDisplayName": team.DisplayName})
if channelsLen == 1 {
channelName := channels[0].DisplayName
subject = i18n.T("api.templates.invite_team_and_channel_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"ChannelName": channelName,
"SiteName": es.config().TeamSettings.SiteName},
)
title = i18n.T("api.templates.invite_team_and_channel_body.title", map[string]any{
"SenderName": senderName,
"ChannelName": channelName,
"TeamDisplayName": team.DisplayName,
})
}
var invitesWithErrors []*model.EmailInviteWithError
for _, invite := range invites {
if invite == "" {
continue
}
channelIDs := []string{}
for _, channel := range channels {
channelIDs = append(channelIDs, channel.Id)
}
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
if message != "" {
message = bluemonday.NewPolicy().Sanitize(message)
}
data.Props["Message"] = message
token := model.NewToken(
TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{
"teamId": team.Id,
"email": invite,
"channels": strings.Join(channelIDs, " "),
"senderId": senderUserId,
}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
if reminderData != nil {
reminder := i18n.T("api.templates.invite_body.title.reminder")
title = fmt.Sprintf("%s: %s", reminder, title)
tokenProps["reminder_interval"] = reminderData.Interval
}
data.Props["Title"] = title
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if message != "" {
if senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
}
pData := postData{
SenderName: senderName,
Message: template.HTML(message),
SenderPhoto: senderPhoto,
}
data.Props["Posts"] = []postData{pData}
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
}
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles, "", "", "", "InviteEmailToTeamsAndChannels"); nErr != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
if errorWhenNotSent {
inviteWithError := &model.EmailInviteWithError{
Email: invite,
Error: &model.AppError{Message: nErr.Error()},
}
invitesWithErrors = append(invitesWithErrors, inviteWithError)
}
}
}
return invitesWithErrors, nil
}
func (es *Service) NewEmailTemplateData(locale string) templates.Data {
var localT i18n.TranslateFunc
if locale != "" {
localT = i18n.GetUserTranslations(locale)
} else {
localT = i18n.T
}
organization := ""
if *es.config().EmailSettings.FeedbackOrganization != "" {
organization = localT("api.templates.email_organization") + *es.config().EmailSettings.FeedbackOrganization
}
return templates.Data{
Props: map[string]any{
"EmailInfo1": localT("api.templates.email_info1"),
"EmailInfo2": localT("api.templates.email_info2"),
"EmailInfo3": localT("api.templates.email_info3",
map[string]any{"SiteName": es.config().TeamSettings.SiteName}),
"SupportEmail": *es.config().SupportSettings.SupportEmail,
"Footer": localT("api.templates.email_footer"),
"FooterV2": localT("api.templates.email_footer_v2"),
"Organization": organization,
},
HTML: map[string]template.HTML{},
}
}
func (es *Service) SendDeactivateAccountEmail(email string, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.deactivate_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"ServerURL": serverURL})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.deactivate_body.title", map[string]any{"ServerURL": serverURL})
data.Props["Info"] = T("api.templates.deactivate_body.info",
map[string]any{"SiteURL": siteURL})
data.Props["Warning"] = T("api.templates.deactivate_body.warning")
body, err := es.templatesContainer.RenderToString("deactivate_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "DeactivateAccountEmail"); err != nil { // this needs to receive the header options
return err
}
return nil
}
func (es *Service) SendNotificationMail(to, subject, htmlBody string) error {
if !*es.config().EmailSettings.SendEmailNotifications {
return nil
}
return es.sendMail(to, subject, htmlBody, "NotificationEmail")
}
func (es *Service) sendMail(to, subject, htmlBody, category string) error {
return es.sendMailWithCC(to, subject, htmlBody, "", category)
}
func (es *Service) sendEmailWithCustomReplyTo(to, subject, htmlBody, replyToAddress, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig(replyToAddress)
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailUsingConfig(to, subject, htmlBody, mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", category)
}
func (es *Service) sendMailWithCC(to, subject, htmlBody, ccMail, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig("")
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailUsingConfig(to, subject, htmlBody, mailConfig, license != nil && *license.Features.Compliance, "", "", "", ccMail, category)
}
func (es *Service) SendMailWithEmbeddedFilesAndCustomReplyTo(to, subject, htmlBody, replyToAddress string, embeddedFiles map[string]io.Reader, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig(replyToAddress)
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", category)
}
func (es *Service) SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig("")
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, mailConfig, license != nil && *license.Features.Compliance, messageID, inReplyTo, references, "", category)
}
func (es *Service) InvalidateVerifyEmailTokensForUser(userID string) *model.AppError {
tokens, err := es.store.Token().GetAllTokensByType(TokenTypeVerifyEmail)
if err != nil {
return model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var appErr *model.AppError = nil
for _, token := range tokens {
tokenExtra := struct {
UserId string
Email string
}{}
if err := json.Unmarshal([]byte(token.Extra), &tokenExtra); err != nil {
appErr = model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens_parse.error", nil, "", http.StatusInternalServerError).Wrap(err)
continue
}
if tokenExtra.UserId != userID {
continue
}
if err := es.store.Token().Delete(token.Token); err != nil {
appErr = model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens_delete.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return appErr
}
func (es *Service) CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error) {
tokenExtra := struct {
UserId string
Email string
}{
userID,
newEmail,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, errors.Wrap(CreateEmailTokenError, err.Error())
}
token := model.NewToken(TokenTypeVerifyEmail, string(jsonData))
if err := es.InvalidateVerifyEmailTokensForUser(userID); err != nil {
return nil, err
}
if err = es.store.Token().Save(token); err != nil {
return nil, err
}
return token, nil
}
func (es *Service) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.license_up_for_renewal_subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.license_up_for_renewal_title")
data.Props["SubTitle"] = T("api.templates.license_up_for_renewal_subtitle", map[string]any{"UserName": name, "Days": daysToExpiration})
data.Props["SubTitleTwo"] = ctaTitle
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Button"] = ctaText
data.Props["ButtonURL"] = ctaLink
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["SupportEmail"] = "feedback@mattermost.com"
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("license_up_for_renewal", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "LicenseUpForRenewal"); err != nil {
return err
}
return nil
}
func (es *Service) SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error) {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.payment_failed.title")
data.Props["SubTitle1"] = T("api.templates.payment_failed.info1", map[string]any{"CardBrand": failedPayment.CardBrand, "LastFour": failedPayment.LastFour})
data.Props["SubTitle2"] = T("api.templates.payment_failed.info2")
data.Props["FailedReason"] = failedPayment.FailureMessage
data.Props["SubTitle3"] = T("api.templates.payment_failed.info3", map[string]any{"Plan": planName})
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_45.button")
data.Props["IncludeSecondaryActionButton"] = false
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("payment_failed_body", data)
if err != nil {
return false, err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "PaymentFailed"); err != nil {
return false, err
}
return true, nil
}
func (es *Service) SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed_no_card.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.payment_failed_no_card.title")
data.Props["Info1"] = T("api.templates.payment_failed_no_card.info1")
data.Props["Info3"] = T("api.templates.payment_failed_no_card.info3")
data.Props["Button"] = T("api.templates.payment_failed_no_card.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("payment_failed_no_card_body", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "NoCardPaymentFailed"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail7(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_7.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_7.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_7.subtitle2", map[string]any{"Plan": planName})
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_7.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_7_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency7"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail14(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_14.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_14.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_14.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_14.subtitle2")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_14.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_14_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency14"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail30(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_30.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_30.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_30.subtitle1", map[string]any{"Plan": planName})
data.Props["SubTitle2"] = T("api.templates.delinquency_30.subtitle2")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_30.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["BulletListItems"] = []string{T("api.templates.delinquency_30.bullet.message_history"), T("api.templates.delinquency_30.bullet.files")}
data.Props["LimitsDocs"] = T("api.templates.delinquency_30.limits_documentation")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_30_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency30"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_45.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_45.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_45.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
data.Props["SubTitle2"] = T("api.templates.delinquency_45.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_45.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_45.button")
data.Props["IncludeSecondaryActionButton"] = false
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency45"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail60(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_60.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_60.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_60.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_60.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_60.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_60.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_60.downgrade_to_free")
data.Props["Footer"] = T("api.templates.copyright")
// 45 day template is the same as the 60 day one so its reused
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency60"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_75.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_75.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_75.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
data.Props["SubTitle2"] = T("api.templates.delinquency_75.subtitle2", map[string]any{"Plan": planName})
data.Props["SubTitle3"] = T("api.templates.delinquency_75.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_75.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_75.downgrade_to_free")
data.Props["Footer"] = T("api.templates.copyright")
// 45 day template is the same as the 75 day one so its reused
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency75"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_90.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_90.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_90.subtitle1", map[string]any{"SiteURL": siteURL})
data.Props["SubTitle2"] = T("api.templates.delinquency_90.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_90.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_90.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_90.secondary_action_button")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_90_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency90"); err != nil {
return err
}
return nil
}
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (es *Service) SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.remove_expired_license.subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.remove_expired_license.body.title")
data.Props["Link"] = ctaLink
data.Props["LinkButton"] = ctaText
body, err := es.templatesContainer.RenderToString("remove_expired_license", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "RemoveExpiredLicense"); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EmailBatchingTaskName = "Email Batching"
)
type postData struct {
SenderName string
ChannelName string
Message template.HTML
MessageURL string
SenderPhoto string
PostPhoto string
Time string
ShowChannelIcon bool
OtherChannelMembersCount int
MessageAttachments []*EmailMessageAttachment
}
func (es *Service) InitEmailBatching() {
if *es.config().EmailSettings.EnableEmailBatching {
if es.EmailBatching == nil {
es.EmailBatching = NewEmailBatchingJob(es, *es.config().EmailSettings.EmailBatchingBufferSize)
}
// note that we don't support changing EmailBatchingBufferSize without restarting the server
es.EmailBatching.Start()
}
}
func (es *Service) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
if !*es.config().EmailSettings.EnableEmailBatching {
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if !es.EmailBatching.Add(user, post, team) {
mlog.Error("Email batching job's receiving buffer was full. Please increase the EmailBatchingBufferSize. Falling back to sending immediate mail.")
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "", http.StatusInternalServerError)
}
return nil
}
type batchedNotification struct {
userID string
post *model.Post
teamName string
}
type EmailBatchingJob struct {
config func() *model.Config
service *Service
newNotifications chan *batchedNotification
pendingNotifications map[string][]*batchedNotification
task *model.ScheduledTask
taskMutex sync.Mutex
}
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
return &EmailBatchingJob{
config: es.config,
service: es,
newNotifications: make(chan *batchedNotification, bufferSize),
pendingNotifications: make(map[string][]*batchedNotification),
}
}
func (job *EmailBatchingJob) Start() {
mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.config().EmailSettings.EmailBatchingInterval))
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.config().EmailSettings.EmailBatchingInterval)*time.Second)
job.taskMutex.Lock()
oldTask := job.task
job.task = newTask
job.taskMutex.Unlock()
if oldTask != nil {
oldTask.Cancel()
}
}
// Stop will cancel the task properly, flushing out any pending notifications.
// Although this still won't send those notifications which are yet to be sent
// due to a user's PreferenceNameEmailInterval.
func (job *EmailBatchingJob) Stop() {
job.taskMutex.Lock()
if task := job.task; task != nil {
task.Cancel()
}
job.taskMutex.Unlock()
}
func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
notification := &batchedNotification{
userID: user.Id,
post: post,
teamName: team.Name,
}
select {
case job.newNotifications <- notification:
return true
default:
// return false if we couldn't queue the email notification so that we can send an immediate email
return false
}
}
func (job *EmailBatchingJob) CheckPendingEmails() {
job.handleNewNotifications()
// it's a bit weird to pass the send email function through here, but it makes it so that we can test
// without actually sending emails
job.checkPendingNotifications(time.Now(), job.service.sendBatchedEmailNotification)
mlog.Debug("Email batching job ran. Notifications might be still pending.", mlog.Int("number_of_users", len(job.pendingNotifications)))
}
func (job *EmailBatchingJob) handleNewNotifications() {
receiving := true
// read in new notifications to send
for receiving {
select {
case notification := <-job.newNotifications:
userID := notification.userID
if _, ok := job.pendingNotifications[userID]; !ok {
job.pendingNotifications[userID] = []*batchedNotification{notification}
} else {
job.pendingNotifications[userID] = append(job.pendingNotifications[userID], notification)
}
default:
receiving = false
}
}
}
func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) {
for userID, notifications := range job.pendingNotifications {
// Defensive code.
if len(notifications) == 0 {
mlog.Warn("Unexpected result. Got 0 pending notifications for batched email.", mlog.String("user_id", userID))
continue
}
// get how long we need to wait to send notifications to the user
var interval int64
preference, err := job.service.store.Preference().Get(userID, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval)
if err != nil {
// use the default batching interval if an error occurs while fetching user preferences
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
} else {
if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
// // use the default batching interval if an error occurs while deserializing user preferences
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
} else {
interval = value
}
}
batchStartTime := notifications[0].post.CreateAt
// Ignore if it isn't time yet to send.
if now.Sub(time.UnixMilli(batchStartTime)) <= time.Duration(interval)*time.Second {
continue
}
// If the user has viewed any channels in this team since the notification was queued, delete
// all queued notifications
inspectedTeamNames := make(map[string]string)
for _, notification := range notifications {
// at most, we'll do one check for each team that notifications were sent for
if inspectedTeamNames[notification.teamName] != "" {
continue
}
team, nErr := job.service.store.Team().GetByName(notifications[0].teamName)
if nErr != nil {
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
continue
}
if team != nil {
inspectedTeamNames[notification.teamName] = team.Id
}
channelMembers, err := job.service.store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
if err != nil {
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
continue
}
deleted := false
for _, channelMember := range channelMembers {
if channelMember.LastViewedAt >= batchStartTime {
mlog.Debug("Deleted notifications for user", mlog.String("user_id", userID))
delete(job.pendingNotifications, userID)
deleted = true
break
}
}
if deleted {
break
}
}
// The notifications might have been cleared from the above step.
// We need to check again.
if len(job.pendingNotifications[userID]) == 0 {
continue
}
handler(userID, job.pendingNotifications[userID])
delete(job.pendingNotifications, userID)
}
}
/**
* If the name is longer than i characters, replace remaining characters with ...
*/
func truncateUserNames(name string, i int) string {
runes := []rune(name)
if len(runes) > i {
newString := string(runes[:i])
return newString + "..."
}
return name
}
func (es *Service) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
user, err := es.userService.GetUser(userID)
if err != nil {
mlog.Warn("Unable to find recipient for batched email notification")
return
}
translateFunc := i18n.GetUserTranslations(user.Locale)
displayNameFormat := *es.config().TeamSettings.TeammateNameDisplay
siteURL := *es.config().ServiceSettings.SiteURL
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
embeddedFiles := make(map[string]io.Reader)
emailNotificationContentsType := model.EmailNotificationContentsFull
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
}
// check if user has CRT set to ON
appCRT := *es.config().ServiceSettings.CollapsedThreads
threadsEnabled := appCRT == model.CollapsedThreadsAlwaysOn
if !threadsEnabled && appCRT != model.CollapsedThreadsDisabled {
threadsEnabled = appCRT == model.CollapsedThreadsDefaultOn
// check if a participant has overridden collapsed threads settings
if preference, errCrt := es.store.Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); errCrt == nil {
threadsEnabled = preference.Value == "on"
}
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
for i, notification := range notifications {
sender, errSender := es.userService.GetUser(notification.post.UserId)
if errSender != nil {
mlog.Warn("Unable to find sender of post for batched email notification")
}
channel, errCh := es.store.Channel().Get(notification.post.ChannelId, true)
if errCh != nil {
mlog.Warn("Unable to find channel of post for batched email notification")
}
senderProfileImage, _, errProfileImage := es.userService.GetProfileImage(sender)
if errProfileImage != nil {
mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage))
}
senderPhoto := fmt.Sprintf("user-avatar-%d.png", i)
if senderProfileImage != nil {
embeddedFiles[senderPhoto] = bytes.NewReader(senderProfileImage)
}
tm := time.Unix(notification.post.CreateAt/1000, 0)
timezone, _ := tm.Zone()
t := translateFunc("api.email_batching.send_batched_email_notification.time", map[string]any{
"Hour": tm.Hour(),
"Minute": fmt.Sprintf("%02d", tm.Minute()),
"Month": translateFunc(tm.Month().String()),
"Day": tm.Day(),
"Year": tm.Year(),
"TimeZone": timezone,
})
MessageURL := siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
channelDisplayName := channel.DisplayName
showChannelIcon := true
otherChannelMembersCount := 0
if threadsEnabled && notification.post.RootId != "" {
props := map[string]any{"channelName": channelDisplayName}
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads", props)
if channel.Type == model.ChannelTypeDirect {
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads_dm")
}
}
if channel.Type == model.ChannelTypeGroup {
otherChannelMembersCount = len(strings.Split(channelDisplayName, ",")) - 1
showChannelIcon = false
channelDisplayName = truncateUserNames(channel.DisplayName, 11)
}
postsData = append(postsData, &postData{
SenderPhoto: senderPhoto,
SenderName: truncateUserNames(sender.GetDisplayName(displayNameFormat), 22),
Time: t,
ChannelName: channelDisplayName,
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
MessageURL: MessageURL,
ShowChannelIcon: showChannelIcon,
OtherChannelMembersCount: otherChannelMembersCount,
MessageAttachments: ProcessMessageAttachments(notification.post, siteURL),
})
}
}
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]any{
"SiteName": es.config().TeamSettings.SiteName,
"Year": tm.Year(),
"Month": translateFunc(tm.Month().String()),
"Day": tm.Day(),
})
data := es.NewEmailTemplateData(user.Locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1)
data.Props["SubTitle"] = translateFunc("api.email_batching.send_batched_email_notification.subTitle")
data.Props["Button"] = translateFunc("api.email_batching.send_batched_email_notification.button")
data.Props["ButtonURL"] = siteURL
data.Props["Posts"] = postsData
data.Props["MessageButton"] = translateFunc("api.email_batching.send_batched_email_notification.messageButton")
data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title")
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
renderedPage, renderErr := es.templatesContainer.RenderToString("messages_notification", data)
if renderErr != nil {
mlog.Error("Unable to render email", mlog.Err(renderErr))
}
if nErr := es.SendMailWithEmbeddedFiles(user.Email, subject, renderedPage, embeddedFiles, "", "", "", "BatchedEmailNotification"); nErr != nil {
mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"html"
"html/template"
"net/url"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type FieldRow struct {
Cells []*model.SlackAttachmentField
}
type EmailMessageAttachment struct {
model.SlackAttachment
Pretext template.HTML
Text template.HTML
FieldRows []FieldRow
}
func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
return post.Message
}
// extract the filenames from their paths and determine what type of files are attached
infos, err := es.store.FileInfo().GetForPost(post.Id, true, false, true)
if err != nil {
mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err))
}
filenames := make([]string, len(infos))
onlyImages := true
for i, info := range infos {
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
// this should never error since filepath was escaped using url.QueryEscape
filenames[i] = escaped
} else {
filenames[i] = info.Name
}
onlyImages = onlyImages && info.IsImage()
}
props := map[string]any{"Filenames": strings.Join(filenames, ", ")}
if onlyImages {
return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
}
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
}
func ProcessMessageAttachments(post *model.Post, siteURL string) []*EmailMessageAttachment {
emailMessageAttachments := []*EmailMessageAttachment{}
for _, messageAttachment := range post.Attachments() {
emailMessageAttachment := &EmailMessageAttachment{
SlackAttachment: *messageAttachment,
Pretext: prepareTextForEmail(messageAttachment.Pretext, siteURL),
Text: prepareTextForEmail(messageAttachment.Text, siteURL),
}
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
if err != nil {
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
stripedTitle = ""
}
emailMessageAttachment.Title = stripedTitle
shortFieldRow := FieldRow{}
for i := range messageAttachment.Fields {
// Create a new instance to avoid altering the original pointer reference
// We update field value to parse markdown.
// If we do that on the original pointer, the rendered text in mattermost
// becomes invalid as its no longer a markdown string, but rather an HTML string.
field := &model.SlackAttachmentField{
Title: messageAttachment.Fields[i].Title,
Value: messageAttachment.Fields[i].Value,
Short: messageAttachment.Fields[i].Short,
}
if stringValue, ok := field.Value.(string); ok {
field.Value = prepareTextForEmail(stringValue, siteURL)
}
if !field.Short {
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
} else {
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
if len(shortFieldRow.Cells) == 2 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
}
}
// collect any leftover short fields
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
}
return emailMessageAttachments
}
func prepareTextForEmail(text, siteURL string) template.HTML {
escapedText := html.EscapeString(text)
markdownText, err := utils.MarkdownToHTML(escapedText, siteURL)
if err != nil {
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
return template.HTML(text)
}
return template.HTML(markdownText)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"io"
"net/url"
"path"
"github.com/pkg/errors"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store/memstore"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
const (
emailRateLimitingMemstoreSize = 65536
emailRateLimitingPerHour = 20
emailRateLimitingMaxBurst = 20
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
)
func condenseSiteURL(siteURL string) string {
parsedSiteURL, _ := url.Parse(siteURL)
if parsedSiteURL.Path == "" || parsedSiteURL.Path == "/" {
return parsedSiteURL.Host
}
return path.Join(parsedSiteURL.Host, parsedSiteURL.Path)
}
type Service struct {
config func() *model.Config
license func() *model.License
userService *users.UserService
store store.Store
templatesContainer *templates.Container
perHourEmailRateLimiter *throttled.GCRARateLimiter
perDayEmailRateLimiter *throttled.GCRARateLimiter
EmailBatching *EmailBatchingJob
}
type ServiceConfig struct {
ConfigFn func() *model.Config
LicenseFn func() *model.License
TemplatesContainer *templates.Container
UserService *users.UserService
Store store.Store
}
func NewService(config ServiceConfig) (*Service, error) {
if err := config.validate(); err != nil {
return nil, err
}
service := &Service{
config: config.ConfigFn,
templatesContainer: config.TemplatesContainer,
license: config.LicenseFn,
store: config.Store,
userService: config.UserService,
}
if err := service.setUpRateLimiters(); err != nil {
return nil, err
}
service.InitEmailBatching()
return service, nil
}
func (es *Service) Stop() {
mlog.Info("Shutting down Email batching service...")
if es.EmailBatching != nil {
es.EmailBatching.Stop()
}
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.Store == nil || c.LicenseFn == nil || c.TemplatesContainer == nil {
return errors.New("invalid service config")
}
return nil
}
func (es *Service) setUpRateLimiters() error {
store, err := memstore.New(emailRateLimitingMemstoreSize)
if err != nil {
return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
}
perHourQuota := throttled.RateQuota{
MaxRate: throttled.PerHour(emailRateLimitingPerHour),
MaxBurst: emailRateLimitingMaxBurst,
}
perDayQuota := throttled.RateQuota{
MaxRate: throttled.PerDay(1),
MaxBurst: 0,
}
perHourRateLimiter, err := throttled.NewGCRARateLimiter(store, perHourQuota)
if err != nil || perHourRateLimiter == nil {
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
}
perDayRateLimiter, err := throttled.NewGCRARateLimiter(store, perDayQuota)
if err != nil || perDayRateLimiter == nil {
return errors.Wrap(err, "Unable to setup per day email rate limiting GCRA rate limiter.")
}
es.perHourEmailRateLimiter = perHourRateLimiter
es.perDayEmailRateLimiter = perDayRateLimiter
return nil
}
type ServiceInterface interface {
GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter
NewEmailTemplateData(locale string) templates.Data
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
SendSignInChangeEmail(email, method, locale, siteURL string) error
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
SendPasswordChangeEmail(email, method, locale, siteURL string) error
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error)
SendDeactivateAccountEmail(email string, locale, siteURL string) error
SendNotificationMail(to, subject, htmlBody string) error
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error)
// Cloud delinquency email sequence
SendDelinquencyEmail7(email, locale, siteURL, planName string) error
SendDelinquencyEmail14(email, locale, siteURL, planName string) error
SendDelinquencyEmail30(email, locale, siteURL, planName string) error
SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error
SendDelinquencyEmail60(email, locale, siteURL string) error
SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
SendDelinquencyEmail90(email, locale, siteURL string) error
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
InitEmailBatching()
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
Stop()
}
func (es *Service) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perDayEmailRateLimiter
}
func (es *Service) GetPerHourEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perHourEmailRateLimiter
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
)
func (es *Service) mailServiceConfig(replyToAddress string) *mail.SMTPConfig {
emailSettings := es.config().EmailSettings
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
if replyToAddress == "" {
replyToAddress = *emailSettings.ReplyToAddress
}
cfg := mail.SMTPConfig{
Hostname: hostname,
ConnectionSecurity: *emailSettings.ConnectionSecurity,
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
ServerName: *emailSettings.SMTPServer,
Server: *emailSettings.SMTPServer,
Port: *emailSettings.SMTPPort,
ServerTimeout: *emailSettings.SMTPServerTimeout,
Username: *emailSettings.SMTPUsername,
Password: *emailSettings.SMTPPassword,
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
SendEmailNotifications: *emailSettings.SendEmailNotifications,
FeedbackName: *emailSettings.FeedbackName,
FeedbackEmail: *emailSettings.FeedbackEmail,
ReplyToAddress: replyToAddress,
}
return &cfg
}
func (es *Service) GetTrackFlowStartedByRole(isFirstAdmin bool, isSystemAdmin bool) string {
trackFlowStartedByRole := "su"
if isFirstAdmin {
trackFlowStartedByRole = "fa"
} else if isSystemAdmin {
trackFlowStartedByRole = "sa"
}
return trackFlowStartedByRole
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"image"
"image/color/palette"
"image/draw"
"image/gif"
_ "image/jpeg"
"io"
"mime/multipart"
"net/http"
"path"
"github.com/disintegration/imaging"
_ "golang.org/x/image/webp"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxEmojiFileSize = 1 << 19 // 512 KiB
MaxEmojiWidth = 128
MaxEmojiHeight = 128
MaxEmojiOriginalWidth = 1028
MaxEmojiOriginalHeight = 1028
)
func (a *App) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
// wipe the emoji id so that existing emojis can't get overwritten
emoji.Id = ""
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
// orphaned files left over when validation fails later on
emoji.PreSave()
if appErr := emoji.IsValid(); appErr != nil {
return nil, appErr
}
if emoji.CreatorId != sessionUserId {
return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
}
if existingEmoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emoji.Name, true); err == nil && existingEmoji != nil {
return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
imageData := multiPartImageData.File["image"]
if len(imageData) == 0 {
return nil, model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": "createEmoji"}, "", http.StatusBadRequest)
}
if appErr := a.UploadEmojiImage(c, emoji.Id, imageData[0]); appErr != nil {
return nil, appErr
}
emoji, err := a.Srv().Store().Emoji().Save(emoji)
if err != nil {
return nil, model.NewAppError("CreateEmoji", "app.emoji.create.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventEmojiAdded, "", "", "", nil, "")
emojiJSON, jsonErr := json.Marshal(emoji)
if jsonErr != nil {
return nil, model.NewAppError("CreateEmoji", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("emoji", string(emojiJSON))
a.Publish(message)
return emoji, nil
}
func (a *App) GetEmojiList(c request.CTX, page, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
list, err := a.Srv().Store().Emoji().GetList(page*perPage, perPage, sort)
if err != nil {
return nil, model.NewAppError("GetEmojiList", "app.emoji.get_list.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("UploadEmojiImage", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
file, err := imageData.Open()
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
// make sure the file is an image and is within the required dimensions
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if config.Width > MaxEmojiOriginalWidth || config.Height > MaxEmojiOriginalHeight {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.too_large.app_error", map[string]any{
"MaxWidth": MaxEmojiOriginalWidth,
"MaxHeight": MaxEmojiOriginalHeight,
}, "", http.StatusBadRequest)
}
if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
data := buf.Bytes()
newbuf := bytes.NewBuffer(nil)
info, err := model.GetInfoForBytes(imageData.Filename, bytes.NewReader(data), len(data))
if err != nil {
return err
}
if info.MimeType == "image/gif" {
gif_data, err := gif.DecodeAll(bytes.NewReader(data))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resized_gif := resizeEmojiGif(gif_data)
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf = newbuf
} else {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resizedImg := resizeEmoji(img, config.Width, config.Height)
if err := a.ch.imgEncoder.EncodePNG(newbuf, resizedImg); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf = newbuf
}
}
_, appErr := a.WriteFile(buf, getEmojiImagePath(id))
return appErr
}
func (a *App) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
if err := a.Srv().Store().Emoji().Delete(emoji, model.GetMillis()); err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("DeleteEmoji", "app.emoji.delete.no_results", nil, "id="+emoji.Id, http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("DeleteEmoji", "app.emoji.delete.app_error", nil, "id="+emoji.Id, http.StatusInternalServerError).Wrap(err)
}
}
a.deleteEmojiImage(emoji.Id)
a.deleteReactionsForEmoji(emoji.Name)
return nil
}
func (a *App) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return emoji, nil
}
func (a *App) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmojiByName", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return emoji, nil
}
func (a *App) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetMultipleEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().GetMultipleByName(names)
if err != nil {
return nil, model.NewAppError("GetMultipleEmojiByName", "app.emoji.get_by_name.app_error", nil, fmt.Sprintf("names=%v, %v", names, err.Error()), http.StatusInternalServerError)
}
return emoji, nil
}
func (a *App) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) {
_, storeErr := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
if storeErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(storeErr, &nfErr):
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(storeErr)
default:
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
img, appErr := a.ReadFile(getEmojiImagePath(emojiId))
if appErr != nil {
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
}
_, imageType, err := image.DecodeConfig(bytes.NewReader(img))
if err != nil {
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return img, imageType, nil
}
func (a *App) SearchEmoji(c request.CTX, name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
list, err := a.Srv().Store().Emoji().Search(name, prefixOnly, limit)
if err != nil {
return nil, model.NewAppError("SearchEmoji", "app.emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error(), http.StatusInternalServerError)
}
return list, nil
}
// GetEmojiStaticURL returns a relative static URL for system default emojis,
// and the API route for custom ones. Errors if not found or if custom and deleted.
func (a *App) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model.AppError) {
subPath, _ := utils.GetSubpathFromConfig(a.Config())
if id, found := model.GetSystemEmojiId(emojiName); found {
return path.Join(subPath, "/static/emoji", id+".png"), nil
}
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
if err == nil {
return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
}
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
// Create a new RGBA image to hold the incremental frames.
firstFrame := gifImg.Image[0].Bounds()
b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
img := image.NewRGBA(b)
resizedImage := image.Image(nil)
// Resize each frame.
for index, frame := range gifImg.Image {
bounds := frame.Bounds()
draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
gifImg.Image[index] = imageToPaletted(resizedImage)
}
// Set new gif width and height
gifImg.Config.Width = resizedImage.Bounds().Dx()
gifImg.Config.Height = resizedImage.Bounds().Dy()
return gifImg
}
func getEmojiImagePath(id string) string {
return "emoji/" + id + "/image"
}
func resizeEmoji(img image.Image, width int, height int) image.Image {
emojiWidth := float64(width)
emojiHeight := float64(height)
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
return img
}
return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
}
func imageToPaletted(img image.Image) *image.Paletted {
b := img.Bounds()
pm := image.NewPaletted(b, palette.Plan9)
draw.FloydSteinberg.Draw(pm, b, img, image.Point{})
return pm
}
func (a *App) deleteEmojiImage(id string) {
if err := a.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
mlog.Warn("Failed to rename image when deleting emoji", mlog.String("emoji_id", id))
}
}
func (a *App) deleteReactionsForEmoji(emojiName string) {
if err := a.Srv().Store().Reaction().DeleteAllWithEmojiName(emojiName); err != nil {
mlog.Warn("Unable to delete reactions when deleting emoji", mlog.String("emoji_name", emojiName), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
ejobs "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/jobs"
)
var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface
func RegisterAccountMigrationInterface(f func(*App) einterfaces.AccountMigrationInterface) {
accountMigrationInterface = f
}
var complianceInterface func(*App) einterfaces.ComplianceInterface
func RegisterComplianceInterface(f func(*App) einterfaces.ComplianceInterface) {
complianceInterface = f
}
var dataRetentionInterface func(*App) einterfaces.DataRetentionInterface
func RegisterDataRetentionInterface(f func(*App) einterfaces.DataRetentionInterface) {
dataRetentionInterface = f
}
var jobsDataRetentionJobInterface func(*Server) ejobs.DataRetentionJobInterface
func RegisterJobsDataRetentionJobInterface(f func(*Server) ejobs.DataRetentionJobInterface) {
jobsDataRetentionJobInterface = f
}
var jobsMessageExportJobInterface func(*Server) ejobs.MessageExportJobInterface
func RegisterJobsMessageExportJobInterface(f func(*Server) ejobs.MessageExportJobInterface) {
jobsMessageExportJobInterface = f
}
var jobsElasticsearchAggregatorInterface func(*Server) ejobs.ElasticsearchAggregatorInterface
func RegisterJobsElasticsearchAggregatorInterface(f func(*Server) ejobs.ElasticsearchAggregatorInterface) {
jobsElasticsearchAggregatorInterface = f
}
var jobsElasticsearchIndexerInterface func(*Server) ejobs.IndexerJobInterface
func RegisterJobsElasticsearchIndexerInterface(f func(*Server) ejobs.IndexerJobInterface) {
jobsElasticsearchIndexerInterface = f
}
var jobsLdapSyncInterface func(*App) ejobs.LdapSyncInterface
func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
jobsLdapSyncInterface = f
}
var ldapInterface func(*App) einterfaces.LdapInterface
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
ldapInterface = f
}
var messageExportInterface func(*App) einterfaces.MessageExportInterface
func RegisterMessageExportInterface(f func(*App) einterfaces.MessageExportInterface) {
messageExportInterface = f
}
var cloudInterface func(*Server) einterfaces.CloudInterface
func RegisterCloudInterface(f func(*Server) einterfaces.CloudInterface) {
cloudInterface = f
}
var samlInterfaceNew func(*App) einterfaces.SamlInterface
func RegisterNewSamlInterface(f func(*App) einterfaces.SamlInterface) {
samlInterfaceNew = f
}
var notificationInterface func(*App) einterfaces.NotificationInterface
func RegisterNotificationInterface(f func(*App) einterfaces.NotificationInterface) {
notificationInterface = f
}
func (s *Server) initEnterprise() {
if cloudInterface != nil {
s.Cloud = cloudInterface(s)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
OneHourMillis = 60 * 60 * 1000
)
// NotifySessionsExpired is called periodically from the job server to notify any mobile sessions that have expired.
func (a *App) NotifySessionsExpired() error {
if !a.canSendPushNotifications() {
return nil
}
// Get all mobile sessions that expired within the last hour.
sessions, err := a.ch.srv.Store().Session().GetSessionsExpired(OneHourMillis, true, true)
if err != nil {
return model.NewAppError("NotifySessionsExpired", "app.session.analytics_session_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Version: model.PushMessageV2,
Type: model.PushTypeSession,
}
for _, session := range sessions {
tmpMessage := msg.DeepCopy()
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
tmpMessage.AckId = model.NewId()
tmpMessage.Message = a.getSessionExpiredPushMessage(session)
errPush := a.sendToPushProxy(tmpMessage, session)
if errPush != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", errPush.Error()),
)
continue
}
a.NotificationsLog().Info("Notification sent",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", model.PushSendSuccess),
)
if a.Metrics() != nil {
a.Metrics().IncrementPostSentPush()
}
err = a.ch.srv.Store().Session().UpdateExpiredNotify(session.Id, true)
if err != nil {
mlog.Error("Failed to update ExpiredNotify flag", mlog.String("sessionid", session.Id), mlog.Err(err))
}
}
return nil
}
func (a *App) getSessionExpiredPushMessage(session *model.Session) string {
locale := model.DefaultLocale
user, err := a.GetUser(session.UserId)
if err == nil {
locale = user.Locale
}
T := i18n.GetUserTranslations(locale)
siteName := *a.Config().TeamSettings.SiteName
props := map[string]any{"siteName": siteName, "hoursCount": *a.Config().ServiceSettings.SessionLengthMobileInHours}
return T("api.push_notifications.session.expired", props)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// We use this map to identify the exportable preferences.
// Here we link the preference category and name, to the name of the relevant field in the import struct.
var exportablePreferences = map[imports.ComparablePreference]string{{
Category: model.PreferenceCategoryTheme,
Name: "",
}: "Theme", {
Category: model.PreferenceCategoryAdvancedSettings,
Name: "feature_enabled_markdown_preview",
}: "UseMarkdownPreview", {
Category: model.PreferenceCategoryAdvancedSettings,
Name: "formatting",
}: "UseFormatting", {
Category: model.PreferenceCategorySidebarSettings,
Name: "show_unread_section",
}: "ShowUnreadSection", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
}: "UseMilitaryTime", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
}: "CollapsePreviews", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
}: "MessageDisplay", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
}: "CollapseConsecutive", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "collapse_consecutive_messages",
}: "ColorizeUsernames", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "colorize_usernames",
}: "ChannelDisplayMode", {
Category: model.PreferenceCategoryTutorialSteps,
Name: "",
}: "TutorialStep", {
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
}: "EmailInterval",
}
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
var zipWr *zip.Writer
if opts.CreateArchive {
var err error
zipWr = zip.NewWriter(writer)
defer zipWr.Close()
writer, err = zipWr.Create("import.jsonl")
if err != nil {
return model.NewAppError("BulkExport", "app.export.zip_create.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
}
if job != nil && job.Data == nil {
job.Data = make(model.StringMap)
}
ctx.Logger().Info("Bulk export: exporting version")
if err := a.exportVersion(writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting teams")
teamNames, err := a.exportAllTeams(ctx, job, writer)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting channels")
if err = a.exportAllChannels(ctx, job, writer, teamNames); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting users")
if err = a.exportAllUsers(ctx, job, writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting posts")
attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting emoji")
emojiPaths, err := a.exportCustomEmoji(ctx, job, writer, outPath, "exported_emoji", !opts.CreateArchive)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting direct channels")
if err = a.exportAllDirectChannels(ctx, job, writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting direct posts")
directAttachments, err := a.exportAllDirectPosts(ctx, job, writer, opts.IncludeAttachments)
if err != nil {
return err
}
if opts.IncludeAttachments {
ctx.Logger().Info("Bulk export: exporting file attachments")
for _, attachment := range attachments {
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
return err
}
}
for _, attachment := range directAttachments {
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
return err
}
}
for _, emojiPath := range emojiPaths {
if err := a.exportFile(outPath, emojiPath, zipWr); err != nil {
return err
}
}
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "attachments_exported", len(attachments)+len(directAttachments)+len(emojiPaths))
}
return nil
}
func (a *App) exportWriteLine(w io.Writer, line *imports.LineImportData) *model.AppError {
b, err := json.Marshal(line)
if err != nil {
return model.NewAppError("BulkExport", "app.export.export_write_line.json_marshall.error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err := w.Write(append(b, '\n')); err != nil {
return model.NewAppError("BulkExport", "app.export.export_write_line.io_writer.error", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
func (a *App) exportVersion(writer io.Writer) *model.AppError {
version := 1
info := &imports.VersionInfoImportData{
Generator: "mattermost-server",
Version: fmt.Sprintf("%s (%s, enterprise: %s)", model.CurrentVersion, model.BuildHash, model.BuildEnterpriseReady),
Created: time.Now().Format(time.RFC3339Nano),
}
versionLine := &imports.LineImportData{
Type: "version",
Version: &version,
Info: info,
}
return a.exportWriteLine(writer, versionLine)
}
func (a *App) exportAllTeams(ctx request.CTX, job *model.Job, writer io.Writer) (map[string]bool, *model.AppError) {
afterId := strings.Repeat("0", 26)
teamNames := make(map[string]bool)
cnt := 0
for {
teams, err := a.Srv().Store().Team().GetAllForExportAfter(1000, afterId)
if err != nil {
return nil, model.NewAppError("exportAllTeams", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(teams) == 0 {
break
}
cnt += len(teams)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "teams_exported", cnt)
for _, team := range teams {
afterId = team.Id
// Skip deleted.
if team.DeleteAt != 0 {
continue
}
teamNames[team.Name] = true
teamLine := ImportLineFromTeam(team)
if err := a.exportWriteLine(writer, teamLine); err != nil {
return nil, err
}
}
}
return teamNames, nil
}
func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
channels, err := a.Srv().Store().Channel().GetAllChannelsForExportAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllChannels", "app.channel.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(channels) == 0 {
break
}
cnt += len(channels)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "channels_exported", cnt)
for _, channel := range channels {
afterId = channel.Id
// Skip deleted.
if channel.DeleteAt != 0 {
continue
}
// Skip channels on deleted teams.
if ok := teamNames[channel.TeamName]; !ok {
continue
}
channelLine := ImportLineFromChannel(channel)
if err := a.exportWriteLine(writer, channelLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
users, err := a.Srv().Store().User().GetAllAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) == 0 {
break
}
cnt += len(users)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "users_exported", cnt)
for _, user := range users {
afterId = user.Id
// Gathering here the exportable preferences to pass them on to ImportLineFromUser
exportedPrefs := make(map[string]*string)
allPrefs, err := a.GetPreferencesForUser(user.Id)
if err != nil {
return err
}
for _, pref := range allPrefs {
// We need to manage the special cases
// Here we manage Tutorial steps
if pref.Category == model.PreferenceCategoryTutorialSteps {
pref.Name = ""
// Then the email interval
} else if pref.Category == model.PreferenceCategoryNotifications && pref.Name == model.PreferenceNameEmailInterval {
switch pref.Value {
case model.PreferenceEmailIntervalNoBatchingSeconds:
pref.Value = model.PreferenceEmailIntervalImmediately
case model.PreferenceEmailIntervalFifteenAsSeconds:
pref.Value = model.PreferenceEmailIntervalFifteen
case model.PreferenceEmailIntervalHourAsSeconds:
pref.Value = model.PreferenceEmailIntervalHour
case "0":
pref.Value = ""
}
}
id, ok := exportablePreferences[imports.ComparablePreference{
Category: pref.Category,
Name: pref.Name,
}]
if ok {
prefPtr := pref.Value
if prefPtr != "" {
exportedPrefs[id] = &prefPtr
} else {
exportedPrefs[id] = nil
}
}
}
userLine := ImportLineFromUser(user, exportedPrefs)
userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps)
// Do the Team Memberships.
members, err := a.buildUserTeamAndChannelMemberships(user.Id)
if err != nil {
return err
}
userLine.User.Teams = members
if err := a.exportWriteLine(writer, userLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.UserTeamImportData, *model.AppError) {
var memberships []imports.UserTeamImportData
members, err := a.Srv().Store().Team().GetTeamMembersForExport(userID)
if err != nil {
return nil, model.NewAppError("buildUserTeamAndChannelMemberships", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, member := range members {
// Skip deleted.
if member.DeleteAt != 0 {
continue
}
memberData := ImportUserTeamDataFromTeamMember(member)
// Do the Channel Memberships.
channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId)
if err != nil {
return nil, err
}
// Get the user theme
themePreference, nErr := a.Srv().Store().Preference().Get(member.UserId, model.PreferenceCategoryTheme, member.TeamId)
if nErr == nil {
memberData.Theme = &themePreference.Value
}
memberData.Channels = channelMembers
memberships = append(memberships, *memberData)
}
return &memberships, nil
}
func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]imports.UserChannelImportData, *model.AppError) {
members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID)
if nErr != nil {
return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
category := model.PreferenceCategoryFavoriteChannel
preferences, err := a.GetPreferenceByCategoryForUser(userID, category)
if err != nil && err.StatusCode != http.StatusNotFound {
return nil, err
}
memberships := make([]imports.UserChannelImportData, len(members))
for i, member := range members {
memberships[i] = *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences)
}
return &memberships, nil
}
func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNotifyPropsImportData {
getProp := func(key string) *string {
if v, ok := notifyProps[key]; ok {
return &v
}
return nil
}
return &imports.UserNotifyPropsImportData{
Desktop: getProp(model.DesktopNotifyProp),
DesktopSound: getProp(model.DesktopSoundNotifyProp),
Email: getProp(model.EmailNotifyProp),
Mobile: getProp(model.PushNotifyProp),
MobilePushStatus: getProp(model.PushStatusNotifyProp),
ChannelTrigger: getProp(model.ChannelMentionsNotifyProp),
CommentsTrigger: getProp(model.CommentsNotifyProp),
MentionKeys: getProp(model.MentionKeysNotifyProp),
}
}
func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
var attachments []imports.AttachmentImportData
afterId := strings.Repeat("0", 26)
var postProcessCount uint64
logCheckpoint := time.Now()
cnt := 0
for {
if time.Since(logCheckpoint) > 5*time.Minute {
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount))
logCheckpoint = time.Now()
}
posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId)
if nErr != nil {
return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if len(posts) == 0 {
return attachments, nil
}
cnt += len(posts)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "posts_exported", cnt)
for _, post := range posts {
afterId = post.Id
postProcessCount++
// Skip deleted.
if post.DeleteAt != 0 {
continue
}
postLine := ImportLineForPost(post)
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
if err != nil {
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine.Post.Replies = &replies
postLine.Post.Reactions = &[]imports.ReactionImportData{}
if post.HasReactions {
postLine.Post.Reactions, err = a.BuildPostReactions(ctx, post.Id)
if err != nil {
return nil, err
}
}
if len(post.FileIds) > 0 {
postAttachments, err := a.buildPostAttachments(post.Id)
if err != nil {
return nil, err
}
postLine.Post.Attachments = &postAttachments
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return nil, err
}
}
}
}
func (a *App) buildPostReplies(ctx request.CTX, postID string, withAttachments bool) ([]imports.ReplyImportData, []imports.AttachmentImportData, *model.AppError) {
var replies []imports.ReplyImportData
var attachments []imports.AttachmentImportData
replyPosts, nErr := a.Srv().Store().Post().GetRepliesForExport(postID)
if nErr != nil {
return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, reply := range replyPosts {
replyImportObject := ImportReplyFromPost(reply)
if reply.HasReactions {
var appErr *model.AppError
replyImportObject.Reactions, appErr = a.BuildPostReactions(ctx, reply.Id)
if appErr != nil {
return nil, nil, appErr
}
}
if len(reply.FileIds) > 0 {
postAttachments, appErr := a.buildPostAttachments(reply.Id)
if appErr != nil {
return nil, nil, appErr
}
replyImportObject.Attachments = &postAttachments
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
replies = append(replies, *replyImportObject)
}
return replies, attachments, nil
}
func (a *App) BuildPostReactions(ctx request.CTX, postID string) (*[]ReactionImportData, *model.AppError) {
var reactionsOfPost []imports.ReactionImportData
reactions, nErr := a.Srv().Store().Reaction().GetForPost(postID, true)
if nErr != nil {
return nil, model.NewAppError("BuildPostReactions", "app.reaction.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, reaction := range reactions {
user, err := a.Srv().Store().User().Get(context.Background(), reaction.UserId)
if err != nil {
var nfErr *store.ErrNotFound
if errors.As(err, &nfErr) { // this is a valid case, the user that reacted might've been deleted by now
ctx.Logger().Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId))
continue
}
return nil, model.NewAppError("BuildPostReactions", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction))
}
return &reactionsOfPost, nil
}
func (a *App) buildPostAttachments(postID string) ([]imports.AttachmentImportData, *model.AppError) {
infos, nErr := a.Srv().Store().FileInfo().GetForPost(postID, false, false, false)
if nErr != nil {
return nil, model.NewAppError("buildPostAttachments", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
attachments := make([]imports.AttachmentImportData, 0, len(infos))
for _, info := range infos {
attachments = append(attachments, imports.AttachmentImportData{Path: &info.Path})
}
return attachments, nil
}
func (a *App) exportCustomEmoji(c request.CTX, job *model.Job, writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) {
var emojiPaths []string
pageNumber := 0
cnt := 0
for {
customEmojiList, err := a.GetEmojiList(c, pageNumber, 100, model.EmojiSortByName)
if err != nil {
return nil, err
}
if len(customEmojiList) == 0 {
break
}
cnt += len(customEmojiList)
updateJobProgress(c.Logger(), a.Srv().Store(), job, "emojis_exported", cnt)
pageNumber++
emojiPath := filepath.Join(*a.Config().FileSettings.Directory, "emoji")
pathToDir := filepath.Join(outPath, exportDir)
if exportFiles {
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
os.Mkdir(pathToDir, os.ModePerm)
}
}
for _, emoji := range customEmojiList {
emojiImagePath := filepath.Join(emojiPath, emoji.Id, "image")
filePath := filepath.Join(exportDir, emoji.Id, "image")
if exportFiles {
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
if err != nil {
return nil, model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
}
} else {
filePath = filepath.Join("emoji", emoji.Id, "image")
emojiPaths = append(emojiPaths, filePath)
}
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
return nil, err
}
}
}
return emojiPaths, nil
}
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error {
fromPath, err := os.Open(emojiImagePath)
if fromPath == nil || err != nil {
return errors.New("Error reading " + emojiImagePath + "file")
}
defer fromPath.Close()
emojiDir := pathToDir + "/" + emojiId
if _, err = os.Stat(emojiDir); err != nil {
if !os.IsNotExist(err) {
return errors.Wrapf(err, "Error fetching file info of emoji directory %v", emojiDir)
}
if err = os.Mkdir(emojiDir, os.ModePerm); err != nil {
return errors.Wrapf(err, "Error creating emoji directory %v", emojiDir)
}
}
toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return errors.New("Error creating the image file " + err.Error())
}
defer toPath.Close()
_, err = io.Copy(toPath, fromPath)
if err != nil {
return errors.New("Error copying emojis " + err.Error())
}
return nil
}
func (a *App) exportAllDirectChannels(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
channels, err := a.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllDirectChannels", "app.channel.get_all_direct.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(channels) == 0 {
break
}
cnt += len(channels)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_channels_exported", cnt)
for _, channel := range channels {
afterId = channel.Id
// Skip if there are no active members in the channel
if len(*channel.Members) == 0 {
continue
}
// Skip deleted.
if channel.DeleteAt != 0 {
continue
}
favoritedBy, err := a.buildFavoritedByList(channel.Id)
if err != nil {
return err
}
channelLine := ImportLineFromDirectChannel(channel, favoritedBy)
if err := a.exportWriteLine(writer, channelLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) {
prefs, err := a.Srv().Store().Preference().GetCategoryAndName(model.PreferenceCategoryFavoriteChannel, channelID)
if err != nil {
return nil, model.NewAppError("buildFavoritedByList", "app.preference.get_category.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userIDs := make([]string, 0, len(prefs))
for _, pref := range prefs {
if pref.Value != "true" {
continue
}
user, err := a.Srv().Store().User().Get(context.Background(), pref.UserId)
if err != nil {
return nil, model.NewAppError("buildFavoritedByList", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userIDs = append(userIDs, user.Username)
}
return userIDs, nil
}
func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
var attachments []imports.AttachmentImportData
afterId := strings.Repeat("0", 26)
var postProcessCount uint64
logCheckpoint := time.Now()
cnt := 0
for {
if time.Since(logCheckpoint) > 5*time.Minute {
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount))
logCheckpoint = time.Now()
}
posts, err := a.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, afterId)
if err != nil {
return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(posts) == 0 {
break
}
cnt += len(posts)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_posts_exported", cnt)
for _, post := range posts {
afterId = post.Id
postProcessCount++
// Skip deleted.
if post.DeleteAt != 0 {
continue
}
// Handle attachments.
var postAttachments []imports.AttachmentImportData
var err *model.AppError
if len(post.FileIds) > 0 {
postAttachments, err = a.buildPostAttachments(post.Id)
if err != nil {
return nil, err
}
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
// Do the Replies.
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
if err != nil {
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine := ImportLineForDirectPost(post)
postLine.DirectPost.Replies = &replies
if len(postAttachments) > 0 {
postLine.DirectPost.Attachments = &postAttachments
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return nil, err
}
}
}
return attachments, nil
}
func (a *App) exportFile(outPath, filePath string, zipWr *zip.Writer) *model.AppError {
var wr io.Writer
var err error
rd, appErr := a.FileReader(filePath)
if appErr != nil {
return appErr
}
defer rd.Close()
if zipWr != nil {
wr, err = zipWr.CreateHeader(&zip.FileHeader{
Name: filepath.Join(model.ExportDataDir, filePath),
Method: zip.Store,
})
if err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.zip_create_header.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
} else {
filePath = filepath.Join(outPath, model.ExportDataDir, filePath)
if err = os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.mkdirall.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
wr, err = os.Create(filePath)
if err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.create_file.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
defer wr.(*os.File).Close()
}
if _, err := io.Copy(wr, rd); err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.copy_file.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) ListExports() ([]string, *model.AppError) {
exports, appErr := a.ListDirectory(*a.Config().ExportSettings.Directory)
if appErr != nil {
return nil, appErr
}
results := make([]string, len(exports))
for i := range exports {
results[i] = filepath.Base(exports[i])
}
return results, nil
}
func (a *App) DeleteExport(name string) *model.AppError {
filePath := filepath.Join(*a.Config().ExportSettings.Directory, name)
if ok, err := a.FileExists(filePath); err != nil {
return err
} else if !ok {
return nil
}
return a.RemoveFile(filePath)
}
func updateJobProgress(logger mlog.LoggerIFace, store store.Store, job *model.Job, key string, value int) {
if job != nil {
job.Data[key] = strconv.Itoa(value)
if _, err2 := store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err2 != nil {
logger.Warn("Failed to update job status", mlog.Err(err2))
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
)
func ImportLineFromTeam(team *model.TeamForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "team",
Team: &imports.TeamImportData{
Name: &team.Name,
DisplayName: &team.DisplayName,
Type: &team.Type,
Description: &team.Description,
AllowOpenInvite: &team.AllowOpenInvite,
Scheme: team.SchemeName,
},
}
}
func ImportLineFromChannel(channel *model.ChannelForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "channel",
Channel: &imports.ChannelImportData{
Team: &channel.TeamName,
Name: &channel.Name,
DisplayName: &channel.DisplayName,
Type: &channel.Type,
Header: &channel.Header,
Purpose: &channel.Purpose,
Scheme: channel.SchemeName,
},
}
}
func ImportLineFromDirectChannel(channel *model.DirectChannelForExport, favoritedBy []string) *imports.LineImportData {
channelMembers := *channel.Members
if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]}
}
line := &imports.LineImportData{
Type: "direct_channel",
DirectChannel: &imports.DirectChannelImportData{
Header: &channel.Header,
Members: &channelMembers,
},
}
if len(favoritedBy) != 0 {
line.DirectChannel.FavoritedBy = &favoritedBy
}
return line
}
func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imports.LineImportData {
// Bulk Importer doesn't accept "empty string" for AuthService.
var authService *string
if user.AuthService != "" {
authService = &user.AuthService
}
return &imports.LineImportData{
Type: "user",
User: &imports.UserImportData{
Username: &user.Username,
Email: &user.Email,
AuthService: authService,
AuthData: user.AuthData,
Nickname: &user.Nickname,
FirstName: &user.FirstName,
LastName: &user.LastName,
Position: &user.Position,
Roles: &user.Roles,
Locale: &user.Locale,
UseMarkdownPreview: exportedPrefs["UseMarkdownPreview"],
UseFormatting: exportedPrefs["UseFormatting"],
ShowUnreadSection: exportedPrefs["ShowUnreadSection"],
Theme: exportedPrefs["Theme"],
UseMilitaryTime: exportedPrefs["UseMilitaryTime"],
CollapsePreviews: exportedPrefs["CollapsePreviews"],
MessageDisplay: exportedPrefs["MessageDisplay"],
ColorizeUsernames: exportedPrefs["ColorizeUsernames"],
ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"],
TutorialStep: exportedPrefs["TutorialStep"],
EmailInterval: exportedPrefs["EmailInterval"],
DeleteAt: &user.DeleteAt,
},
}
}
func ImportUserTeamDataFromTeamMember(member *model.TeamMemberForExport) *imports.UserTeamImportData {
rolesList := strings.Fields(member.Roles)
if member.SchemeAdmin {
rolesList = append(rolesList, model.TeamAdminRoleId)
}
if member.SchemeUser {
rolesList = append(rolesList, model.TeamUserRoleId)
}
if member.SchemeGuest {
rolesList = append(rolesList, model.TeamGuestRoleId)
}
roles := strings.Join(rolesList, " ")
return &imports.UserTeamImportData{
Name: &member.TeamName,
Roles: &roles,
}
}
func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelMemberForExport, preferences *model.Preferences) *imports.UserChannelImportData {
rolesList := strings.Fields(member.Roles)
if member.SchemeAdmin {
rolesList = append(rolesList, model.ChannelAdminRoleId)
}
if member.SchemeUser {
rolesList = append(rolesList, model.ChannelUserRoleId)
}
if member.SchemeGuest {
rolesList = append(rolesList, model.ChannelGuestRoleId)
}
props := member.NotifyProps
notifyProps := imports.UserChannelNotifyPropsImportData{}
desktop, exist := props[model.DesktopNotifyProp]
if exist {
notifyProps.Desktop = &desktop
}
mobile, exist := props[model.PushNotifyProp]
if exist {
notifyProps.Mobile = &mobile
}
markUnread, exist := props[model.MarkUnreadNotifyProp]
if exist {
notifyProps.MarkUnread = &markUnread
}
favorite := false
for _, preference := range *preferences {
if member.ChannelId == preference.Name {
favorite = true
}
}
roles := strings.Join(rolesList, " ")
return &imports.UserChannelImportData{
Name: &member.ChannelName,
Roles: &roles,
NotifyProps: ¬ifyProps,
Favorite: &favorite,
MentionCount: &member.MentionCount,
MentionCountRoot: &member.MentionCountRoot,
UrgentMentionCount: &member.UrgentMentionCount,
MsgCount: &member.MsgCount,
MsgCountRoot: &member.MsgCountRoot,
LastViewedAt: &member.LastViewedAt,
}
}
func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "post",
Post: &imports.PostImportData{
Team: &post.TeamName,
Channel: &post.ChannelName,
User: &post.Username,
Type: &post.Type,
Message: &post.Message,
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
},
}
}
func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImportData {
channelMembers := *post.ChannelMembers
if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]}
}
return &imports.LineImportData{
Type: "direct_post",
DirectPost: &imports.DirectPostImportData{
ChannelMembers: &channelMembers,
User: &post.User,
Type: &post.Type,
Message: &post.Message,
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
},
}
}
func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData {
return &imports.ReplyImportData{
User: &post.Username,
Type: &post.Type,
Message: &post.Message,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
}
}
func ImportReactionFromPost(user *model.User, reaction *model.Reaction) *imports.ReactionImportData {
return &imports.ReactionImportData{
User: &user.Username,
EmojiName: &reaction.EmojiName,
CreateAt: &reaction.CreateAt,
}
}
func ImportLineFromEmoji(emoji *model.Emoji, filePath string) *imports.LineImportData {
return &imports.LineImportData{
Type: "emoji",
Emoji: &imports.EmojiImportData{
Name: &emoji.Name,
Image: &filePath,
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// extractTarGz takes in an io.Reader containing the bytes for a .tar.gz file and
// a destination string to extract to.
func extractTarGz(gzipStream io.Reader, dst string) error {
if dst == "" {
return errors.New("no destination path provided")
}
uncompressedStream, err := gzip.NewReader(gzipStream)
if err != nil {
return errors.Wrap(err, "failed to initialize gzip reader")
}
defer uncompressedStream.Close()
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return errors.Wrap(err, "failed to read next file from archive")
}
// Preemptively check type flag to avoid reporting a misleading error in
// trying to sanitize the header name.
switch header.Typeflag {
case tar.TypeDir:
case tar.TypeReg:
default:
mlog.Warn("skipping unsupported header type on extracting tar file", mlog.String("header_type", string(header.Typeflag)), mlog.String("header_name", header.Name))
continue
}
// filepath.HasPrefix is deprecated, so we just use strings.HasPrefix to ensure
// the target path remains rooted at dst and has no `../` escaping outside.
path := filepath.Join(dst, header.Name)
if !strings.HasPrefix(path, dst) {
return errors.Errorf("failed to sanitize path %s", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path, 0744); err != nil && !os.IsExist(err) {
return err
}
case tar.TypeReg:
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0744); err != nil {
return err
}
copyFile := func() error {
outFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return err
}
return nil
}
if err := copyFile(); err != nil {
return err
}
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package featureflag
import (
"math"
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/splitio/go-client/v6/splitio/client"
"github.com/splitio/go-client/v6/splitio/conf"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SyncParams struct {
ServerID string
SplitKey string
SyncIntervalSeconds int
Log *mlog.Logger
Attributes map[string]any
}
type Synchronizer struct {
SyncParams
client *client.SplitClient
stop chan struct{}
stopped chan struct{}
}
var featureNames = getStructFields(model.FeatureFlags{})
func NewSynchronizer(params SyncParams) (*Synchronizer, error) {
cfg := conf.Default()
if params.Log != nil {
cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))}
} else {
cfg.LoggerConfig.LogLevel = math.MinInt32
}
factory, err := client.NewSplitFactory(params.SplitKey, cfg)
if err != nil {
return nil, errors.Wrap(err, "unable to create split factory")
}
return &Synchronizer{
SyncParams: params,
client: factory.Client(),
stop: make(chan struct{}),
stopped: make(chan struct{}),
}, nil
}
// EnsureReady blocks until the synchronizer is ready to update feature flag values
func (f *Synchronizer) EnsureReady() error {
if err := f.client.BlockUntilReady(10); err != nil {
return errors.Wrap(err, "split.io client could not initialize")
}
return nil
}
func (f *Synchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags {
featuresMap := f.client.Treatments(f.ServerID, featureNames, f.Attributes)
ffm := featureFlagsFromMap(featuresMap, base)
return ffm
}
func (f *Synchronizer) Close() {
f.client.Destroy()
}
// featureFlagsFromMap sets the feature flags from a map[string]string.
// It starts with baseFeatureFlags and only sets values that are
// given by the upstream management system.
// Makes the assumption that all feature flags are strings or booleans.
// Strings are converted to booleans by considering case insensitive "on" or any value considered by strconv.ParseBool as true and any other value as false.
func featureFlagsFromMap(featuresMap map[string]string, baseFeatureFlags model.FeatureFlags) model.FeatureFlags {
refStruct := reflect.ValueOf(&baseFeatureFlags).Elem()
for fieldName, fieldValue := range featuresMap {
refField := refStruct.FieldByName(fieldName)
// "control" is returned by split.io if the treatment is not found, in this case we should use the default value.
if !refField.IsValid() || !refField.CanSet() || fieldValue == "control" {
continue
}
switch refField.Type().Kind() {
case reflect.Bool:
parsedBoolValue, _ := strconv.ParseBool(fieldValue)
refField.Set(reflect.ValueOf(strings.ToLower(fieldValue) == "on" || parsedBoolValue))
default:
refField.Set(reflect.ValueOf(fieldValue))
}
}
return baseFeatureFlags
}
func getStructFields(s any) []string {
structType := reflect.TypeOf(s)
fieldNames := make([]string, 0, structType.NumField())
for i := 0; i < structType.NumField(); i++ {
fieldNames = append(fieldNames, structType.Field(i).Name)
}
return fieldNames
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package featureflag
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type splitLogger struct {
wrappedLog *mlog.Logger
}
func (s *splitLogger) Error(msg ...any) {
s.wrappedLog.Error(fmt.Sprint(msg...))
}
func (s *splitLogger) Warning(msg ...any) {
s.wrappedLog.Warn(fmt.Sprint(msg...))
}
// Ignoring more verbose messages from split
func (s *splitLogger) Info(msg ...any) {
//s.wrappedLog.Info(fmt.Sprint(msg...))
}
func (s *splitLogger) Debug(msg ...any) {
//s.wrappedLog.Debug(fmt.Sprint(msg...))
}
func (s *splitLogger) Verbose(msg ...any) {
//s.wrappedLog.Info(fmt.Sprint(msg...))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"io"
"math"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/docextractor"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
const (
imageThumbnailWidth = 120
imageThumbnailHeight = 100
imagePreviewWidth = 1920
miniPreviewImageWidth = 16
miniPreviewImageHeight = 16
jpegEncQuality = 90
maxUploadInitialBufferSize = 1024 * 1024 // 1MB
maxContentExtractionSize = 1024 * 1024 // 1MB
)
// Ensure fileInfo service wrapper implements `product.FileInfoStoreService`
var _ product.FileInfoStoreService = (*fileInfoWrapper)(nil)
// fileInfoWrapper implements `product.FileInfoStoreService` for use by products.
type fileInfoWrapper struct {
srv *Server
}
func (f *fileInfoWrapper) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return f.srv.getFileInfo(fileID)
}
func (a *App) FileBackend() filestore.FileBackend {
return a.ch.filestore
}
func (a *App) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
fileBackendSettings := settings.ToFileBackendSettings(false, false)
err := fileBackendSettings.CheckMandatoryS3Fields()
if err != nil {
return model.NewAppError("CheckMandatoryS3Fields", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
func connectionTestErrorToAppError(connTestErr error) *model.AppError {
switch err := connTestErr.(type) {
case *filestore.S3FileBackendAuthError:
return model.NewAppError("TestConnection", "api.file.test_connection_s3_auth.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case *filestore.S3FileBackendNoBucketError:
return model.NewAppError("TestConnection", "api.file.test_connection_s3_bucket_does_not_exist.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("TestConnection", "api.file.test_connection.app_error", nil, "", http.StatusInternalServerError).Wrap(connTestErr)
}
}
func (a *App) TestFileStoreConnection() *model.AppError {
nErr := a.FileBackend().TestConnection()
if nErr != nil {
return connectionTestErrorToAppError(nErr)
}
return nil
}
func (a *App) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.AppError {
license := a.Srv().License()
insecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err := filestore.NewFileBackend(cfg.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err != nil {
return model.NewAppError("FileBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
nErr := backend.TestConnection()
if nErr != nil {
return connectionTestErrorToAppError(nErr)
}
return nil
}
func (a *App) ReadFile(path string) ([]byte, *model.AppError) {
return a.ch.srv.ReadFile(path)
}
func (s *Server) fileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
result, nErr := s.FileBackend().Reader(path)
if nErr != nil {
return nil, model.NewAppError("FileReader", "api.file.file_reader.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
// Caller must close the first return value
func (a *App) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return a.Srv().fileReader(path)
}
func (a *App) FileExists(path string) (bool, *model.AppError) {
return a.Srv().fileExists(path)
}
func (s *Server) fileExists(path string) (bool, *model.AppError) {
result, nErr := s.FileBackend().FileExists(path)
if nErr != nil {
return false, model.NewAppError("FileExists", "api.file.file_exists.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (a *App) FileSize(path string) (int64, *model.AppError) {
size, nErr := a.FileBackend().FileSize(path)
if nErr != nil {
return 0, model.NewAppError("FileSize", "api.file.file_size.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return size, nil
}
func (a *App) FileModTime(path string) (time.Time, *model.AppError) {
modTime, nErr := a.FileBackend().FileModTime(path)
if nErr != nil {
return time.Time{}, model.NewAppError("FileModTime", "api.file.file_mod_time.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return modTime, nil
}
func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
nErr := a.FileBackend().MoveFile(oldPath, newPath)
if nErr != nil {
return model.NewAppError("MoveFile", "api.file.move_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
return a.Srv().writeFileContext(ctx, fr, path)
}
func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
return a.Srv().writeFile(fr, path)
}
func (s *Server) writeFile(fr io.Reader, path string) (int64, *model.AppError) {
result, nErr := s.FileBackend().WriteFile(fr, path)
if nErr != nil {
return result, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (s *Server) writeFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
// Check if we can provide a custom context, otherwise just use the default method.
written, err := filestore.TryWriteFileContext(s.FileBackend(), ctx, fr, path)
if err != nil {
return written, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return written, nil
}
func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
result, nErr := a.FileBackend().AppendFile(fr, path)
if nErr != nil {
return result, model.NewAppError("AppendFile", "api.file.append_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (a *App) RemoveFile(path string) *model.AppError {
return a.Srv().removeFile(path)
}
func (s *Server) removeFile(path string) *model.AppError {
nErr := s.FileBackend().RemoveFile(path)
if nErr != nil {
return model.NewAppError("RemoveFile", "api.file.remove_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) ListDirectory(path string) ([]string, *model.AppError) {
return a.Srv().listDirectory(path, false)
}
func (a *App) ListDirectoryRecursively(path string) ([]string, *model.AppError) {
return a.Srv().listDirectory(path, true)
}
func (s *Server) listDirectory(path string, recursion bool) ([]string, *model.AppError) {
backend := s.FileBackend()
var paths []string
var nErr error
if recursion {
paths, nErr = backend.ListDirectoryRecursively(path)
} else {
paths, nErr = backend.ListDirectory(path)
}
if nErr != nil {
return nil, model.NewAppError("ListDirectory", "api.file.list_directory.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return paths, nil
}
func (a *App) RemoveDirectory(path string) *model.AppError {
nErr := a.FileBackend().RemoveDirectory(path)
if nErr != nil {
return model.NewAppError("RemoveDirectory", "api.file.remove_directory.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) getInfoForFilename(post *model.Post, teamID, channelID, userID, oldId, filename string) *model.FileInfo {
name, _ := url.QueryUnescape(filename)
pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamID, channelID, userID, oldId)
path := pathPrefix + name
// Open the file and populate the fields of the FileInfo
data, err := a.ReadFile(path)
if err != nil {
mlog.Error(
"File not found when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("filename", filename),
mlog.String("path", path),
mlog.Err(err),
)
return nil
}
info, err := model.GetInfoForBytes(name, bytes.NewReader(data), len(data))
if err != nil {
mlog.Warn(
"Unable to fully decode file info when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("filename", filename),
mlog.Err(err),
)
}
// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
info.Id = model.NewId()
info.CreatorId = post.UserId
info.PostId = post.Id
info.ChannelId = post.ChannelId
info.CreateAt = post.CreateAt
info.UpdateAt = post.UpdateAt
info.Path = path
if info.IsImage() && !info.IsSvg() {
nameWithoutExtension := name[:strings.LastIndex(name, ".")]
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
}
return info
}
func (a *App) findTeamIdForFilename(post *model.Post, id, filename string) string {
name, _ := url.QueryUnescape(filename)
// This post is in a direct channel so we need to figure out what team the files are stored under.
teams, err := a.Srv().Store().Team().GetTeamsByUserId(post.UserId)
if err != nil {
mlog.Error("Unable to get teams when migrating post to use FileInfo", mlog.Err(err), mlog.String("post_id", post.Id))
return ""
}
if len(teams) == 1 {
// The user has only one team so the post must've been sent from it
return teams[0].Id
}
for _, team := range teams {
path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
if ok, err := a.FileExists(path); ok && err == nil {
// Found the team that this file was posted from
return team.Id
}
}
return ""
}
var fileMigrationLock sync.Mutex
var oldFilenameMatchExp *regexp.Regexp = regexp.MustCompile(`^\/([a-z\d]{26})\/([a-z\d]{26})\/([a-z\d]{26})\/([^\/]+)$`)
// Parse the path from the Filename of the form /{channelID}/{userID}/{uid}/{nameWithExtension}
func parseOldFilenames(filenames []string, channelID, userID string) [][]string {
parsed := [][]string{}
for _, filename := range filenames {
matches := oldFilenameMatchExp.FindStringSubmatch(filename)
if len(matches) != 5 {
mlog.Error("Failed to parse old Filename", mlog.String("filename", filename))
continue
}
if matches[1] != channelID {
mlog.Error("ChannelId in Filename does not match", mlog.String("channel_id", channelID), mlog.String("matched", matches[1]))
} else if matches[2] != userID {
mlog.Error("UserId in Filename does not match", mlog.String("user_id", userID), mlog.String("matched", matches[2]))
} else {
parsed = append(parsed, matches[1:])
}
}
return parsed
}
// Creates and stores FileInfos for a post created before the FileInfos table existed.
func (a *App) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
if len(post.Filenames) == 0 {
mlog.Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
channel, errCh := a.Srv().Store().Channel().Get(post.ChannelId, true)
// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
if errCh != nil {
mlog.Error(
"Unable to get channel when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(errCh),
)
return []*model.FileInfo{}
}
// Parse and validate filenames before further processing
parsedFilenames := parseOldFilenames(filenames, post.ChannelId, post.UserId)
if len(parsedFilenames) == 0 {
mlog.Error("Unable to parse filenames")
return []*model.FileInfo{}
}
// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
var teamID string
if channel.TeamId == "" {
// This post was made in a cross-team DM channel, so we need to find where its files were saved
teamID = a.findTeamIdForFilename(post, parsedFilenames[0][2], parsedFilenames[0][3])
} else {
teamID = channel.TeamId
}
// Create FileInfo objects for this post
infos := make([]*model.FileInfo, 0, len(filenames))
if teamID == "" {
mlog.Error(
"Unable to find team id for files when migrating post to use FileInfos",
mlog.String("filenames", strings.Join(filenames, ",")),
mlog.String("post_id", post.Id),
)
} else {
for _, parsed := range parsedFilenames {
info := a.getInfoForFilename(post, teamID, parsed[0], parsed[1], parsed[2], parsed[3])
if info == nil {
continue
}
infos = append(infos, info)
}
}
// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
fileMigrationLock.Lock()
defer fileMigrationLock.Unlock()
result, nErr := a.Srv().Store().Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
if nErr != nil {
mlog.Error("Unable to get post when migrating post to use FileInfos", mlog.Err(nErr), mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
if newPost := result.Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
// Another thread has already created FileInfos for this post, so just return those
var fileInfos []*model.FileInfo
fileInfos, nErr = a.Srv().Store().FileInfo().GetForPost(post.Id, true, false, false)
if nErr != nil {
mlog.Error("Unable to get FileInfos for migrated post", mlog.Err(nErr), mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
mlog.Debug("Post already migrated to use FileInfos", mlog.String("post_id", post.Id))
return fileInfos
}
mlog.Debug("Migrating post to use FileInfos", mlog.String("post_id", post.Id))
savedInfos := make([]*model.FileInfo, 0, len(infos))
fileIDs := make([]string, 0, len(filenames))
for _, info := range infos {
if _, nErr = a.Srv().Store().FileInfo().Save(info); nErr != nil {
mlog.Error(
"Unable to save file info when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("file_info_id", info.Id),
mlog.String("file_info_path", info.Path),
mlog.Err(nErr),
)
continue
}
savedInfos = append(savedInfos, info)
fileIDs = append(fileIDs, info.Id)
}
// Copy and save the updated post
newPost := post.Clone()
newPost.Filenames = []string{}
newPost.FileIds = fileIDs
// Update Posts to clear Filenames and set FileIds
if _, nErr = a.Srv().Store().Post().Update(newPost, post); nErr != nil {
mlog.Error(
"Unable to save migrated post when migrating to use FileInfos",
mlog.String("new_file_ids", strings.Join(newPost.FileIds, ",")),
mlog.String("old_filenames", strings.Join(post.Filenames, ",")),
mlog.String("post_id", post.Id),
mlog.Err(nErr),
)
return []*model.FileInfo{}
}
return savedInfos
}
func (a *App) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt)
return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash)
}
func GeneratePublicLinkHash(fileID, salt string) string {
hash := sha256.New()
hash.Write([]byte(salt))
hash.Write([]byte(fileID))
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
}
// UploadFile uploads a single file in form of a completely constructed byte array for a channel.
func (a *App) UploadFile(c request.CTX, data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
_, err := a.GetChannel(c, channelID)
if err != nil && channelID != "" {
return nil, model.NewAppError("UploadFile", "api.file.upload_file.incorrect_channelId.app_error",
map[string]any{"channelId": channelID}, "", http.StatusBadRequest)
}
info, _, appError := a.DoUploadFileExpectModification(c, time.Now(), "noteam", channelID, "nouser", filename, data)
if appError != nil {
return nil, appError
}
if info.PreviewPath != "" || info.ThumbnailPath != "" {
previewPathList := []string{info.PreviewPath}
thumbnailPathList := []string{info.ThumbnailPath}
imageDataList := [][]byte{data}
a.HandleImages(previewPathList, thumbnailPathList, imageDataList)
}
return info, nil
}
func (a *App) DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
info, _, err := a.DoUploadFileExpectModification(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
return info, err
}
func UploadFileSetTeamId(teamID string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.TeamId = filepath.Base(teamID)
}
}
func UploadFileSetUserId(userID string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.UserId = filepath.Base(userID)
}
}
func UploadFileSetTimestamp(timestamp time.Time) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.Timestamp = timestamp
}
}
func UploadFileSetContentLength(contentLength int64) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.ContentLength = contentLength
}
}
func UploadFileSetClientId(clientId string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.ClientId = clientId
}
}
func UploadFileSetRaw() func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.Raw = true
}
}
type UploadFileTask struct {
// File name.
Name string
ChannelId string
TeamId string
UserId string
// Time stamp to use when creating the file.
Timestamp time.Time
// The value of the Content-Length http header, when available.
ContentLength int64
// The file data stream.
Input io.Reader
// An optional, client-assigned Id field.
ClientId string
// If Raw, do not execute special processing for images, just upload
// the file. Plugins are still invoked.
Raw bool
//=============================================================
// Internal state
buf *bytes.Buffer
limit int64
limitedInput io.Reader
teeInput io.Reader
fileinfo *model.FileInfo
maxFileSize int64
maxImageRes int64
// Cached image data that (may) get initialized in preprocessImage and
// is used in postprocessImage
decoded image.Image
imageType string
imageOrientation int
// Testing: overridable dependency functions
pluginsEnvironment *plugin.Environment
writeFile func(io.Reader, string) (int64, *model.AppError)
saveToDatabase func(*model.FileInfo) (*model.FileInfo, error)
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
}
func (t *UploadFileTask) init(a *App) {
t.buf = &bytes.Buffer{}
if t.ContentLength > 0 {
t.limit = t.ContentLength
} else {
t.limit = t.maxFileSize
}
if t.ContentLength > 0 && t.ContentLength < maxUploadInitialBufferSize {
t.buf.Grow(int(t.ContentLength))
} else {
t.buf.Grow(maxUploadInitialBufferSize)
}
t.fileinfo = model.NewInfo(filepath.Base(t.Name))
t.fileinfo.Id = model.NewId()
t.fileinfo.CreatorId = t.UserId
t.fileinfo.CreateAt = t.Timestamp.UnixNano() / int64(time.Millisecond)
t.fileinfo.Path = t.pathPrefix() + t.Name
t.limitedInput = &io.LimitedReader{
R: t.Input,
N: t.limit + 1,
}
t.teeInput = io.TeeReader(t.limitedInput, t.buf)
t.pluginsEnvironment = a.GetPluginsEnvironment()
t.writeFile = a.WriteFile
t.saveToDatabase = a.Srv().Store().FileInfo().Save
}
// UploadFileX uploads a single file as specified in t. It applies the upload
// constraints, executes plugins and image processing logic as needed. It
// returns a filled-out FileInfo and an optional error. A plugin may reject the
// upload, returning a rejection error. In this case FileInfo would have
// contained the last "good" FileInfo before the execution of that plugin.
func (a *App) UploadFileX(c *request.Context, channelID, name string, input io.Reader,
opts ...func(*UploadFileTask)) (*model.FileInfo, *model.AppError) {
t := &UploadFileTask{
ChannelId: filepath.Base(channelID),
Name: filepath.Base(name),
Input: input,
maxFileSize: *a.Config().FileSettings.MaxFileSize,
maxImageRes: *a.Config().FileSettings.MaxImageResolution,
imgDecoder: a.ch.imgDecoder,
imgEncoder: a.ch.imgEncoder,
}
for _, o := range opts {
o(t)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, t.newAppError("api.file.upload_file.storage.app_error", http.StatusNotImplemented)
}
if t.ContentLength > t.maxFileSize {
return nil, t.newAppError("api.file.upload_file.too_large_detailed.app_error", http.StatusRequestEntityTooLarge, "Length", t.ContentLength, "Limit", t.maxFileSize)
}
t.init(a)
var aerr *model.AppError
if !t.Raw && t.fileinfo.IsImage() {
aerr = t.preprocessImage()
if aerr != nil {
return t.fileinfo, aerr
}
}
written, aerr := t.writeFile(io.MultiReader(t.buf, t.limitedInput), t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
if written > t.maxFileSize {
if fileErr := a.RemoveFile(t.fileinfo.Path); fileErr != nil {
mlog.Error("Failed to remove file", mlog.Err(fileErr))
}
return nil, t.newAppError("api.file.upload_file.too_large_detailed.app_error", http.StatusRequestEntityTooLarge, "Length", t.ContentLength, "Limit", t.maxFileSize)
}
t.fileinfo.Size = written
file, aerr := a.FileReader(t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
defer file.Close()
aerr = a.runPluginsHook(c, t.fileinfo, file)
if aerr != nil {
return nil, aerr
}
if !t.Raw && t.fileinfo.IsImage() {
file, aerr = a.FileReader(t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
defer file.Close()
t.postprocessImage(file)
}
if _, err := t.saveToDatabase(t.fileinfo); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UploadFileX", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *t.fileinfo
a.Srv().GoBuffered(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
return t.fileinfo, nil
}
func (t *UploadFileTask) preprocessImage() *model.AppError {
// If SVG, attempt to extract dimensions and then return
if t.fileinfo.IsSvg() {
svgInfo, err := imaging.ParseSVG(t.teeInput)
if err != nil {
mlog.Warn("Failed to parse SVG", mlog.Err(err))
}
if svgInfo.Width > 0 && svgInfo.Height > 0 {
t.fileinfo.Width = svgInfo.Width
t.fileinfo.Height = svgInfo.Height
}
t.fileinfo.HasPreviewImage = false
return nil
}
// If we fail to decode, return "as is".
w, h, err := imaging.GetDimensions(t.teeInput)
if err != nil {
return nil
}
t.fileinfo.Width = w
t.fileinfo.Height = h
if err = checkImageResolutionLimit(w, h, t.maxImageRes); err != nil {
return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest)
}
t.fileinfo.HasPreviewImage = true
nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")]
t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview." + getFileExtFromMimeType(t.fileinfo.MimeType)
t.fileinfo.ThumbnailPath = t.pathPrefix() + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(t.fileinfo.MimeType)
// check the image orientation with goexif; consume the bytes we
// already have first, then keep Tee-ing from input.
// TODO: try to reuse exif's .Raw buffer rather than Tee-ing
if t.imageOrientation, err = imaging.GetImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput)); err == nil &&
(t.imageOrientation == imaging.RotatedCWMirrored ||
t.imageOrientation == imaging.RotatedCCW ||
t.imageOrientation == imaging.RotatedCCWMirrored ||
t.imageOrientation == imaging.RotatedCW) {
t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width
}
// For animated GIFs disable the preview; since we have to Decode gifs
// anyway, cache the decoded image for later.
if t.fileinfo.MimeType == "image/gif" {
image, format, err := t.imgDecoder.Decode(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput))
if err == nil && image != nil {
t.fileinfo.HasPreviewImage = false
t.decoded = image
t.imageType = format
}
}
return nil
}
func (t *UploadFileTask) postprocessImage(file io.Reader) {
// don't try to process SVG files
if t.fileinfo.IsSvg() {
return
}
decoded, imgType := t.decoded, t.imageType
if decoded == nil {
var err error
var release func()
decoded, imgType, release, err = t.imgDecoder.DecodeMemBounded(file)
if err != nil {
mlog.Error("Unable to decode image", mlog.Err(err))
return
}
defer release()
}
decoded = imaging.MakeImageUpright(decoded, t.imageOrientation)
if decoded == nil {
return
}
writeImage := func(img image.Image, path string) {
r, w := io.Pipe()
go func() {
var err error
// It's okay to access imgType in a separate goroutine,
// because imgType is only written once and never written again.
if imgType == "png" {
err = t.imgEncoder.EncodePNG(w, img)
} else {
err = t.imgEncoder.EncodeJPEG(w, img, jpegEncQuality)
}
if err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err))
w.CloseWithError(err)
} else {
w.Close()
}
}()
_, aerr := t.writeFile(r, path)
if aerr != nil {
mlog.Error("Unable to upload", mlog.String("path", path), mlog.Err(aerr))
r.CloseWithError(aerr) // always returns nil
return
}
}
var wg sync.WaitGroup
wg.Add(3)
// Generating thumbnail and preview regardless of HasPreviewImage value.
// This is needed on mobile in case of animated GIFs.
go func() {
defer wg.Done()
writeImage(imaging.GenerateThumbnail(decoded, imageThumbnailWidth, imageThumbnailHeight), t.fileinfo.ThumbnailPath)
}()
go func() {
defer wg.Done()
writeImage(imaging.GeneratePreview(decoded, imagePreviewWidth), t.fileinfo.PreviewPath)
}()
go func() {
defer wg.Done()
if t.fileinfo.MiniPreview == nil {
if miniPreview, err := imaging.GenerateMiniPreviewImage(decoded,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
t.fileinfo.MiniPreview = &miniPreview
}
}
}()
wg.Wait()
}
func (t UploadFileTask) pathPrefix() string {
return t.Timestamp.Format("20060102") +
"/teams/" + t.TeamId +
"/channels/" + t.ChannelId +
"/users/" + t.UserId +
"/" + t.fileinfo.Id + "/"
}
func (t UploadFileTask) newAppError(id string, httpStatus int, extra ...any) *model.AppError {
params := map[string]any{
"Name": t.Name,
"Filename": t.Name,
"ChannelId": t.ChannelId,
"TeamId": t.TeamId,
"UserId": t.UserId,
"ContentLength": t.ContentLength,
"ClientId": t.ClientId,
}
if t.fileinfo != nil {
params["Width"] = t.fileinfo.Width
params["Height"] = t.fileinfo.Height
}
for i := 0; i+1 < len(extra); i += 2 {
params[fmt.Sprintf("%v", extra[i])] = extra[i+1]
}
return model.NewAppError("uploadFileTask", id, params, "", httpStatus)
}
func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) {
filename := filepath.Base(rawFilename)
teamID := filepath.Base(rawTeamId)
channelID := filepath.Base(rawChannelId)
userID := filepath.Base(rawUserId)
info, err := model.GetInfoForBytes(filename, bytes.NewReader(data), len(data))
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, data, err
}
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data)); err == nil &&
(orientation == imaging.RotatedCWMirrored ||
orientation == imaging.RotatedCCW ||
orientation == imaging.RotatedCCWMirrored ||
orientation == imaging.RotatedCW) {
info.Width, info.Height = info.Height, info.Width
}
info.Id = model.NewId()
info.CreatorId = userID
info.CreateAt = now.UnixNano() / int64(time.Millisecond)
pathPrefix := now.Format("20060102") + "/teams/" + teamID + "/channels/" + channelID + "/users/" + userID + "/" + info.Id + "/"
info.Path = pathPrefix + filename
if info.IsImage() && !info.IsSvg() {
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]any{"Filename": filename}, "", http.StatusBadRequest).Wrap(limitErr)
return nil, data, err
}
nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
}
var rejectionError *model.AppError
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
var newBytes bytes.Buffer
replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes)
if rejectionReason != "" {
rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
return false
}
if replacementInfo != nil {
info = replacementInfo
}
if newBytes.Len() != 0 {
data = newBytes.Bytes()
info.Size = int64(len(data))
}
return true
}, plugin.FileWillBeUploadedID)
if rejectionError != nil {
return nil, data, rejectionError
}
if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
return nil, data, err
}
if _, err := a.Srv().Store().FileInfo().Save(info); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, data, appErr
default:
return nil, data, model.NewAppError("DoUploadFileExpectModification", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *info
a.Srv().GoBuffered(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
return info, data, nil
}
func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
wg := new(sync.WaitGroup)
for i := range fileData {
img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData[i]))
if err != nil {
mlog.Debug("Failed to prepare image", mlog.Err(err))
continue
}
wg.Add(2)
go func(img image.Image, imgType, path string) {
defer wg.Done()
a.generateThumbnailImage(img, imgType, path)
}(img, imgType, thumbnailPathList[i])
go func(img image.Image, imgType, path string) {
defer wg.Done()
a.generatePreviewImage(img, imgType, path)
}(img, imgType, previewPathList[i])
wg.Wait()
release()
}
}
func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image.Image, imgType string, release func(), err error) {
// Decode image bytes into Image object
img, imgType, release, err = imgDecoder.DecodeMemBounded(imgData)
if err != nil {
return nil, "", nil, fmt.Errorf("prepareImage: failed to decode image: %w", err)
}
imgData.Seek(0, io.SeekStart)
// Flip the image to be upright
orientation, err := imaging.GetImageOrientation(imgData)
if err != nil {
mlog.Debug("GetImageOrientation failed", mlog.Err(err))
}
img = imaging.MakeImageUpright(img, orientation)
return img, imgType, release, nil
}
func (a *App) generateThumbnailImage(img image.Image, imgType, thumbnailPath string) {
var buf bytes.Buffer
thumb := imaging.GenerateThumbnail(img, imageThumbnailWidth, imageThumbnailHeight)
if imgType == "png" {
if err := a.ch.imgEncoder.EncodePNG(&buf, thumb); err != nil {
mlog.Error("Unable to encode image as png", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
} else {
if err := a.ch.imgEncoder.EncodeJPEG(&buf, thumb, jpegEncQuality); err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
}
if _, err := a.WriteFile(&buf, thumbnailPath); err != nil {
mlog.Error("Unable to upload thumbnail", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
}
func (a *App) generatePreviewImage(img image.Image, imgType, previewPath string) {
var buf bytes.Buffer
preview := imaging.GeneratePreview(img, imagePreviewWidth)
if imgType == "png" {
if err := a.ch.imgEncoder.EncodePNG(&buf, preview); err != nil {
mlog.Error("Unable to encode image as preview png", mlog.Err(err), mlog.String("path", previewPath))
return
}
} else {
if err := a.ch.imgEncoder.EncodeJPEG(&buf, preview, jpegEncQuality); err != nil {
mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath))
return
}
}
if _, err := a.WriteFile(&buf, previewPath); err != nil {
mlog.Error("Unable to upload preview", mlog.Err(err), mlog.String("path", previewPath))
return
}
}
// generateMiniPreview updates mini preview if needed
// will save fileinfo with the preview added
func (a *App) generateMiniPreview(fi *model.FileInfo) {
if fi.IsImage() && !fi.IsSvg() && fi.MiniPreview == nil {
file, appErr := a.FileReader(fi.Path)
if appErr != nil {
mlog.Debug("error reading image file", mlog.Err(appErr))
return
}
defer file.Close()
img, _, release, err := prepareImage(a.ch.imgDecoder, file)
if err != nil {
mlog.Debug("generateMiniPreview: prepareImage failed", mlog.Err(err),
mlog.String("fileinfo_id", fi.Id), mlog.String("channel_id", fi.ChannelId),
mlog.String("creator_id", fi.CreatorId))
return
}
defer release()
var miniPreview []byte
if miniPreview, err = imaging.GenerateMiniPreviewImage(img,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
fi.MiniPreview = &miniPreview
}
if _, err = a.Srv().Store().FileInfo().Upsert(fi); err != nil {
mlog.Debug("creating mini preview failed", mlog.Err(err))
} else {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(fi.PostId, false)
}
}
}
func (a *App) generateMiniPreviewForInfos(fileInfos []*model.FileInfo) {
wg := new(sync.WaitGroup)
wg.Add(len(fileInfos))
for _, fileInfo := range fileInfos {
go func(fi *model.FileInfo) {
defer wg.Done()
a.generateMiniPreview(fi)
}(fileInfo)
}
wg.Wait()
}
func (s *Server) getFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, err := s.Store().FileInfo().Get(fileID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetFileInfo", "app.file_info.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetFileInfo", "app.file_info.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return fileInfo, nil
}
func (a *App) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, appErr := a.Srv().getFileInfo(fileID)
if appErr != nil {
return nil, appErr
}
firstInaccessibleFileTime, appErr := a.isInaccessibleFile(fileInfo)
if appErr != nil {
return nil, appErr
}
if firstInaccessibleFileTime > 0 {
return nil, model.NewAppError("GetFileInfo", "app.file.cloud.get.app_error", nil, "", http.StatusForbidden)
}
a.generateMiniPreview(fileInfo)
return fileInfo, appErr
}
func (a *App) getFileInfoIgnoreCloudLimit(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, appErr := a.Srv().getFileInfo(fileID)
if appErr == nil {
a.generateMiniPreview(fileInfo)
}
return fileInfo, appErr
}
func (a *App) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetWithOptions(page, perPage, opt)
if err != nil {
var invErr *store.ErrInvalidInput
var ltErr *store.ErrLimitExceeded
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, <Err):
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
filterOptions := filterFileOptions{}
if opt != nil && (opt.SortBy == "" || opt.SortBy == model.FileinfoSortByCreated) {
filterOptions.assumeSortedCreatedAt = true
}
fileInfos, _, appErr := a.getFilteredAccessibleFiles(fileInfos, filterOptions)
if appErr != nil {
return nil, appErr
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) GetFile(fileID string) ([]byte, *model.AppError) {
info, err := a.GetFileInfo(fileID)
if err != nil {
return nil, err
}
data, err := a.ReadFile(info.Path)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) getFileIgnoreCloudLimit(fileID string) ([]byte, *model.AppError) {
info, err := a.getFileInfoIgnoreCloudLimit(fileID)
if err != nil {
return nil, err
}
data, err := a.ReadFile(info.Path)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
var newFileIds []string
now := model.GetMillis()
for _, fileID := range fileIDs {
fileInfo, err := a.Srv().Store().FileInfo().Get(fileID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CopyFileInfos", "app.file_info.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CopyFileInfos", "app.file_info.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
fileInfo.Id = model.NewId()
fileInfo.CreatorId = userID
fileInfo.CreateAt = now
fileInfo.UpdateAt = now
fileInfo.PostId = ""
fileInfo.ChannelId = ""
if _, err := a.Srv().Store().FileInfo().Save(fileInfo); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CopyFileInfos", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
newFileIds = append(newFileIds, fileInfo.Id)
}
return newFileIds, nil
}
// This function zip's up all the files in fileDatas array and then saves it to the directory specified with the specified zip file name
// Ensure the zip file name ends with a .zip
func (a *App) CreateZipFileAndAddFiles(fileBackend filestore.FileBackend, fileDatas []model.FileData, zipFileName, directory string) error {
// Create Zip File (temporarily stored on disk)
conglomerateZipFile, err := os.Create(zipFileName)
if err != nil {
return err
}
defer os.Remove(zipFileName)
// Create a new zip archive.
zipFileWriter := zip.NewWriter(conglomerateZipFile)
// Populate Zip file with File Datas array
err = populateZipfile(zipFileWriter, fileDatas)
if err != nil {
return err
}
conglomerateZipFile.Seek(0, 0)
_, err = fileBackend.WriteFile(conglomerateZipFile, path.Join(directory, zipFileName))
if err != nil {
return err
}
return nil
}
// This is a implementation of Go's example of writing files to zip (with slight modification)
// https://golang.org/src/archive/zip/example_test.go
func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error {
defer w.Close()
for _, fd := range fileDatas {
f, err := w.Create(fd.Filename)
if err != nil {
return err
}
_, err = f.Write(fd.Body)
if err != nil {
return err
}
}
return nil
}
func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) {
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
if !*a.Config().ServiceSettings.EnableFileSearch {
return nil, model.NewAppError("SearchFilesInTeamForUser", "store.sql_file_info.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamId, userId), http.StatusNotImplemented)
}
finalParamsList := []*model.SearchParams{}
for _, params := range paramsList {
params.Modifier = modifier
params.OrTerms = isOrSearch
params.IncludeDeletedChannels = includeDeleted
// Don't allow users to search for "*"
if params.Terms != "*" {
// Convert channel names to channel IDs
params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userId, teamId, includeDeletedChannels)
params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userId, teamId, includeDeletedChannels)
// Convert usernames to user IDs
params.FromUsers = a.convertUserNameToUserIds(params.FromUsers)
params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers)
finalParamsList = append(finalParamsList, params)
}
}
// If the processed search params are empty, return empty search results.
if len(finalParamsList) == 0 {
return model.NewFileInfoList(), nil
}
fileInfoSearchResults, nErr := a.Srv().Store().FileInfo().Search(finalParamsList, userId, teamId, page, perPage)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SearchFilesInTeamForUser", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return fileInfoSearchResults, a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true})
}
func (a *App) ExtractContentFromFileInfo(fileInfo *model.FileInfo) error {
// We don't process images.
if fileInfo.IsImage() {
return nil
}
file, aerr := a.FileReader(fileInfo.Path)
if aerr != nil {
return errors.Wrap(aerr, "failed to open file for extract file content")
}
defer file.Close()
text, err := docextractor.Extract(fileInfo.Name, file, docextractor.ExtractSettings{
ArchiveRecursion: *a.Config().FileSettings.ArchiveRecursion,
})
if err != nil {
return errors.Wrap(err, "failed to extract file content")
}
if text != "" {
if len(text) > maxContentExtractionSize {
text = text[0:maxContentExtractionSize]
}
if storeErr := a.Srv().Store().FileInfo().SetContent(fileInfo.Id, text); storeErr != nil {
return errors.Wrap(storeErr, "failed to save the extracted file content")
}
reloadFileInfo, storeErr := a.Srv().Store().FileInfo().Get(fileInfo.Id)
if storeErr != nil {
mlog.Warn("Failed to invalidate the fileInfo cache.", mlog.Err(storeErr), mlog.String("file_info_id", fileInfo.Id))
} else {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(reloadFileInfo.PostId, false)
}
}
return nil
}
// GetLastAccessibleFileTime returns CreateAt time(from cache) of the last accessible post as per the cloud limit
func (a *App) GetLastAccessibleFileTime() (int64, *model.AppError) {
license := a.Srv().License()
if !license.IsCloud() {
return 0, nil
}
system, err := a.Srv().Store().System().GetByName(model.SystemLastAccessibleFileTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All files are accessible
return 0, nil
default:
return 0, model.NewAppError("GetLastAccessibleFileTime", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
lastAccessibleFileTime, err := strconv.ParseInt(system.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("GetLastAccessibleFileTime", "common.parse_error_int64", map[string]interface{}{"Value": system.Value}, err.Error(), http.StatusInternalServerError)
}
return lastAccessibleFileTime, nil
}
// ComputeLastAccessibleFileTime updates cache with CreateAt time of the last accessible file as per the cloud plan's limit.
// Use GetLastAccessibleFileTime() to access the result.
func (a *App) ComputeLastAccessibleFileTime() error {
limit, appErr := a.getCloudFilesSizeLimit()
if appErr != nil {
return appErr
}
if limit == 0 {
// All files are accessible - we must check if a previous value was set so we can clear it
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessibleFileTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All files are already accessible
return nil
default:
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if systemValue != nil {
// Previous value was set, so we must clear it
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessibleFileTime); err != nil {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.permanent_delete_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return nil
}
createdAt, err := a.Srv().GetStore().FileInfo().GetUptoNSizeFileTime(limit)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.last_accessible_file.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// Update Cache
err = a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemLastAccessibleFileTime,
Value: strconv.FormatInt(createdAt, 10),
})
if err != nil {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
// getCloudFilesSizeLimit returns size in bytes
func (a *App) getCloudFilesSizeLimit() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
// limits is in bits
limits, err := a.Cloud().GetCloudLimits("")
if err != nil {
return 0, model.NewAppError("getCloudFilesSizeLimit", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if limits == nil || limits.Files == nil || limits.Files.TotalStorage == nil {
// Cloud limit is not applicable
return 0, nil
}
return int64(math.Ceil(float64(*limits.Files.TotalStorage) / 8)), nil
}
func getFileExtFromMimeType(mimeType string) string {
if mimeType == "image/png" {
return "png"
}
return "jpg"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
// removeInaccessibleContentFromFilesSlice removes content from the files beyond the cloud plan's limit
// and also returns the firstInaccessibleFileTime
func (a *App) removeInaccessibleContentFromFilesSlice(files []*model.FileInfo) (int64, *model.AppError) {
if len(files) == 0 {
return 0, nil
}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return 0, model.NewAppError("removeInaccessibleFileListContent", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
if lastAccessibleFileTime == 0 {
// No need to remove content, all files are accessible
return 0, nil
}
var firstInaccessibleFileTime int64 = 0
for _, file := range files {
if createAt := file.CreateAt; createAt < lastAccessibleFileTime {
file.MakeContentInaccessible()
if createAt > firstInaccessibleFileTime {
firstInaccessibleFileTime = createAt
}
}
}
return firstInaccessibleFileTime, nil
}
// filterInaccessibleFiles filters out the files, past the cloud limit
func (a *App) filterInaccessibleFiles(fileList *model.FileInfoList, options filterFileOptions) *model.AppError {
if fileList == nil || fileList.FileInfos == nil || len(fileList.FileInfos) == 0 {
return nil
}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return model.NewAppError("filterInaccessibleFiles", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
if lastAccessibleFileTime == 0 {
// No need to filter, all files are accessible
return nil
}
if len(fileList.FileInfos) == len(fileList.Order) && options.assumeSortedCreatedAt {
lenFiles := len(fileList.FileInfos)
getCreateAt := func(i int) int64 { return fileList.FileInfos[fileList.Order[i]].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessibleFileTime, lenFiles, getCreateAt)
if bounds.allAccessible(lenFiles) {
return nil
}
if bounds.noAccessible() {
if lenFiles > 0 {
firstFileCreatedAt := fileList.FileInfos[fileList.Order[0]].CreateAt
lastFileCreatedAt := fileList.FileInfos[fileList.Order[lenFiles-1]].CreateAt
fileList.FirstInaccessibleFileTime = max(firstFileCreatedAt, lastFileCreatedAt)
}
fileList.FileInfos = map[string]*model.FileInfo{}
fileList.Order = []string{}
return nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(fileList.Order))
startInaccessibleCreatedAt := fileList.FileInfos[fileList.Order[startInaccessibleIndex]].CreateAt
endInaccessibleCreatedAt := fileList.FileInfos[fileList.Order[endInaccessibleIndex]].CreateAt
fileList.FirstInaccessibleFileTime = max(startInaccessibleCreatedAt, endInaccessibleCreatedAt)
files := fileList.FileInfos
order := fileList.Order
accessibleCount := bounds.end - bounds.start + 1
inaccessibleCount := lenFiles - accessibleCount
// Linearly cover shorter route to traverse files map
if inaccessibleCount < accessibleCount {
for i := 0; i < bounds.start; i++ {
delete(files, order[i])
}
for i := bounds.end + 1; i < lenFiles; i++ {
delete(files, order[i])
}
} else {
accessibleFiles := make(map[string]*model.FileInfo, accessibleCount)
for i := bounds.start; i <= bounds.end; i++ {
accessibleFiles[order[i]] = files[order[i]]
}
fileList.FileInfos = accessibleFiles
}
fileList.Order = fileList.Order[bounds.start : bounds.end+1]
} else {
linearFilterFileList(fileList, lastAccessibleFileTime)
}
return nil
}
// isInaccessibleFile indicates if the file is past the cloud plan's limit.
func (a *App) isInaccessibleFile(file *model.FileInfo) (int64, *model.AppError) {
if file == nil {
return 0, nil
}
fl := &model.FileInfoList{
Order: []string{file.Id},
FileInfos: map[string]*model.FileInfo{file.Id: file},
}
appErr := a.filterInaccessibleFiles(fl, filterFileOptions{assumeSortedCreatedAt: true})
return fl.FirstInaccessibleFileTime, appErr
}
// getFilteredAccessibleFiles returns accessible files filtered as per the cloud plan's limit and also indicates if there were any inaccessible files
func (a *App) getFilteredAccessibleFiles(files []*model.FileInfo, options filterFileOptions) ([]*model.FileInfo, int64, *model.AppError) {
if len(files) == 0 {
return files, 0, nil
}
filteredFiles := []*model.FileInfo{}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return filteredFiles, 0, model.NewAppError("getFilteredAccessibleFiles", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
} else if lastAccessibleFileTime == 0 {
// No need to filter, all files are accessible
return files, 0, nil
}
if options.assumeSortedCreatedAt {
lenFiles := len(files)
getCreateAt := func(i int) int64 { return files[i].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessibleFileTime, lenFiles, getCreateAt)
if bounds.allAccessible(lenFiles) {
return files, 0, nil
}
if bounds.noAccessible() {
var firstInaccessibleFileTime int64 = 0
if lenFiles > 0 {
firstFileCreatedAt := files[0].CreateAt
lastFileCreatedAt := files[len(files)-1].CreateAt
firstInaccessibleFileTime = max(firstFileCreatedAt, lastFileCreatedAt)
}
return filteredFiles, firstInaccessibleFileTime, nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(files))
firstFileCreatedAt := files[startInaccessibleIndex].CreateAt
lastFileCreatedAt := files[endInaccessibleIndex].CreateAt
firstInaccessibleFileTime := max(firstFileCreatedAt, lastFileCreatedAt)
filteredFiles = files[bounds.start : bounds.end+1]
return filteredFiles, firstInaccessibleFileTime, nil
}
filteredFiles, firstInaccessibleFileTime := linearFilterFilesSlice(files, lastAccessibleFileTime)
return filteredFiles, firstInaccessibleFileTime, nil
}
type filterFileOptions struct {
assumeSortedCreatedAt bool
}
// linearFilterFileList make no assumptions about ordering, go through files one by one
// this is the slower fallback that is still safe
// if we can not assume files are ordered by CreatedAt
func linearFilterFileList(fileList *model.FileInfoList, earliestAccessibleTime int64) {
files := fileList.FileInfos
order := fileList.Order
n := 0
for i, fileID := range order {
if createAt := files[fileID].CreateAt; createAt >= earliestAccessibleTime {
order[n] = order[i]
n++
} else {
if createAt > fileList.FirstInaccessibleFileTime {
fileList.FirstInaccessibleFileTime = createAt
}
delete(files, fileID)
}
}
fileList.Order = order[:n]
}
// linearFilterFilesSlice make no assumptions about ordering, go through files one by one
// this is the slower fallback that is still safe
// if we can not assume files are ordered by CreatedAt
func linearFilterFilesSlice(files []*model.FileInfo, earliestAccessibleTime int64) ([]*model.FileInfo, int64) {
var firstInaccessibleFileTime int64 = 0
n := 0
for i := range files {
if createAt := files[i].CreateAt; createAt >= earliestAccessibleTime {
files[n] = files[i]
n++
} else {
if createAt > firstInaccessibleFileTime {
firstInaccessibleFileTime = createAt
}
}
}
return files[:n], firstInaccessibleFileTime
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if opts != nil && opts.IncludeMemberCount {
memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
}
group.MemberCount = model.NewInt(int(memberCount))
}
return group, nil
}
func (a *App) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByName(name, opts)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByName", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByName", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByRemoteID(remoteID, groupSource)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByRemoteID", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByRemoteID", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllBySource(groupSource)
if err != nil {
return nil, model.NewAppError("GetGroupsBySource", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByUser(userID)
if err != nil {
return nil, model.NewAppError("GetGroupsByUserId", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := a.isUniqueToUsernames(group.GetName()); err != nil {
err.Where = "CreateGroup"
return nil, err
}
group, err := a.Srv().Store().Group().Create(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroup", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) isUniqueToUsernames(val string) *model.AppError {
if val == "" {
return nil
}
var notFoundErr *store.ErrNotFound
user, err := a.Srv().Store().User().GetByUsername(val)
if err != nil && !errors.As(err, ¬FoundErr) {
return model.NewAppError("isUniqueToUsernames", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if user != nil {
return model.NewAppError("isUniqueToUsernames", "app.group.username_conflict", map[string]interface{}{"Username": val}, "", http.StatusBadRequest)
}
return nil
}
func (a *App) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "CreateGroupWithUserIds"
return nil, appErr
}
newGroup, err := a.Srv().Store().Group().CreateWithUserIds(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroupWithUserIds", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
count, err := a.Srv().Store().Group().GetMemberCount(newGroup.Id)
if err != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
group.MemberCount = model.NewInt(int(count))
groupJSON, jsonErr := json.Marshal(newGroup)
if jsonErr != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return newGroup, nil
}
func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "UpdateGroup"
return nil, appErr
}
updatedGroup, err := a.Srv().Store().Group().Update(group)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroup", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
count, err := a.Srv().Store().Group().GetMemberCount(updatedGroup.Id)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
updatedGroup.MemberCount = model.NewInt(int(count))
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
groupJSON, err := json.Marshal(updatedGroup)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return updatedGroup, nil
}
func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
deletedGroup, err := a.Srv().Store().Group().Delete(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroup", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return deletedGroup, nil
}
func (a *App) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
restoredGroup, err := a.Srv().Store().Group().Restore(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("RestoreGroup", "app.group.no_rows", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("RestoreGroup", "app.update_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return restoredGroup, nil
}
func (a *App) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) {
count, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err != nil {
return 0, model.NewAppError("GetGroupMemberCount", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().Group().GetMemberUsers(groupID)
if err != nil {
return nil, model.NewAppError("GetGroupMemberUsers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) {
members, err := a.Srv().Store().Group().GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err != nil {
return nil, 0, model.NewAppError("GetGroupMemberUsersPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, appErr := a.GetGroupMemberCount(groupID, viewRestrictions)
if appErr != nil {
return nil, 0, appErr
}
return a.sanitizeProfiles(members, false), int(count), nil
}
func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) {
return a.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInGroupPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(members, false), nil
}
func (a *App) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().UpsertMember(groupID, userID)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().DeleteMember(groupID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().GetGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
var notFoundErr *store.ErrNotFound
if err != nil && !errors.As(err, ¬FoundErr) {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// reject the syncable creation if the group isn't already associated to the parent team
if groupSyncable.Type == model.GroupSyncableTypeChannel {
channel, nErr := a.Srv().Store().Channel().Get(groupSyncable.SyncableId, true)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
var team *model.Team
team, nErr = a.Srv().Store().Team().Get(channel.TeamId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if team.IsGroupConstrained() {
var teamGroups []*model.GroupWithSchemeAdmin
teamGroups, err = a.Srv().Store().Group().GetGroupsByTeam(channel.TeamId, model.GroupSearchOpts{})
if err != nil {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var permittedGroup bool
for _, teamGroup := range teamGroups {
if teamGroup.Group.Id == groupSyncable.GroupId {
permittedGroup = true
break
}
}
if !permittedGroup {
return nil, model.NewAppError("UpsertGroupSyncable", "group_not_associated_to_synced_team", nil, "", http.StatusBadRequest)
}
} else {
_, appErr := a.UpsertGroupSyncable(model.NewGroupTeam(groupSyncable.GroupId, team.Id, groupSyncable.AutoAdd))
if appErr != nil {
return nil, appErr
}
}
}
if gs == nil {
gs, err = a.Srv().Store().Group().CreateGroupSyncable(groupSyncable)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "store.sql_channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
} else {
gs, err = a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
func (a *App) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
group, err := a.Srv().Store().Group().GetGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err != nil {
return nil, model.NewAppError("GetGroupSyncables", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if groupSyncable.DeleteAt == 0 {
// updating a *deleted* GroupSyncable, so no need to ensure the GroupTeam is present (as done in the upsert)
gs, err := a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return gs, nil
}
// do an upsert to ensure that there's an associated GroupTeam
gs, err := a.UpsertGroupSyncable(groupSyncable)
if err != nil {
return nil, err
}
return gs, nil
}
func (a *App) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().DeleteGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// if a GroupTeam is being deleted delete all associated GroupChannels
if gs.Type == model.GroupSyncableTypeTeam {
allGroupChannels, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(gs.GroupId, model.GroupSyncableTypeChannel)
if err != nil {
return nil, model.NewAppError("DeleteGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, groupChannel := range allGroupChannels {
_, err = a.Srv().Store().Group().DeleteGroupSyncable(groupChannel.GroupId, groupChannel.SyncableId, groupChannel.Type)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
// TeamMembersToAdd returns a slice of UserTeamIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given team.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be included; otherwise, they will be excluded.
func (a *App) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, *model.AppError) {
userTeams, err := a.Srv().Store().Group().TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err != nil {
return nil, model.NewAppError("TeamMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userTeams, nil
}
// ChannelMembersToAdd returns a slice of UserChannelIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given channel.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
func (a *App) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError) {
userChannels, err := a.Srv().Store().Group().ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err != nil {
return nil, model.NewAppError("ChannelMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userChannels, nil
}
func (a *App) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Group().TeamMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("TeamMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) {
channelMembers, err := a.Srv().Store().Group().ChannelMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("ChannelMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
func (a *App) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
func (a *App) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) {
groupsAssociatedByChannelId, err := a.Srv().Store().Group().GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err != nil {
return nil, model.NewAppError("GetGroupsAssociatedToChannelsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groupsAssociatedByChannelId, nil
}
func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroups(page, perPage, opts, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a team if the team
// were group-constrained with the given groups.
func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
func (a *App) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByIDs(groupIDs)
if err != nil {
return nil, model.NewAppError("GetGroupsByIDs", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a channel if the
// channel were group-constrained with the given groups.
func (a *App) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
// admins in the given syncable.
func (a *App) UserIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) {
groupIDs, err := a.Srv().Store().Group().AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
return false, model.NewAppError("UserIsInAdminRoleGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(groupIDs) == 0 {
return false, nil
}
return true, nil
}
func (a *App) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().UpsertMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMembers", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMembers", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().DeleteMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) publishGroupMemberEvent(eventName string, groupMember *model.GroupMember) *model.AppError {
messageWs := model.NewWebSocketEvent(eventName, "", "", groupMember.UserId, nil, "")
groupMemberJSON, jsonErr := json.Marshal(groupMember)
if jsonErr != nil {
return model.NewAppError("publishGroupMemberEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group_member", string(groupMemberJSON))
a.Publish(messageWs)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) NotifySelfHostedSignupProgress(progress string, userId string) {
// this is an event only the relevant admin should receive.
// If there is no progress, there is nothing to report.
// If there is no userId, we do not want to mistakenly broadcast to all users.
if progress == "" || userId == "" {
return
}
message := model.NewWebSocketEvent(model.WebsocketEventHostedCustomerSignupProgressUpdated, "", "", userId, nil, "")
message.Add("progress", progress)
a.Srv().Platform().Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"io"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
)
func checkImageResolutionLimit(w, h int, maxRes int64) error {
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
imageRes := int64(w) * int64(h)
if imageRes > maxRes {
return fmt.Errorf("image resolution is too high: %d, max allowed is %d", imageRes, maxRes)
}
return nil
}
func checkImageLimits(imageData io.Reader, maxRes int64) error {
w, h, err := imaging.GetDimensions(imageData)
if err != nil {
return fmt.Errorf("failed to get image dimensions: %w", err)
}
return checkImageResolutionLimit(w, h, maxRes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
_ "github.com/oov/psd"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
// DecoderOptions holds configuration options for an image decoder.
type DecoderOptions struct {
// The level of concurrency for the decoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *DecoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to decode images.
// This is safe to be used from multiple goroutines.
type Decoder struct {
sem chan struct{}
opts DecoderOptions
}
// NewDecoder creates and returns a new image decoder with the given options.
func NewDecoder(opts DecoderOptions) (*Decoder, error) {
var d Decoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating decoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
d.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
d.opts = opts
return &d, nil
}
// Decode decodes the given encoded data and returns the decoded image.
func (d *Decoder) Decode(rd io.Reader) (img image.Image, format string, err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() { <-d.sem }()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", fmt.Errorf("imaging: failed to decode image: %w", err)
}
return img, format, nil
}
// DecodeMemBounded works similarly to Decode but also returns a release function that
// must be called when access to the raw image is not needed anymore.
// This sets the raw image data pointer to nil in an attempt to help the GC to re-use the underlying data as soon as possible.
func (d *Decoder) DecodeMemBounded(rd io.Reader) (img image.Image, format string, releaseFunc func(), err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() {
if err != nil {
<-d.sem
}
}()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", nil, fmt.Errorf("imaging: failed to decode image: %w", err)
}
var once sync.Once
releaseFunc = func() {
if d.opts.ConcurrencyLevel == 0 {
return
}
once.Do(func() {
if img != nil {
releaseImageData(img)
}
<-d.sem
})
}
return img, format, releaseFunc, nil
}
// DecodeConfig returns the image config for the given data.
func (d *Decoder) DecodeConfig(rd io.Reader) (image.Config, string, error) {
img, format, err := image.DecodeConfig(rd)
if err != nil {
return image.Config{}, "", fmt.Errorf("imaging: failed to decode image config: %w", err)
}
return img, format, nil
}
// GetDimensions returns the dimensions for the given encoded image data.
func GetDimensions(imageData io.Reader) (int, int, error) {
cfg, _, err := image.DecodeConfig(imageData)
if seeker, ok := imageData.(io.ReadSeeker); ok {
defer seeker.Seek(0, 0)
}
return cfg.Width, cfg.Height, err
}
// This is only needed to try and simplify GC work.
func releaseImageData(img image.Image) {
switch raw := img.(type) {
case *image.Alpha:
raw.Pix = nil
case *image.Alpha16:
raw.Pix = nil
case *image.Gray:
raw.Pix = nil
case *image.Gray16:
raw.Pix = nil
case *image.NRGBA:
raw.Pix = nil
case *image.NRGBA64:
raw.Pix = nil
case *image.Paletted:
raw.Pix = nil
case *image.RGBA:
raw.Pix = nil
case *image.RGBA64:
raw.Pix = nil
default:
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
"io"
"image/jpeg"
"image/png"
)
// EncoderOptions holds configuration options for an image encoder.
type EncoderOptions struct {
// The level of concurrency for the encoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *EncoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to encode images.
// This is safe to be used from multiple goroutines.
type Encoder struct {
sem chan struct{}
opts EncoderOptions
pngEncoder *png.Encoder
}
// NewEncoder creates and returns a new image encoder with the given options.
func NewEncoder(opts EncoderOptions) (*Encoder, error) {
var e Encoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating encoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
e.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
e.opts = opts
e.pngEncoder = &png.Encoder{
CompressionLevel: png.BestCompression,
}
return &e, nil
}
// EncodeJPEG encodes the given image in JPEG format and writes the data to
// the passed writer.
func (e *Encoder) EncodeJPEG(wr io.Writer, img image.Image, quality int) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
var encOpts jpeg.Options
encOpts.Quality = quality
if err := jpeg.Encode(wr, img, &encOpts); err != nil {
return fmt.Errorf("imaging: failed to encode jpeg: %w", err)
}
return nil
}
// EncodePNG encodes the given image in PNG format and writes the data to
// the passed writer.
func (e *Encoder) EncodePNG(wr io.Writer, img image.Image) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
if err := e.pngEncoder.Encode(wr, img); err != nil {
return fmt.Errorf("imaging: failed to encode png: %w", err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"fmt"
"image"
"io"
"github.com/disintegration/imaging"
"github.com/rwcarlsen/goexif/exif"
)
const (
/*
EXIF Image Orientations
1 2 3 4 5 6 7 8
888888 888888 88 88 8888888888 88 88 8888888888
88 88 88 88 88 88 88 88 88 88 88 88
8888 8888 8888 8888 88 8888888888 8888888888 88
88 88 88 88
88 88 888888 888888
*/
Upright = iota + 1
UprightMirrored
UpsideDown
UpsideDownMirrored
RotatedCWMirrored
RotatedCCW
RotatedCCWMirrored
RotatedCW
)
// MakeImageUpright changes the orientation of the given image.
func MakeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
case UprightMirrored:
return imaging.FlipH(img)
case UpsideDown:
return imaging.Rotate180(img)
case UpsideDownMirrored:
return imaging.FlipV(img)
case RotatedCWMirrored:
return imaging.Transpose(img)
case RotatedCCW:
return imaging.Rotate270(img)
case RotatedCCWMirrored:
return imaging.Transverse(img)
case RotatedCW:
return imaging.Rotate90(img)
default:
return img
}
}
// GetImageOrientation reads the input data and returns the EXIF encoded
// image orientation.
func GetImageOrientation(input io.Reader) (int, error) {
exifData, err := exif.Decode(input)
if err != nil {
return Upright, fmt.Errorf("failed to decode exif data: %w", err)
}
tag, err := exifData.Get("Orientation")
if err != nil {
return Upright, fmt.Errorf("failed to get orientation field from exif data: %w", err)
}
orientation, err := tag.Int(0)
if err != nil {
return Upright, fmt.Errorf("failed to get value from exif tag: %w", err)
}
return orientation, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"github.com/disintegration/imaging"
)
// GeneratePreview generates the preview for the given image.
func GeneratePreview(img image.Image, width int) image.Image {
preview := img
w := img.Bounds().Dx()
if w > width {
preview = imaging.Resize(img, width, 0, imaging.Lanczos)
}
return preview
}
// GenerateThumbnail generates the thumbnail for the given image.
func GenerateThumbnail(img image.Image, width, height int) image.Image {
thumb := img
w := img.Bounds().Dx()
h := img.Bounds().Dy()
expectedRatio := float64(height) / float64(width)
if h > height || w > width {
ratio := float64(h) / float64(w)
if ratio < expectedRatio {
// we pre-calculate the thumbnail's width to make sure we are not upscaling.
targetWidth := int(float64(height) * float64(w) / float64(h))
if targetWidth <= w {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
}
} else {
// we pre-calculate the thumbnail's height to make sure we are not upscaling.
targetHeight := int(float64(width) * float64(h) / float64(w))
if targetHeight <= h {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
}
}
}
return thumb
}
// GenerateMiniPreviewImage generates the mini preview for the given image.
func GenerateMiniPreviewImage(img image.Image, w, h, q int) ([]byte, error) {
var buf bytes.Buffer
preview := imaging.Resize(img, w, h, imaging.Lanczos)
if err := jpeg.Encode(&buf, preview, &jpeg.Options{Quality: q}); err != nil {
return nil, fmt.Errorf("failed to encode image to JPEG format: %w", err)
}
return buf.Bytes(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"encoding/xml"
"fmt"
"io"
"strings"
"github.com/pkg/errors"
)
// SVGInfo holds information for a SVG image.
type SVGInfo struct {
Width int
Height int
}
// ParseSVG returns information for the given SVG input data.
func ParseSVG(svgReader io.Reader) (SVGInfo, error) {
svgInfo := SVGInfo{
Width: 0,
Height: 0,
}
decoder := xml.NewDecoder(svgReader)
for {
token, err := decoder.Token()
if err != nil {
return svgInfo, err
}
switch t := token.(type) {
case xml.StartElement:
for _, attr := range t.Attr {
if attr.Name.Local == "viewBox" {
values := strings.Fields(attr.Value)
if len(values) == 4 {
width := 0
_, widthErr := fmt.Sscan(values[2], &width)
height := 0
_, heightErr := fmt.Sscan(values[3], &height)
if widthErr != nil || heightErr != nil {
return svgInfo, err
}
svgInfo.Width = width
svgInfo.Height = height
return svgInfo, nil
}
}
if attr.Name.Local == "width" {
width := 0
_, err := fmt.Sscan(attr.Value, &width)
if err != nil {
return svgInfo, err
}
svgInfo.Width = width
}
if attr.Name.Local == "height" {
height := 0
_, err := fmt.Sscan(attr.Value, &height)
if err != nil {
return svgInfo, err
}
svgInfo.Height = height
}
}
if svgInfo.Width == 0 || svgInfo.Height == 0 {
return svgInfo, errors.New("unable to extract SVG dimensions")
}
return svgInfo, nil
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"image"
"image/color"
"github.com/disintegration/imaging"
)
type rawImg interface {
Set(x, y int, c color.Color)
Opaque() bool
}
func isFullyTransparent(c color.Color) bool {
// TODO: This can be optimized by checking the color type and
// only extract the needed alpha value.
_, _, _, a := c.RGBA()
return a == 0
}
// FillImageTransparency fills in-place all the fully transparent pixels of the
// input image with the given color.
func FillImageTransparency(img image.Image, c color.Color) {
var i rawImg
bounds := img.Bounds()
fillFunc := func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if isFullyTransparent(img.At(x, y)) {
i.Set(x, y, c)
}
}
}
}
switch raw := img.(type) {
case *image.Alpha:
i = raw
case *image.Alpha16:
i = raw
case *image.Gray:
i = raw
case *image.Gray16:
i = raw
case *image.NRGBA:
i = raw
col := color.NRGBAModel.Convert(c).(color.NRGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.NRGBA64:
i = raw
col := color.NRGBA64Model.Convert(c).(color.NRGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
case *image.Paletted:
i = raw
fillFunc = func() {
for i := range raw.Palette {
if isFullyTransparent(raw.Palette[i]) {
raw.Palette[i] = c
}
}
}
case *image.RGBA:
i = raw
col := color.RGBAModel.Convert(c).(color.RGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.RGBA64:
i = raw
col := color.RGBA64Model.Convert(c).(color.RGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
default:
return
}
if !i.Opaque() {
fillFunc()
}
}
// FillCenter creates an image with the specified dimensions and fills it with
// the centered and scaled source image.
func FillCenter(img image.Image, w, h int) *image.NRGBA {
return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type ReactionImportData = imports.ReactionImportData // part of the app interface
const (
importMultiplePostsThreshold = 1000
maxScanTokenSize = 16 * 1024 * 1024 // Need to set a higher limit than default because some customers cross the limit. See MM-22314
statusUpdateAfterLines = 8192
)
func stopOnError(c request.CTX, err imports.LineImportWorkerError) bool {
switch err.Error.Id {
case "api.file.upload_file.large_image.app_error":
c.Logger().Warn("Large image import error", mlog.Err(err.Error))
return false
case "app.import.validate_direct_channel_import_data.members_too_few.error", "app.import.validate_direct_channel_import_data.members_too_many.error":
c.Logger().Warn("Invalid direct channel import data", mlog.Err(err.Error))
return false
default:
return true
}
}
func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData, basePath string, filesMap map[string]*zip.File) error {
if files == nil {
return nil
}
var ok bool
for i, f := range *files {
if f.Path != nil {
path := filepath.Join(basePath, *f.Path)
*f.Path = path
if len(filesMap) > 0 {
if (*files)[i].Data, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
}
return nil
}
func processAttachments(c request.CTX, line *imports.LineImportData, basePath string, filesMap map[string]*zip.File) error {
var ok bool
switch line.Type {
case "post", "direct_post":
var replies []imports.ReplyImportData
if line.Type == "direct_post" {
if err := processAttachmentPaths(c, line.DirectPost.Attachments, basePath, filesMap); err != nil {
return err
}
if line.DirectPost.Replies != nil {
replies = *line.DirectPost.Replies
}
} else {
if err := processAttachmentPaths(c, line.Post.Attachments, basePath, filesMap); err != nil {
return err
}
if line.Post.Replies != nil {
replies = *line.Post.Replies
}
}
for _, reply := range replies {
if err := processAttachmentPaths(c, reply.Attachments, basePath, filesMap); err != nil {
return err
}
}
case "user":
if line.User.ProfileImage != nil {
path := filepath.Join(basePath, *line.User.ProfileImage)
*line.User.ProfileImage = path
if len(filesMap) > 0 {
if line.User.ProfileImageData, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
case "emoji":
if line.Emoji.Image != nil {
path := filepath.Join(basePath, *line.Emoji.Image)
*line.Emoji.Image = path
if len(filesMap) > 0 {
if line.Emoji.Data, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
}
return nil
}
func (a *App) bulkImportWorker(c request.CTX, dryRun bool, wg *sync.WaitGroup, lines <-chan imports.LineImportWorkerData, errors chan<- imports.LineImportWorkerError) {
workerID := model.NewId()
processedLines := uint64(0)
c.Logger().Info("Started new bulk import worker", mlog.String("bulk_import_worker_id", workerID))
defer func() {
wg.Done()
c.Logger().Info("Bulk import worker finished", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines))
}()
postLines := []imports.LineImportWorkerData{}
directPostLines := []imports.LineImportWorkerData{}
for line := range lines {
switch {
case line.LineImportData.Type == "post":
postLines = append(postLines, line)
if line.Post == nil {
errors <- imports.LineImportWorkerError{Error: model.NewAppError("BulkImport", "app.import.import_line.null_post.error", nil, "", http.StatusBadRequest), LineNumber: line.LineNumber}
}
if len(postLines) >= importMultiplePostsThreshold {
if errLine, err := a.importMultiplePostLines(c, postLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
postLines = []imports.LineImportWorkerData{}
}
case line.LineImportData.Type == "direct_post":
directPostLines = append(directPostLines, line)
if line.DirectPost == nil {
errors <- imports.LineImportWorkerError{Error: model.NewAppError("BulkImport", "app.import.import_line.null_direct_post.error", nil, "", http.StatusBadRequest), LineNumber: line.LineNumber}
}
if len(directPostLines) >= importMultiplePostsThreshold {
if errLine, err := a.importMultipleDirectPostLines(c, directPostLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
directPostLines = []imports.LineImportWorkerData{}
}
default:
if err := a.importLine(c, line.LineImportData, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: line.LineNumber}
}
}
processedLines++
if processedLines%statusUpdateAfterLines == 0 {
c.Logger().Info("Worker progress", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines))
}
}
if len(postLines) > 0 {
if errLine, err := a.importMultiplePostLines(c, postLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
}
if len(directPostLines) > 0 {
if errLine, err := a.importMultipleDirectPostLines(c, directPostLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
}
}
func (a *App) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, "")
}
func (a *App) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
}
// bulkImport will extract attachments from attachmentsReader if it is
// not nil. If it is nil, it will look for attachments on the
// filesystem in the locations specified by the JSONL file according
// to the older behavior
func (a *App) bulkImport(c request.CTX, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
scanner := bufio.NewScanner(jsonlReader)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, maxScanTokenSize)
lineNumber := 0
a.Srv().Store().LockToMaster()
defer a.Srv().Store().UnlockFromMaster()
errorsChan := make(chan imports.LineImportWorkerError, (2*workers)+1) // size chosen to ensure it never gets filled up completely.
var wg sync.WaitGroup
var linesChan chan imports.LineImportWorkerData
lastLineType := ""
var attachedFiles map[string]*zip.File
if attachmentsReader != nil {
attachedFiles = make(map[string]*zip.File, len(attachmentsReader.File))
for _, fi := range attachmentsReader.File {
attachedFiles[fi.Name] = fi
}
}
for scanner.Scan() {
lineNumber++
if lineNumber%statusUpdateAfterLines == 0 {
c.Logger().Info("Reader progress", mlog.Int("processed_lines", lineNumber))
}
var line imports.LineImportData
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, "", http.StatusBadRequest).Wrap(err), lineNumber
}
if err := processAttachments(c, &line, importPath, attachedFiles); err != nil {
c.Logger().Warn("Error while processing import attachments. Objects might be broken.", mlog.Err(err))
}
if lineNumber == 1 {
importDataFileVersion, appErr := processImportDataFileVersionLine(line)
if appErr != nil {
return appErr, lineNumber
}
if importDataFileVersion != 1 {
return model.NewAppError("BulkImport", "app.import.bulk_import.unsupported_version.error", nil, "", http.StatusBadRequest), lineNumber
}
lastLineType = line.Type
continue
}
if line.Type != lastLineType {
// Only clear the worker queue if is not the first data entry
if lineNumber != 2 {
c.Logger().Info(
"Finished parsing segment, waiting for workers to finish",
mlog.String("old_segment", lastLineType),
mlog.String("new_segment", line.Type),
)
// Changing type. Clear out the worker queue before continuing.
close(linesChan)
wg.Wait()
// Check no errors occurred while waiting for the queue to empty.
if len(errorsChan) != 0 {
err := <-errorsChan
if stopOnError(c, err) {
return err.Error, err.LineNumber
}
}
}
c.Logger().Info(
"Starting workers for new segment",
mlog.String("old_segment", lastLineType),
mlog.String("new_segment", line.Type),
mlog.Int("workers", workers),
)
// Set up the workers and channel for this type.
lastLineType = line.Type
linesChan = make(chan imports.LineImportWorkerData, workers)
for i := 0; i < workers; i++ {
wg.Add(1)
go a.bulkImportWorker(c, dryRun, &wg, linesChan, errorsChan)
}
}
select {
case linesChan <- imports.LineImportWorkerData{LineImportData: line, LineNumber: lineNumber}:
case err := <-errorsChan:
if stopOnError(c, err) {
close(linesChan)
wg.Wait()
return err.Error, err.LineNumber
}
}
}
// No more lines. Clear out the worker queue before continuing.
if linesChan != nil {
close(linesChan)
}
wg.Wait()
// Check no errors occurred while waiting for the queue to empty.
if len(errorsChan) != 0 {
err := <-errorsChan
if stopOnError(c, err) {
return err.Error, err.LineNumber
}
}
if err := scanner.Err(); err != nil {
return model.NewAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, "", http.StatusInternalServerError).Wrap(err), 0
}
return nil, 0
}
func processImportDataFileVersionLine(line imports.LineImportData) (int, *model.AppError) {
if line.Type != "version" || line.Version == nil {
return -1, model.NewAppError("BulkImport", "app.import.process_import_data_file_version_line.invalid_version.error", nil, "", http.StatusBadRequest)
}
return *line.Version, nil
}
func (a *App) importLine(c request.CTX, line imports.LineImportData, dryRun bool) *model.AppError {
switch {
case line.Type == "scheme":
if line.Scheme == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_scheme.error", nil, "", http.StatusBadRequest)
}
return a.importScheme(c, line.Scheme, dryRun)
case line.Type == "team":
if line.Team == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_team.error", nil, "", http.StatusBadRequest)
}
return a.importTeam(c, line.Team, dryRun)
case line.Type == "channel":
if line.Channel == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_channel.error", nil, "", http.StatusBadRequest)
}
return a.importChannel(c, line.Channel, dryRun)
case line.Type == "user":
if line.User == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_user.error", nil, "", http.StatusBadRequest)
}
return a.importUser(c, line.User, dryRun)
case line.Type == "direct_channel":
if line.DirectChannel == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_direct_channel.error", nil, "", http.StatusBadRequest)
}
return a.importDirectChannel(c, line.DirectChannel, dryRun)
case line.Type == "emoji":
if line.Emoji == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest)
}
return a.importEmoji(c, line.Emoji, dryRun)
default:
return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]any{"Type": line.Type}, "", http.StatusBadRequest)
}
}
func (a *App) ListImports() ([]string, *model.AppError) {
imports, appErr := a.ListDirectory(*a.Config().ImportSettings.Directory)
if appErr != nil {
return nil, appErr
}
results := make([]string, 0, len(imports))
for i := 0; i < len(imports); i++ {
filename := filepath.Base(imports[i])
if !strings.HasSuffix(filename, model.IncompleteUploadSuffix) {
results = append(results, filename)
}
}
return results, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/sha1"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// -- Bulk Import Functions --
// These functions import data directly into the database. Security and permission checks are bypassed but validity is
// still enforced.
func (a *App) importScheme(c request.CTX, data *imports.SchemeImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("schema_name", *data.Name))
}
c.Logger().Info("Validating schema", fields...)
if err := imports.ValidateSchemeImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing schema", fields...)
scheme, err := a.GetSchemeByName(*data.Name)
if err != nil {
scheme = new(model.Scheme)
} else if scheme.Scope != *data.Scope {
return model.NewAppError("BulkImport", "app.import.import_scheme.scope_change.error", map[string]any{"SchemeName": scheme.Name}, "", http.StatusBadRequest)
}
scheme.Name = *data.Name
scheme.DisplayName = *data.DisplayName
scheme.Scope = *data.Scope
if data.Description != nil {
scheme.Description = *data.Description
}
if scheme.Id == "" {
scheme, err = a.CreateScheme(scheme)
} else {
scheme, err = a.UpdateScheme(scheme)
}
if err != nil {
return err
}
if scheme.Scope == model.SchemeScopeTeam {
data.DefaultTeamAdminRole.Name = &scheme.DefaultTeamAdminRole
if err := a.importRole(c, data.DefaultTeamAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultTeamUserRole.Name = &scheme.DefaultTeamUserRole
if err := a.importRole(c, data.DefaultTeamUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultTeamGuestRole == nil {
data.DefaultTeamGuestRole = &imports.RoleImportData{
DisplayName: model.NewString("Team Guest Role for Scheme"),
}
}
data.DefaultTeamGuestRole.Name = &scheme.DefaultTeamGuestRole
if err := a.importRole(c, data.DefaultTeamGuestRole, dryRun, true); err != nil {
return err
}
}
if scheme.Scope == model.SchemeScopeTeam || scheme.Scope == model.SchemeScopeChannel {
data.DefaultChannelAdminRole.Name = &scheme.DefaultChannelAdminRole
if err := a.importRole(c, data.DefaultChannelAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultChannelUserRole.Name = &scheme.DefaultChannelUserRole
if err := a.importRole(c, data.DefaultChannelUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultChannelGuestRole == nil {
data.DefaultChannelGuestRole = &imports.RoleImportData{
DisplayName: model.NewString("Channel Guest Role for Scheme"),
}
}
data.DefaultChannelGuestRole.Name = &scheme.DefaultChannelGuestRole
if err := a.importRole(c, data.DefaultChannelGuestRole, dryRun, true); err != nil {
return err
}
}
return nil
}
func (a *App) importRole(c request.CTX, data *imports.RoleImportData, dryRun bool, isSchemeRole bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("role_name", *data.Name))
}
if !isSchemeRole {
c.Logger().Info("Validating role", fields...)
if err := imports.ValidateRoleImportData(data); err != nil {
return err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing role", fields...)
role, err := a.GetRoleByName(context.Background(), *data.Name)
if err != nil {
role = new(model.Role)
}
role.Name = *data.Name
if data.DisplayName != nil {
role.DisplayName = *data.DisplayName
}
if data.Description != nil {
role.Description = *data.Description
}
if data.Permissions != nil {
role.Permissions = *data.Permissions
}
if isSchemeRole {
role.SchemeManaged = true
} else {
role.SchemeManaged = false
}
if role.Id == "" {
_, err = a.CreateRole(role)
} else {
_, err = a.UpdateRole(role)
}
return err
}
func (a *App) importTeam(c request.CTX, data *imports.TeamImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("team_name", *data.Name))
}
c.Logger().Info("Validating team", fields...)
if err := imports.ValidateTeamImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing team", fields...)
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(*data.Name)
if err != nil {
team = &model.Team{}
}
team.Name = *data.Name
team.DisplayName = *data.DisplayName
team.Type = *data.Type
if data.Description != nil {
team.Description = *data.Description
}
if data.AllowOpenInvite != nil {
team.AllowOpenInvite = *data.AllowOpenInvite
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SchemeScopeTeam {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
team.SchemeId = &scheme.Id
}
if team.Id == "" {
if _, err := a.CreateTeam(c, team); err != nil {
return err
}
} else {
if _, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{Imported: true}); err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("BulkImport", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return model.NewAppError("BulkImport", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("BulkImport", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
return nil
}
func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("channel_name", *data.Name))
}
c.Logger().Info("Validating channel", fields...)
if err := imports.ValidateChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing channel", fields...)
team, err := a.Srv().Store().Team().GetByName(*data.Team)
if err != nil {
return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]any{"TeamName": *data.Team}, "", http.StatusBadRequest).Wrap(err)
}
var channel *model.Channel
if result, err := a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, *data.Name, true); err == nil {
channel = result
} else {
channel = &model.Channel{}
}
channel.TeamId = team.Id
channel.Name = *data.Name
channel.DisplayName = *data.DisplayName
channel.Type = *data.Type
if data.Header != nil {
channel.Header = *data.Header
}
if data.Purpose != nil {
channel.Purpose = *data.Purpose
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SchemeScopeChannel {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
channel.SchemeId = &scheme.Id
}
if channel.Id == "" {
if _, err := a.CreateChannel(c, channel, false); err != nil {
return err
}
} else {
if _, err := a.UpdateChannel(c, channel); err != nil {
return err
}
}
return nil
}
func (a *App) importUser(c request.CTX, data *imports.UserImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Username != nil {
fields = append(fields, mlog.String("user_name", *data.Username))
}
c.Logger().Info("Validating user", fields...)
if err := imports.ValidateUserImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing user", fields...)
// We want to avoid database writes if nothing has changed.
hasUserChanged := false
hasNotifyPropsChanged := false
hasUserRolesChanged := false
hasUserAuthDataChanged := false
hasUserEmailVerifiedChanged := false
var user *model.User
var nErr error
user, nErr = a.Srv().Store().User().GetByUsername(*data.Username)
if nErr != nil {
user = &model.User{}
user.MakeNonNil()
user.SetDefaultNotifications()
hasUserChanged = true
}
user.Username = *data.Username
if user.Email != *data.Email {
hasUserChanged = true
hasUserEmailVerifiedChanged = true // Changing the email resets email verified to false by default.
user.Email = *data.Email
user.Email = strings.ToLower(user.Email)
}
var password string
var authService string
var authData *string
if data.AuthService != nil {
if user.AuthService != *data.AuthService {
hasUserAuthDataChanged = true
}
authService = *data.AuthService
}
// AuthData and Password are mutually exclusive.
if data.AuthData != nil {
if user.AuthData == nil || *user.AuthData != *data.AuthData {
hasUserAuthDataChanged = true
}
authData = data.AuthData
password = ""
} else if data.Password != nil {
password = *data.Password
authData = nil
} else {
var err error
// If no AuthData or Password is specified, we must generate a password.
password, err = generatePassword(*a.Config().PasswordSettings.MinimumLength)
if err != nil {
return model.NewAppError("importUser", "app.import.generate_password.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
authData = nil
}
user.Password = password
user.AuthService = authService
user.AuthData = authData
// Automatically assume all emails are verified.
emailVerified := true
if user.EmailVerified != emailVerified {
user.EmailVerified = emailVerified
hasUserEmailVerifiedChanged = true
}
if data.Nickname != nil {
if user.Nickname != *data.Nickname {
user.Nickname = *data.Nickname
hasUserChanged = true
}
}
if data.FirstName != nil {
if user.FirstName != *data.FirstName {
user.FirstName = *data.FirstName
hasUserChanged = true
}
}
if data.LastName != nil {
if user.LastName != *data.LastName {
user.LastName = *data.LastName
hasUserChanged = true
}
}
if data.Position != nil {
if user.Position != *data.Position {
user.Position = *data.Position
hasUserChanged = true
}
}
if data.Locale != nil {
if user.Locale != *data.Locale {
user.Locale = *data.Locale
hasUserChanged = true
}
} else {
if user.Locale != *a.Config().LocalizationSettings.DefaultClientLocale {
user.Locale = *a.Config().LocalizationSettings.DefaultClientLocale
hasUserChanged = true
}
}
if data.DeleteAt != nil {
if user.DeleteAt != *data.DeleteAt {
user.DeleteAt = *data.DeleteAt
hasUserChanged = true
}
}
var roles string
if data.Roles != nil {
if user.Roles != *data.Roles {
roles = *data.Roles
hasUserRolesChanged = true
}
} else if user.Roles == "" {
// Set SYSTEM_USER roles on newly created users by default.
if user.Roles != model.SystemUserRoleId {
roles = model.SystemUserRoleId
hasUserRolesChanged = true
}
}
user.Roles = roles
if data.NotifyProps != nil {
if data.NotifyProps.Desktop != nil {
if value, ok := user.NotifyProps[model.DesktopNotifyProp]; !ok || value != *data.NotifyProps.Desktop {
user.AddNotifyProp(model.DesktopNotifyProp, *data.NotifyProps.Desktop)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.DesktopSound != nil {
if value, ok := user.NotifyProps[model.DesktopSoundNotifyProp]; !ok || value != *data.NotifyProps.DesktopSound {
user.AddNotifyProp(model.DesktopSoundNotifyProp, *data.NotifyProps.DesktopSound)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Email != nil {
if value, ok := user.NotifyProps[model.EmailNotifyProp]; !ok || value != *data.NotifyProps.Email {
user.AddNotifyProp(model.EmailNotifyProp, *data.NotifyProps.Email)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Mobile != nil {
if value, ok := user.NotifyProps[model.PushNotifyProp]; !ok || value != *data.NotifyProps.Mobile {
user.AddNotifyProp(model.PushNotifyProp, *data.NotifyProps.Mobile)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MobilePushStatus != nil {
if value, ok := user.NotifyProps[model.PushStatusNotifyProp]; !ok || value != *data.NotifyProps.MobilePushStatus {
user.AddNotifyProp(model.PushStatusNotifyProp, *data.NotifyProps.MobilePushStatus)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.ChannelTrigger != nil {
if value, ok := user.NotifyProps[model.ChannelMentionsNotifyProp]; !ok || value != *data.NotifyProps.ChannelTrigger {
user.AddNotifyProp(model.ChannelMentionsNotifyProp, *data.NotifyProps.ChannelTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.CommentsTrigger != nil {
if value, ok := user.NotifyProps[model.CommentsNotifyProp]; !ok || value != *data.NotifyProps.CommentsTrigger {
user.AddNotifyProp(model.CommentsNotifyProp, *data.NotifyProps.CommentsTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MentionKeys != nil {
if value, ok := user.NotifyProps[model.MentionKeysNotifyProp]; !ok || value != *data.NotifyProps.MentionKeys {
user.AddNotifyProp(model.MentionKeysNotifyProp, *data.NotifyProps.MentionKeys)
hasNotifyPropsChanged = true
}
} else {
user.UpdateMentionKeysFromUsername("")
}
}
var savedUser *model.User
var err error
if user.Id == "" {
if savedUser, err = a.ch.srv.userService.CreateUser(user, users.UserCreateOptions{FromImport: true}); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.Is(err, users.AcceptedDomainError):
return model.NewAppError("importUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, users.UserStoreIsEmptyError):
return model.NewAppError("importUser", "app.user.store_is_empty.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.As(err, &invErr):
switch invErr.Field {
case "email":
return model.NewAppError("importUser", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case "username":
return model.NewAppError("importUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("importUser", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
default:
return model.NewAppError("importUser", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
pref := model.Preference{UserId: savedUser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: savedUser.Id, Value: "0"}
if err := a.Srv().Store().Preference().Save(model.Preferences{pref}); err != nil {
c.Logger().Warn("Encountered error saving tutorial preference", mlog.Err(err))
}
} else {
var appErr *model.AppError
if hasUserChanged {
if savedUser, appErr = a.UpdateUser(c, user, false); appErr != nil {
return appErr
}
}
if hasUserRolesChanged {
if savedUser, appErr = a.UpdateUserRoles(c, user.Id, roles, false); appErr != nil {
return appErr
}
}
if hasNotifyPropsChanged {
if appErr = a.updateUserNotifyProps(user.Id, user.NotifyProps); appErr != nil {
return appErr
}
if savedUser, appErr = a.GetUser(user.Id); appErr != nil {
return appErr
}
}
if password != "" {
if appErr = a.UpdatePassword(user, password); appErr != nil {
return appErr
}
} else {
if hasUserAuthDataChanged {
if _, nErr := a.Srv().Store().User().UpdateAuthData(user.Id, authService, authData, user.Email, false); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
}
if emailVerified {
if hasUserEmailVerifiedChanged {
if err := a.VerifyUserEmail(user.Id, user.Email); err != nil {
return err
}
}
}
}
if savedUser == nil {
savedUser = user
}
if data.ProfileImage != nil {
var file io.ReadCloser
var err error
if data.ProfileImageData != nil {
file, err = data.ProfileImageData.Open()
} else {
file, err = os.Open(*data.ProfileImage)
}
if err != nil {
c.Logger().Warn("Unable to open the profile image.", mlog.Err(err))
} else {
defer file.Close()
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
}
if err := a.SetProfileImageFromFile(c, savedUser.Id, file); err != nil {
c.Logger().Warn("Unable to set the profile image from a file.", mlog.Err(err))
}
}
}
// Preferences.
var preferences model.Preferences
if data.Theme != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryTheme,
Name: "",
Value: *data.Theme,
})
}
if data.UseMilitaryTime != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
Value: *data.UseMilitaryTime,
})
}
if data.CollapsePreviews != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
Value: *data.CollapsePreviews,
})
}
if data.MessageDisplay != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
Value: *data.MessageDisplay,
})
}
if data.CollapseConsecutive != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseConsecutive,
Value: *data.CollapseConsecutive,
})
}
if data.ColorizeUsernames != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameColorizeUsernames,
Value: *data.ColorizeUsernames,
})
}
if data.ChannelDisplayMode != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
Value: *data.ChannelDisplayMode,
})
}
if data.TutorialStep != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryTutorialSteps,
Name: savedUser.Id,
Value: *data.TutorialStep,
})
}
if data.UseMarkdownPreview != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "feature_enabled_markdown_preview",
Value: *data.UseMarkdownPreview,
})
}
if data.UseFormatting != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "formatting",
Value: *data.UseFormatting,
})
}
if data.ShowUnreadSection != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: "show_unread_section",
Value: *data.ShowUnreadSection,
})
}
if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" {
var intervalSeconds string
if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" {
intervalSeconds = "0"
} else {
switch *data.EmailInterval {
case model.PreferenceEmailIntervalImmediately:
intervalSeconds = model.PreferenceEmailIntervalNoBatchingSeconds
case model.PreferenceEmailIntervalFifteen:
intervalSeconds = model.PreferenceEmailIntervalFifteenAsSeconds
case model.PreferenceEmailIntervalHour:
intervalSeconds = model.PreferenceEmailIntervalHourAsSeconds
}
}
if intervalSeconds != "" {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
Value: intervalSeconds,
})
}
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return a.importUserTeams(c, savedUser, data.Teams)
}
func (a *App) importUserTeams(c request.CTX, user *model.User, data *[]imports.UserTeamImportData) *model.AppError {
if data == nil {
return nil
}
teamNames := []string{}
for _, tdata := range *data {
teamNames = append(teamNames, *tdata.Name)
}
allTeams, err := a.getTeamsByNames(teamNames)
if err != nil {
return err
}
var (
teamThemePreferencesByID = map[string]model.Preferences{}
channels = map[string][]imports.UserChannelImportData{}
teamsByID = map[string]*model.Team{}
teamMemberByTeamID = map[string]*model.TeamMember{}
newTeamMembers = []*model.TeamMember{}
oldTeamMembers = []*model.TeamMember{}
rolesByTeamId = map[string]string{}
isGuestByTeamId = map[string]bool{}
isUserByTeamId = map[string]bool{}
isAdminByTeamId = map[string]bool{}
)
existingMemberships, nErr := a.Srv().Store().Team().GetTeamsForUser(context.Background(), user.Id, "", true)
if nErr != nil {
return model.NewAppError("importUserTeams", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
existingMembershipsByTeamId := map[string]*model.TeamMember{}
for _, teamMembership := range existingMemberships {
existingMembershipsByTeamId[teamMembership.TeamId] = teamMembership
}
for _, tdata := range *data {
team := allTeams[strings.ToLower(*tdata.Name)]
// Team-specific theme Preferences.
if tdata.Theme != nil {
teamThemePreferencesByID[team.Id] = append(teamThemePreferencesByID[team.Id], model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryTheme,
Name: team.Id,
Value: *tdata.Theme,
})
}
isGuestByTeamId[team.Id] = false
isUserByTeamId[team.Id] = true
isAdminByTeamId[team.Id] = false
if tdata.Roles == nil {
isUserByTeamId[team.Id] = true
} else {
rawRoles := *tdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.TeamGuestRoleId {
isGuestByTeamId[team.Id] = true
isUserByTeamId[team.Id] = false
} else if role == model.TeamUserRoleId {
isUserByTeamId[team.Id] = true
} else if role == model.TeamAdminRoleId {
isAdminByTeamId[team.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByTeamId[team.Id] = strings.Join(explicitRoles, " ")
}
member := &model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: team.Email == user.Email && !user.IsGuest(),
CreateAt: model.GetMillis(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if tdata.Channels != nil {
channels[team.Id] = append(channels[team.Id], *tdata.Channels...)
}
if !user.IsGuest() {
channels[team.Id] = append(channels[team.Id], imports.UserChannelImportData{Name: model.NewString(model.DefaultChannelName)})
}
teamsByID[team.Id] = team
teamMemberByTeamID[team.Id] = member
if _, ok := existingMembershipsByTeamId[team.Id]; !ok {
newTeamMembers = append(newTeamMembers, member)
} else {
oldTeamMembers = append(oldTeamMembers, member)
}
}
oldMembers, nErr := a.Srv().Store().Team().UpdateMultipleMembers(oldTeamMembers)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserTeams", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
newMembers := []*model.TeamMember{}
if len(newTeamMembers) > 0 {
var nErr error
newMembers, nErr = a.Srv().Store().Team().SaveMultipleMembers(newTeamMembers, *a.Config().TeamSettings.MaxUsersPerTeam)
if nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &limitExceededErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByTeamId[member.TeamId] {
if _, err = a.UpdateTeamMemberRoles(member.TeamId, user.Id, rolesByTeamId[member.TeamId]); err != nil {
return err
}
}
a.UpdateTeamMemberSchemeRoles(member.TeamId, user.Id, isGuestByTeamId[member.TeamId], isUserByTeamId[member.TeamId], isAdminByTeamId[member.TeamId])
}
for _, team := range allTeams {
if len(teamThemePreferencesByID[team.Id]) > 0 {
pref := teamThemePreferencesByID[team.Id]
if err := a.Srv().Store().Preference().Save(pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
channelsToImport := channels[team.Id]
if err := a.importUserChannels(c, user, team, &channelsToImport); err != nil {
return err
}
}
return nil
}
func (a *App) importUserChannels(c request.CTX, user *model.User, team *model.Team, data *[]imports.UserChannelImportData) *model.AppError {
if data == nil {
return nil
}
channelNames := []string{}
for _, tdata := range *data {
channelNames = append(channelNames, *tdata.Name)
}
allChannels, err := a.getChannelsByNames(channelNames, team.Id)
if err != nil {
return err
}
var (
channelsByID = map[string]*model.Channel{}
channelMemberByChannelID = map[string]*model.ChannelMember{}
newChannelMembers = []*model.ChannelMember{}
oldChannelMembers = []*model.ChannelMember{}
rolesByChannelId = map[string]string{}
channelPreferencesByID = map[string]model.Preferences{}
isGuestByChannelId = map[string]bool{}
isUserByChannelId = map[string]bool{}
isAdminByChannelId = map[string]bool{}
)
existingMemberships, nErr := a.Srv().Store().Channel().GetMembersForUser(team.Id, user.Id)
if nErr != nil {
return model.NewAppError("importUserChannels", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
existingMembershipsByChannelId := map[string]model.ChannelMember{}
for _, channelMembership := range existingMemberships {
existingMembershipsByChannelId[channelMembership.ChannelId] = channelMembership
}
for _, cdata := range *data {
channel, ok := allChannels[strings.ToLower(*cdata.Name)]
if !ok {
return model.NewAppError("BulkImport", "app.import.import_user_channels.channel_not_found.error", nil, "", http.StatusInternalServerError)
}
if _, ok = channelsByID[channel.Id]; ok && *cdata.Name == model.DefaultChannelName {
// town-square membership was in the import and added by the importer (skip the added by the importer)
continue
}
isGuestByChannelId[channel.Id] = false
isUserByChannelId[channel.Id] = true
isAdminByChannelId[channel.Id] = false
if cdata.Roles != nil {
rawRoles := *cdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.ChannelGuestRoleId {
isGuestByChannelId[channel.Id] = true
isUserByChannelId[channel.Id] = false
} else if role == model.ChannelUserRoleId {
isUserByChannelId[channel.Id] = true
} else if role == model.ChannelAdminRoleId {
isAdminByChannelId[channel.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByChannelId[channel.Id] = strings.Join(explicitRoles, " ")
}
if cdata.Favorite != nil && *cdata.Favorite {
channelPreferencesByID[channel.Id] = append(channelPreferencesByID[channel.Id], model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
})
}
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: false,
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if cdata.MentionCount != nil && cdata.MentionCountRoot != nil {
member.MentionCount = *cdata.MentionCount
member.MentionCountRoot = *cdata.MentionCountRoot
}
if cdata.UrgentMentionCount != nil {
member.UrgentMentionCount = *cdata.UrgentMentionCount
}
if cdata.MsgCount != nil && cdata.MsgCountRoot != nil {
member.MsgCount = *cdata.MsgCount
member.MsgCountRoot = *cdata.MsgCountRoot
}
if cdata.LastViewedAt != nil {
member.LastViewedAt = *cdata.LastViewedAt
}
if cdata.NotifyProps != nil {
if cdata.NotifyProps.Desktop != nil {
member.NotifyProps[model.DesktopNotifyProp] = *cdata.NotifyProps.Desktop
}
if cdata.NotifyProps.Mobile != nil {
member.NotifyProps[model.PushNotifyProp] = *cdata.NotifyProps.Mobile
}
if cdata.NotifyProps.MarkUnread != nil {
member.NotifyProps[model.MarkUnreadNotifyProp] = *cdata.NotifyProps.MarkUnread
}
}
channelsByID[channel.Id] = channel
channelMemberByChannelID[channel.Id] = member
if _, ok := existingMembershipsByChannelId[channel.Id]; !ok {
newChannelMembers = append(newChannelMembers, member)
} else {
oldChannelMembers = append(oldChannelMembers, member)
}
}
oldMembers, nErr := a.Srv().Store().Channel().UpdateMultipleMembers(oldChannelMembers)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
case errors.As(nErr, &nfErr):
return model.NewAppError("importUserChannels", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("importUserChannels", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
newMembers := []*model.ChannelMember{}
if len(newChannelMembers) > 0 {
newMembers, nErr = a.Srv().Store().Channel().SaveMultipleMembers(newChannelMembers)
if nErr != nil {
var cErr *store.ErrConflict
var appErr *model.AppError
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return model.NewAppError("importUserChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByChannelId[member.ChannelId] {
if _, err = a.UpdateChannelMemberRoles(c, member.ChannelId, user.Id, rolesByChannelId[member.ChannelId]); err != nil {
return err
}
}
a.UpdateChannelMemberSchemeRoles(c, member.ChannelId, user.Id, isGuestByChannelId[member.ChannelId], isUserByChannelId[member.ChannelId], isAdminByChannelId[member.ChannelId])
}
for _, channel := range allChannels {
if len(channelPreferencesByID[channel.Id]) > 0 {
pref := channelPreferencesByID[channel.Id]
if err := a.Srv().Store().Preference().Save(pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_channels.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
return nil
}
func (a *App) importReaction(data *imports.ReactionImportData, post *model.Post) *model.AppError {
if err := imports.ValidateReactionImportData(data, post.CreateAt); err != nil {
return err
}
var user *model.User
var nErr error
if user, nErr = a.Srv().Store().User().GetByUsername(*data.User); nErr != nil {
return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]any{"Username": data.User}, "", http.StatusBadRequest).Wrap(nErr)
}
reaction := &model.Reaction{
UserId: user.Id,
PostId: post.Id,
EmojiName: *data.EmojiName,
CreateAt: *data.CreateAt,
}
if _, nErr = a.Srv().Store().Reaction().Save(reaction); nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importReaction", "app.reaction.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (a *App) importReplies(c request.CTX, data []imports.ReplyImportData, post *model.Post, teamID string) *model.AppError {
var err *model.AppError
usernames := []string{}
for _, replyData := range data {
replyData := replyData
if err = imports.ValidateReplyImportData(&replyData, post.CreateAt, a.MaxPostSize()); err != nil {
return err
}
usernames = append(usernames, *replyData.User)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForOverwriteList = []*model.Post{}
)
for _, replyData := range data {
replyData := replyData
user := users[strings.ToLower(*replyData.User)]
// Check if this post already exists.
replies, nErr := a.Srv().Store().Post().GetPostsCreatedAt(post.ChannelId, *replyData.CreateAt)
if nErr != nil {
return model.NewAppError("importReplies", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var reply *model.Post
for _, r := range replies {
if r.Message == *replyData.Message && r.RootId == post.Id {
reply = r
break
}
}
if reply == nil {
reply = &model.Post{}
}
reply.UserId = user.Id
reply.ChannelId = post.ChannelId
reply.RootId = post.Id
reply.Message = *replyData.Message
reply.CreateAt = *replyData.CreateAt
if reply.CreateAt < post.CreateAt {
c.Logger().Warn("Reply CreateAt is before parent post CreateAt, setting it to parent post CreateAt", mlog.Int64("reply_create_at", reply.CreateAt), mlog.Int64("parent_create_at", post.CreateAt))
reply.CreateAt = post.CreateAt
}
if replyData.Type != nil {
reply.Type = *replyData.Type
}
if replyData.EditAt != nil {
reply.EditAt = *replyData.EditAt
}
fileIDs := a.uploadAttachments(c, replyData.Attachments, reply, teamID)
for _, fileID := range reply.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
reply.FileIds = make([]string, 0)
for fileID := range fileIDs {
reply.FileIds = append(reply.FileIds, fileID)
}
if reply.Id == "" {
postsForCreateList = append(postsForCreateList, reply)
} else {
postsForOverwriteList = append(postsForOverwriteList, reply)
}
postsWithData = append(postsWithData, postAndData{post: reply, replyData: &replyData})
}
if len(postsForCreateList) > 0 {
if _, _, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("importReplies", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("importReplies", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if _, _, nErr := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); nErr != nil {
return model.NewAppError("importReplies", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, postWithData := range postsWithData {
a.updateFileInfoWithPostId(postWithData.post)
}
return nil
}
func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData, post *model.Post, teamID string) (*model.FileInfo, *model.AppError) {
var (
name string
file io.Reader
)
if data.Data != nil {
zipFile, err := data.Data.Open()
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest).Wrap(err)
}
defer zipFile.Close()
name = data.Data.Name
file = zipFile.(io.Reader)
c.Logger().Info("Preparing file upload from ZIP", mlog.String("file_name", name), mlog.Uint64("file_size", data.Data.UncompressedSize64))
} else {
realFile, err := os.Open(*data.Path)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest).Wrap(err)
}
defer realFile.Close()
name = realFile.Name()
file = realFile
fields := []logr.Field{mlog.String("file_name", name)}
if info, err := realFile.Stat(); err != nil {
fields = append(fields, mlog.Int64("file_size", info.Size()))
}
c.Logger().Info("Preparing file upload from file system", fields...)
}
timestamp := utils.TimeFromMillis(post.CreateAt)
fileData, err := io.ReadAll(file)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.read_file_data.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
// Go over existing files in the post and see if there already exists a file with the same name, size and hash. If so - skip it
if post.Id != "" {
oldFiles, err := a.getFileInfosForPostIgnoreCloudLimit(post.Id, true, false)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
for _, oldFile := range oldFiles {
if oldFile.Name != path.Base(name) || oldFile.Size != int64(len(fileData)) {
continue
}
// check sha1
newHash := sha1.Sum(fileData)
oldFileData, err := a.getFileIgnoreCloudLimit(oldFile.Id)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
oldHash := sha1.Sum(oldFileData)
if bytes.Equal(oldHash[:], newHash[:]) {
mlog.Info("Skipping uploading of file because name already exists", mlog.Any("file_name", name))
return oldFile, nil
}
}
}
mlog.Info("Uploading file with name", mlog.String("file_name", name))
fileInfo, appErr := a.DoUploadFile(c, timestamp, teamID, post.ChannelId, post.UserId, name, fileData)
if appErr != nil {
mlog.Error("Failed to upload file", mlog.Err(appErr), mlog.String("file_name", name))
return nil, appErr
}
if fileInfo.IsImage() && !fileInfo.IsSvg() {
a.HandleImages([]string{fileInfo.PreviewPath}, []string{fileInfo.ThumbnailPath}, [][]byte{fileData})
}
return fileInfo, nil
}
type postAndData struct {
post *model.Post
postData *imports.PostImportData
directPostData *imports.DirectPostImportData
replyData *imports.ReplyImportData
team *model.Team
lineNumber int
}
func (a *App) getUsersByUsernames(usernames []string) (map[string]*model.User, *model.AppError) {
uniqueUsernames := utils.RemoveDuplicatesFromStringArray(usernames)
allUsers, err := a.Srv().Store().User().GetProfilesByUsernames(uniqueUsernames, nil)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(allUsers) != len(uniqueUsernames) {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest)
}
users := make(map[string]*model.User)
for _, user := range allUsers {
users[strings.ToLower(user.Username)] = user
}
return users, nil
}
func (a *App) getTeamsByNames(names []string) (map[string]*model.Team, *model.AppError) {
allTeams, err := a.Srv().Store().Team().GetByNames(names)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
teams := make(map[string]*model.Team)
for _, team := range allTeams {
teams[strings.ToLower(team.Name)] = team
}
return teams, nil
}
func (a *App) getChannelsByNames(names []string, teamID string) (map[string]*model.Channel, *model.AppError) {
allChannels, err := a.Srv().Store().Channel().GetByNames(teamID, names, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channels := make(map[string]*model.Channel)
for _, channel := range allChannels {
channels[strings.ToLower(channel.Name)] = channel
}
return channels, nil
}
// getChannelsForPosts returns map[teamName]map[channelName]*model.Channel
func (a *App) getChannelsForPosts(teams map[string]*model.Team, data []*imports.PostImportData) (map[string]map[string]*model.Channel, *model.AppError) {
teamChannels := make(map[string]map[string]*model.Channel)
for _, postData := range data {
teamName := strings.ToLower(*postData.Team)
if _, ok := teamChannels[teamName]; !ok {
teamChannels[teamName] = make(map[string]*model.Channel)
}
channelName := strings.ToLower(*postData.Channel)
if channel, ok := teamChannels[teamName][channelName]; !ok || channel == nil {
var err error
channel, err = a.Srv().Store().Channel().GetByName(teams[teamName].Id, *postData.Channel, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.import_post.channel_not_found.error", map[string]any{"ChannelName": *postData.Channel}, "", http.StatusBadRequest).Wrap(err)
}
teamChannels[teamName][channelName] = channel
}
}
return teamChannels, nil
}
// getPostStrID returns a string ID composed of several post fields to
// uniquely identify a post before it's imported, so it has no ID yet
func getPostStrID(post *model.Post) string {
return fmt.Sprintf("%d%s%s", post.CreateAt, post.ChannelId, post.Message)
}
// importMultiplePostLines will return an error and the line that
// caused it whenever possible
func (a *App) importMultiplePostLines(c request.CTX, lines []imports.LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
c.Logger().Info("Validating post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber))
for _, line := range lines {
if err := imports.ValidatePostImportData(line.Post, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
c.Logger().Info("Importing post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber))
usernames := []string{}
teamNames := make([]string, len(lines))
postsData := make([]*imports.PostImportData, len(lines))
for i, line := range lines {
usernames = append(usernames, *line.Post.User)
if line.Post.FlaggedBy != nil {
usernames = append(usernames, *line.Post.FlaggedBy...)
}
teamNames[i] = *line.Post.Team
postsData[i] = line.Post
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
teams, err := a.getTeamsByNames(teamNames)
if err != nil {
return 0, err
}
channels, err := a.getChannelsForPosts(teams, postsData)
if err != nil {
return 0, err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
)
for _, line := range lines {
team := teams[strings.ToLower(*line.Post.Team)]
channel := channels[*line.Post.Team][*line.Post.Channel]
user := users[strings.ToLower(*line.Post.User)]
// Check if this post already exists.
posts, nErr := a.Srv().Store().Post().GetPostsCreatedAt(channel.Id, *line.Post.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("importMultiplePostLines", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.Post.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.Post.Message
post.UserId = user.Id
post.CreateAt = *line.Post.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.Post.Type != nil {
post.Type = *line.Post.Type
}
if line.Post.EditAt != nil {
post.EditAt = *line.Post.EditAt
}
if line.Post.Props != nil {
post.Props = *line.Post.Props
}
if line.Post.IsPinned != nil {
post.IsPinned = *line.Post.IsPinned
}
fileIDs := a.uploadAttachments(c, line.Post.Attachments, post, team.Id)
for _, fileID := range post.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIDs {
post.FileIds = append(post.FileIds, fileID)
}
if post.Id == "" {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, postData: line.Post, team: team, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, nErr := a.Srv().Store().Post().SaveMultiple(postsForCreateList); nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(nErr, &appErr):
retErr = appErr
case errors.As(nErr, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, postWithData := range postsWithData {
postWithData := postWithData
if postWithData.postData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.postData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if postWithData.postData.Reactions != nil {
for _, reaction := range *postWithData.postData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.postData.Replies != nil && len(*postWithData.postData.Replies) > 0 {
err := a.importReplies(c, *postWithData.postData.Replies, postWithData.post, postWithData.team.Id)
if err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
// uploadAttachments imports new attachments and returns current attachments of the post as a map
func (a *App) uploadAttachments(c request.CTX, attachments *[]imports.AttachmentImportData, post *model.Post, teamID string) map[string]bool {
if attachments == nil {
return nil
}
fileIDs := make(map[string]bool)
for _, attachment := range *attachments {
attachment := attachment
fileInfo, err := a.importAttachment(c, &attachment, post, teamID)
if err != nil {
if attachment.Path != nil {
mlog.Warn(
"failed to import attachment",
mlog.String("path", *attachment.Path),
mlog.String("error", err.Error()))
} else {
mlog.Warn("failed to import attachment; path was nil",
mlog.String("error", err.Error()))
}
continue
}
fileIDs[fileInfo.Id] = true
}
return fileIDs
}
func (a *App) updateFileInfoWithPostId(post *model.Post) {
for _, fileID := range post.FileIds {
if err := a.Srv().Store().FileInfo().AttachToPost(fileID, post.Id, post.ChannelId, post.UserId); err != nil {
mlog.Error("Error attaching files to post.", mlog.String("post_id", post.Id), mlog.Any("post_file_ids", post.FileIds), mlog.Err(err))
}
}
}
func (a *App) importDirectChannel(c request.CTX, data *imports.DirectChannelImportData, dryRun bool) *model.AppError {
var err *model.AppError
if err = imports.ValidateDirectChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var userIDs []string
userMap, err := a.getUsersByUsernames(*data.Members)
if err != nil {
return err
}
for _, user := range *data.Members {
userIDs = append(userIDs, userMap[strings.ToLower(user)].Id)
}
var channel *model.Channel
if len(userIDs) == 2 {
ch, err := a.createDirectChannel(c, userIDs[0], userIDs[1])
if err != nil && err.Id != store.ChannelExistsError {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
} else {
ch, err := a.createGroupChannel(c, userIDs)
if err != nil && err.Id != store.ChannelExistsError {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
}
var preferences model.Preferences
for _, userID := range userIDs {
preferences = append(preferences, model.Preference{
UserId: userID,
Category: model.PreferenceCategoryDirectChannelShow,
Name: channel.Id,
Value: "true",
})
}
if data.FavoritedBy != nil {
for _, favoriter := range *data.FavoritedBy {
preferences = append(preferences, model.Preference{
UserId: userMap[strings.ToLower(favoriter)].Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
})
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
appErr.StatusCode = http.StatusBadRequest
return appErr
default:
return model.NewAppError("importDirectChannel", "app.preference.save.updating.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if data.Header != nil {
channel.Header = *data.Header
if _, appErr := a.Srv().Store().Channel().Update(channel); appErr != nil {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, "", http.StatusBadRequest).Wrap(appErr)
}
}
return nil
}
// importMultipleDirectPostLines will return an error and the line
// that caused it whenever possible
func (a *App) importMultipleDirectPostLines(c request.CTX, lines []imports.LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
for _, line := range lines {
if err := imports.ValidateDirectPostImportData(line.DirectPost, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
usernames := []string{}
for _, line := range lines {
usernames = append(usernames, *line.DirectPost.User)
if line.DirectPost.FlaggedBy != nil {
usernames = append(usernames, *line.DirectPost.FlaggedBy...)
}
usernames = append(usernames, *line.DirectPost.ChannelMembers...)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
)
for _, line := range lines {
var userIDs []string
var err *model.AppError
for _, username := range *line.DirectPost.ChannelMembers {
user := users[strings.ToLower(username)]
userIDs = append(userIDs, user.Id)
}
var channel *model.Channel
var ch *model.Channel
if len(userIDs) == 2 {
ch, err = a.GetOrCreateDirectChannel(c, userIDs[0], userIDs[1])
if err != nil && err.Id != store.ChannelExistsError {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
} else {
ch, err = a.createGroupChannel(c, userIDs)
if err != nil && err.Id != store.ChannelExistsError {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
}
user := users[strings.ToLower(*line.DirectPost.User)]
// Check if this post already exists.
posts, nErr := a.Srv().Store().Post().GetPostsCreatedAt(channel.Id, *line.DirectPost.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("BulkImport", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.DirectPost.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.DirectPost.Message
post.UserId = user.Id
post.CreateAt = *line.DirectPost.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.DirectPost.Type != nil {
post.Type = *line.DirectPost.Type
}
if line.DirectPost.EditAt != nil {
post.EditAt = *line.DirectPost.EditAt
}
if line.DirectPost.Props != nil {
post.Props = *line.DirectPost.Props
}
if line.DirectPost.IsPinned != nil {
post.IsPinned = *line.DirectPost.IsPinned
}
fileIDs := a.uploadAttachments(c, line.DirectPost.Attachments, post, "noteam")
for _, fileID := range post.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIDs {
post.FileIds = append(post.FileIds, fileID)
}
if post.Id == "" {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, directPostData: line.DirectPost, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(err, &appErr):
retErr = appErr
case errors.As(err, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, postWithData := range postsWithData {
if postWithData.directPostData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.directPostData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if postWithData.directPostData.Reactions != nil {
for _, reaction := range *postWithData.directPostData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.directPostData.Replies != nil {
if err := a.importReplies(c, *postWithData.directPostData.Replies, postWithData.post, "noteam"); err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
func (a *App) importEmoji(c request.CTX, data *imports.EmojiImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("emoji_name", *data.Name))
}
c.Logger().Info("Validating emoji", fields...)
aerr := imports.ValidateEmojiImportData(data)
if aerr != nil {
if aerr.Id == "model.emoji.system_emoji_name.app_error" {
mlog.Warn("Skipping emoji import due to name conflict with system emoji", mlog.String("emoji_name", *data.Name))
return nil
}
return aerr
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing emoji", fields...)
var emoji *model.Emoji
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("importEmoji", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
alreadyExists := emoji != nil
if !alreadyExists {
emoji = &model.Emoji{
Name: *data.Name,
}
emoji.PreSave()
}
var file io.ReadCloser
if data.Data != nil {
file, err = data.Data.Open()
} else {
file, err = os.Open(*data.Image)
}
if err != nil {
return model.NewAppError("BulkImport", "app.import.emoji.bad_file.error", map[string]any{"EmojiName": *data.Name}, "", http.StatusBadRequest)
}
defer file.Close()
reader := utils.NewLimitedReaderWithError(file, MaxEmojiFileSize)
if _, err := a.WriteFile(reader, getEmojiImagePath(emoji.Id)); err != nil {
return err
}
if !alreadyExists {
if _, err := a.Srv().Store().Emoji().Save(emoji); err != nil {
return model.NewAppError("importEmoji", "api.emoji.create.internal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/rand"
"math/big"
)
const (
passwordSpecialChars = "!$%^&*(),."
passwordNumbers = "0123456789"
passwordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
passwordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz"
passwordAllChars = passwordSpecialChars + passwordNumbers + passwordUpperCaseLetters + passwordLowerCaseLetters
)
func randInt(max int) (int, error) {
val, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return 0, err
}
return int(val.Int64()), nil
}
func generatePassword(minimumLength int) (string, error) {
upperIdx, err := randInt(len(passwordUpperCaseLetters))
if err != nil {
return "", err
}
numberIdx, err := randInt(len(passwordNumbers))
if err != nil {
return "", err
}
lowerIdx, err := randInt(len(passwordLowerCaseLetters))
if err != nil {
return "", err
}
specialIdx, err := randInt(len(passwordSpecialChars))
if err != nil {
return "", err
}
// Make sure we are guaranteed at least one of each type to meet any possible password complexity requirements.
password := string([]rune(passwordUpperCaseLetters)[upperIdx]) +
string([]rune(passwordNumbers)[numberIdx]) +
string([]rune(passwordLowerCaseLetters)[lowerIdx]) +
string([]rune(passwordSpecialChars)[specialIdx])
for len(password) < minimumLength {
i, err := randInt(len(passwordAllChars))
if err != nil {
return "", err
}
password = password + string([]rune(passwordAllChars)[i])
}
return password, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imports
import (
"encoding/json"
"net/http"
"os"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func ValidateSchemeImportData(data *SchemeImportData) *model.AppError {
if data.Scope == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.null_scope.error", nil, "", http.StatusBadRequest)
}
switch *data.Scope {
case model.SchemeScopeTeam:
if data.DefaultTeamAdminRole == nil || data.DefaultTeamUserRole == nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
}
case model.SchemeScopeChannel:
if data.DefaultTeamAdminRole != nil || data.DefaultTeamUserRole != nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
}
default:
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.unknown_scheme.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil || !model.IsValidSchemeName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.SchemeDisplayNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.SchemeDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DefaultTeamAdminRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamAdminRole); err != nil {
return err
}
}
if data.DefaultTeamUserRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamUserRole); err != nil {
return err
}
}
if data.DefaultTeamGuestRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamGuestRole); err != nil {
return err
}
}
if data.DefaultChannelAdminRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelAdminRole); err != nil {
return err
}
}
if data.DefaultChannelUserRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelUserRole); err != nil {
return err
}
}
if data.DefaultChannelGuestRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelGuestRole); err != nil {
return err
}
}
return nil
}
func ValidateRoleImportData(data *RoleImportData) *model.AppError {
if data.Name == nil || !model.IsValidRoleName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.RoleDisplayNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.RoleDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Permissions != nil {
for _, permission := range *data.Permissions {
permissionValidated := false
for _, p := range append(model.AllPermissions, model.DeprecatedPermissions...) {
if permission == p.Id {
permissionValidated = true
break
}
}
if !permissionValidated {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.invalid_permission.error", nil, "permission"+permission, http.StatusBadRequest)
}
}
}
return nil
}
func ValidateTeamImportData(data *TeamImportData) *model.AppError {
if data.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_missing.error", nil, "", http.StatusBadRequest)
} else if len(*data.Name) > model.TeamNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_length.error", nil, "", http.StatusBadRequest)
} else if model.IsReservedTeamName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_reserved.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidTeamName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_characters.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.DisplayName) == 0 || utf8.RuneCountInString(*data.DisplayName) > model.TeamDisplayNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Type == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Type != model.TeamOpen && *data.Type != model.TeamInvite {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.TeamDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.description_length.error", nil, "", http.StatusBadRequest)
}
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
if data.Team == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.team_missing.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_missing.error", nil, "", http.StatusBadRequest)
} else if len(*data.Name) > model.ChannelNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_length.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidChannelIdentifier(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_characters.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || utf8.RuneCountInString(*data.DisplayName) == 0 {
data.DisplayName = data.Name // when displayName is missing we use name instead for displaying so we might as well convert it here.
} else if utf8.RuneCountInString(*data.DisplayName) > model.ChannelDisplayNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Type == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Type != model.ChannelTypeOpen && *data.Type != model.ChannelTypePrivate {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
}
if data.Purpose != nil && utf8.RuneCountInString(*data.Purpose) > model.ChannelPurposeMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.purpose_length.error", nil, "", http.StatusBadRequest)
}
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateUserImportData(data *UserImportData) *model.AppError {
if data.ProfileImage != nil {
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest)
}
}
if data.Username == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidUsername(*data.Username) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Email == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Email == "" || len(*data.Email) > model.UserEmailMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_length.error", nil, "", http.StatusBadRequest)
}
if data.AuthData != nil && data.Password != nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_password.error", nil, "", http.StatusBadRequest)
}
if data.AuthData != nil && len(*data.AuthData) > model.UserAuthDataMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_length.error", nil, "", http.StatusBadRequest)
}
blank := func(str *string) bool {
if str == nil {
return true
}
return *str == ""
}
if (!blank(data.AuthService) && blank(data.AuthData)) || (blank(data.AuthService) && !blank(data.AuthData)) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_service_dependency.error", nil, "", http.StatusBadRequest)
}
if appErr := validateAuthService(data.AuthService); appErr != nil {
return appErr
}
if data.Password != nil && *data.Password == "" {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
}
if data.Password != nil && len(*data.Password) > model.UserPasswordMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
}
if data.Nickname != nil && utf8.RuneCountInString(*data.Nickname) > model.UserNicknameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.nickname_length.error", nil, "", http.StatusBadRequest)
}
if data.FirstName != nil && utf8.RuneCountInString(*data.FirstName) > model.UserFirstNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest)
}
if data.LastName != nil && utf8.RuneCountInString(*data.LastName) > model.UserLastNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.last_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Position != nil && utf8.RuneCountInString(*data.Position) > model.UserPositionMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.position_length.error", nil, "", http.StatusBadRequest)
}
if data.Roles != nil && !model.IsValidUserRoles(*data.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.roles_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps != nil {
if data.NotifyProps.Desktop != nil && !isValidUserNotifyLevel(*data.NotifyProps.Desktop) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.DesktopSound != nil && !isValidTrueOrFalseString(*data.NotifyProps.DesktopSound) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.Email != nil && !isValidTrueOrFalseString(*data.NotifyProps.Email) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_email_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.Mobile != nil && !isValidUserNotifyLevel(*data.NotifyProps.Mobile) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.MobilePushStatus != nil && !isValidPushStatusNotifyLevel(*data.NotifyProps.MobilePushStatus) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.ChannelTrigger != nil && !isValidTrueOrFalseString(*data.NotifyProps.ChannelTrigger) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.CommentsTrigger != nil && !isValidCommentsNotifyLevel(*data.NotifyProps.CommentsTrigger) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", nil, "", http.StatusBadRequest)
}
}
if data.UseMarkdownPreview != nil && !isValidTrueOrFalseString(*data.UseMarkdownPreview) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", nil, "", http.StatusBadRequest)
}
if data.UseFormatting != nil && !isValidTrueOrFalseString(*data.UseFormatting) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_formatting.error", nil, "", http.StatusBadRequest)
}
if data.ShowUnreadSection != nil && !isValidTrueOrFalseString(*data.ShowUnreadSection) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_show_unread_section.error", nil, "", http.StatusBadRequest)
}
if data.EmailInterval != nil && !isValidEmailBatchingInterval(*data.EmailInterval) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_email_interval.error", nil, "", http.StatusBadRequest)
}
if data.Teams != nil {
return ValidateUserTeamsImportData(data.Teams)
}
return nil
}
var validAuthServices = []string{
"",
model.UserAuthServiceEmail,
model.UserAuthServiceGitlab,
model.UserAuthServiceSaml,
model.UserAuthServiceLdap,
model.ServiceGoogle,
model.ServiceOffice365,
}
func validateAuthService(authService *string) *model.AppError {
if authService == nil {
return nil
}
for _, valid := range validAuthServices {
if *authService == valid {
return nil
}
}
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_auth_service.error", map[string]any{"AuthService": *authService}, "", http.StatusBadRequest)
}
func ValidateUserTeamsImportData(data *[]UserTeamImportData) *model.AppError {
if data == nil {
return nil
}
for _, tdata := range *data {
if tdata.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.team_name_missing.error", nil, "", http.StatusBadRequest)
}
if tdata.Roles != nil && !model.IsValidUserRoles(*tdata.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
}
if tdata.Channels != nil {
if err := ValidateUserChannelsImportData(tdata.Channels); err != nil {
return err
}
}
if tdata.Theme != nil && strings.Trim(*tdata.Theme, " \t\r") != "" {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(*tdata.Theme)).Decode(&unused); err != nil {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_team_theme.error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
}
return nil
}
func ValidateUserChannelsImportData(data *[]UserChannelImportData) *model.AppError {
if data == nil {
return nil
}
for _, cdata := range *data {
if cdata.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.channel_name_missing.error", nil, "", http.StatusBadRequest)
}
if cdata.Roles != nil && !model.IsValidUserRoles(*cdata.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps != nil {
if cdata.NotifyProps.Desktop != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Desktop) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps.Mobile != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Mobile) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps.MarkUnread != nil && !model.IsChannelMarkUnreadLevelValid(*cdata.NotifyProps.MarkUnread) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", nil, "", http.StatusBadRequest)
}
}
}
return nil
}
func ValidateReactionImportData(data *ReactionImportData, parentCreateAt int64) *model.AppError {
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.EmojiName == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.EmojiName) > model.EmojiNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt < parentCreateAt {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_before_parent.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateReplyImportData(data *ReplyImportData, parentCreateAt int64, maxPostSize int) *model.AppError {
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt < parentCreateAt {
mlog.Warn("Reply CreateAt is before parent post CreateAt", mlog.Int64("reply_create_at", *data.CreateAt), mlog.Int64("parent_create_at", parentCreateAt))
}
return nil
}
func ValidatePostImportData(data *PostImportData, maxPostSize int) *model.AppError {
if data.Team == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.team_missing.error", nil, "", http.StatusBadRequest)
}
if data.Channel == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.channel_missing.error", nil, "", http.StatusBadRequest)
}
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
}
if data.Reactions != nil {
for _, reaction := range *data.Reactions {
reaction := reaction
ValidateReactionImportData(&reaction, *data.CreateAt)
}
}
if data.Replies != nil {
for _, reply := range *data.Replies {
reply := reply
ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize)
}
}
if data.Props != nil && utf8.RuneCountInString(model.StringInterfaceToJSON(*data.Props)) > model.PostPropsMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.props_too_large.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateDirectChannelImportData(data *DirectChannelImportData) *model.AppError {
if data.Members == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_required.error", nil, "", http.StatusBadRequest)
}
if len(*data.Members) != 2 {
if len(*data.Members) < model.ChannelGroupMinUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_few.error", nil, "", http.StatusBadRequest)
} else if len(*data.Members) > model.ChannelGroupMaxUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_many.error", nil, "", http.StatusBadRequest)
}
}
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
}
if data.FavoritedBy != nil {
for _, favoriter := range *data.FavoritedBy {
found := false
for _, member := range *data.Members {
if favoriter == member {
found = true
break
}
}
if !found {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.unknown_favoriter.error", map[string]any{"Username": favoriter}, "", http.StatusBadRequest)
}
}
}
return nil
}
func ValidateDirectPostImportData(data *DirectPostImportData, maxPostSize int) *model.AppError {
if data.ChannelMembers == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_required.error", nil, "", http.StatusBadRequest)
}
if len(*data.ChannelMembers) != 2 {
if len(*data.ChannelMembers) < model.ChannelGroupMinUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_few.error", nil, "", http.StatusBadRequest)
} else if len(*data.ChannelMembers) > model.ChannelGroupMaxUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_many.error", nil, "", http.StatusBadRequest)
}
}
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
}
if data.FlaggedBy != nil {
for _, flagger := range *data.FlaggedBy {
found := false
for _, member := range *data.ChannelMembers {
if flagger == member {
found = true
break
}
}
if !found {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.unknown_flagger.error", map[string]any{"Username": flagger}, "", http.StatusBadRequest)
}
}
}
if data.Reactions != nil {
for _, reaction := range *data.Reactions {
reaction := reaction
ValidateReactionImportData(&reaction, *data.CreateAt)
}
}
if data.Replies != nil {
for _, reply := range *data.Replies {
reply := reply
ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize)
}
}
return nil
}
// ValidateEmojiImportData validates emoji data and returns if the import name
// conflicts with a system emoji.
func ValidateEmojiImportData(data *EmojiImportData) *model.AppError {
if data == nil {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.empty.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil || *data.Name == "" {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.name_missing.error", nil, "", http.StatusBadRequest)
}
if data.Image == nil || *data.Image == "" {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest)
}
if err := model.IsValidEmojiName(*data.Name); err != nil {
return err
}
return nil
}
func isValidTrueOrFalseString(value string) bool {
return value == "true" || value == "false"
}
func isValidUserNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.ChannelNotifyAll ||
notifyLevel == model.ChannelNotifyMention ||
notifyLevel == model.ChannelNotifyNone
}
func isValidPushStatusNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.StatusOnline ||
notifyLevel == model.StatusAway ||
notifyLevel == model.StatusOffline
}
func isValidCommentsNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.CommentsNotifyAny ||
notifyLevel == model.CommentsNotifyRoot ||
notifyLevel == model.CommentsNotifyNever
}
func isValidEmailBatchingInterval(emailInterval string) bool {
return emailInterval == model.PreferenceEmailIntervalImmediately ||
emailInterval == model.PreferenceEmailIntervalFifteen ||
emailInterval == model.PreferenceEmailIntervalHour
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Integration Action Flow
//
// 1. An integration creates an interactive message button or menu.
// 2. A user clicks on a button or selects an option from the menu.
// 3. The client sends a request to server to complete the post action, calling DoPostAction below.
// 4. DoPostAction will send an HTTP POST request to the integration containing contextual data, including
// an encoded and signed trigger ID. Slash commands also include trigger IDs in their payloads.
// 5. The integration performs any actions it needs to and optionally makes a request back to the MM server
// using the trigger ID to open an interactive dialog.
// 6. If that optional request is made, OpenInteractiveDialog sends a WebSocket event to all connected clients
// for the relevant user, telling them to display the dialog.
// 7. The user fills in the dialog and submits it, where SubmitInteractiveDialog will submit it back to the
// integration for handling.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) DoPostAction(c *request.Context, postID, actionId, userID, selectedOption string) (string, *model.AppError) {
return a.DoPostActionWithCookie(c, postID, actionId, userID, selectedOption, nil)
}
func (a *App) DoPostActionWithCookie(c *request.Context, postID, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
// PostAction may result in the original post being updated. For the
// updated post, we need to unconditionally preserve the original
// IsPinned and HasReaction attributes, and preserve its entire
// original Props set unless the plugin returns a replacement value.
// originalXxx variables are used to preserve these values.
var originalProps map[string]any
originalIsPinned := false
originalHasReactions := false
// If the updated post does contain a replacement Props set, we still
// need to preserve some original values, as listed in
// model.PostActionRetainPropKeys. remove and retain track these.
remove := []string{}
retain := map[string]any{}
datasource := ""
upstreamURL := ""
rootPostId := ""
upstreamRequest := &model.PostActionIntegrationRequest{
UserId: userID,
PostId: postID,
}
// See if the post exists in the DB, if so ignore the cookie.
// Start all queries here for parallel execution
pchan := make(chan store.StoreResult, 1)
go func() {
post, err := a.Srv().Store().Post().GetSingle(postID, false)
pchan <- store.StoreResult{Data: post, NErr: err}
close(pchan)
}()
cchan := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().GetForPost(postID)
cchan <- store.StoreResult{Data: channel, NErr: err}
close(cchan)
}()
userChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), upstreamRequest.UserId)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
result := <-pchan
if result.NErr != nil {
if cookie == nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
if cookie.Integration == nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "no Integration in action cookie", http.StatusBadRequest)
}
if postID != cookie.PostId {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
}
channel, err := a.Srv().Store().Channel().Get(cookie.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
upstreamRequest.ChannelId = cookie.ChannelId
upstreamRequest.ChannelName = channel.Name
upstreamRequest.TeamId = channel.TeamId
upstreamRequest.Type = cookie.Type
upstreamRequest.Context = cookie.Integration.Context
datasource = cookie.DataSource
retain = cookie.RetainProps
remove = cookie.RemoveProps
rootPostId = cookie.RootPostId
upstreamURL = cookie.Integration.URL
} else {
post := result.Data.(*model.Post)
result = <-cchan
if result.NErr != nil {
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
channel := result.Data.(*model.Channel)
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
}
upstreamRequest.ChannelId = post.ChannelId
upstreamRequest.ChannelName = channel.Name
upstreamRequest.TeamId = channel.TeamId
upstreamRequest.Type = action.Type
upstreamRequest.Context = action.Integration.Context
datasource = action.DataSource
// Save the original values that may need to be preserved (including selected
// Props, i.e. override_username, override_icon_url)
for _, key := range model.PostActionRetainPropKeys {
value, ok := post.GetProps()[key]
if ok {
retain[key] = value
} else {
remove = append(remove, key)
}
}
originalProps = post.GetProps()
originalIsPinned = post.IsPinned
originalHasReactions = post.HasReactions
if post.RootId == "" {
rootPostId = post.Id
} else {
rootPostId = post.RootId
}
upstreamURL = action.Integration.URL
}
teamChan := make(chan store.StoreResult, 1)
go func() {
defer close(teamChan)
// Direct and group channels won't have teams.
if upstreamRequest.TeamId == "" {
return
}
team, err := a.Srv().Store().Team().Get(upstreamRequest.TeamId)
teamChan <- store.StoreResult{Data: team, NErr: err}
}()
ur := <-userChan
if ur.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(ur.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
}
}
user := ur.Data.(*model.User)
upstreamRequest.UserName = user.Username
tr, ok := <-teamChan
if ok {
if tr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(tr.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
}
}
team := tr.Data.(*model.Team)
upstreamRequest.TeamName = team.Name
}
if upstreamRequest.Type == model.PostActionTypeSelect {
if selectedOption != "" {
if upstreamRequest.Context == nil {
upstreamRequest.Context = map[string]any{}
}
upstreamRequest.DataSource = datasource
upstreamRequest.Context["selected_option"] = selectedOption
}
}
clientTriggerId, _, appErr := upstreamRequest.GenerateTriggerId(a.AsymmetricSigningKey())
if appErr != nil {
return "", appErr
}
if strings.HasPrefix(upstreamURL, "/warn_metrics/") {
appErr = a.doLocalWarnMetricsRequest(c, upstreamURL, upstreamRequest)
if appErr != nil {
return "", appErr
}
return "", nil
}
requestJSON, err := json.Marshal(upstreamRequest)
if err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp, appErr := a.DoActionRequest(c, upstreamURL, requestJSON)
if appErr != nil {
return "", appErr
}
defer resp.Body.Close()
var response model.PostActionIntegrationResponse
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(respBytes) > 0 {
if err = json.Unmarshal(respBytes, &response); err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if response.Update != nil {
response.Update.Id = postID
// Restore the post attributes and Props that need to be preserved
if response.Update.GetProps() == nil {
response.Update.SetProps(originalProps)
} else {
for key, value := range retain {
response.Update.AddProp(key, value)
}
for _, key := range remove {
response.Update.DelProp(key)
}
}
response.Update.IsPinned = originalIsPinned
response.Update.HasReactions = originalHasReactions
if _, appErr = a.UpdatePost(c, response.Update, false); appErr != nil {
return "", appErr
}
}
if response.EphemeralText != "" {
ephemeralPost := &model.Post{
Message: response.EphemeralText,
ChannelId: upstreamRequest.ChannelId,
RootId: rootPostId,
UserId: userID,
}
if !response.SkipSlackParsing {
ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
}
for key, value := range retain {
ephemeralPost.AddProp(key, value)
}
a.SendEphemeralPost(c, userID, ephemeralPost)
}
return clientTriggerId, nil
}
// Perform an HTTP POST request to an integration's action endpoint.
// Caller must consume and close returned http.Response as necessary.
// For internal requests, requests are routed directly to a plugin ServerHTTP hook
func (a *App) DoActionRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
inURL, err := url.Parse(rawURL)
if err != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rawURLPath := path.Clean(rawURL)
if strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/") {
return a.DoLocalRequest(c, rawURLPath, body)
}
req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
if err != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Allow access to plugin routes for action buttons
var httpClient *http.Client
subpath, _ := utils.GetSubpathFromConfig(a.Config())
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
if (inURL.Hostname() == "localhost" || inURL.Hostname() == "127.0.0.1" || inURL.Hostname() == siteURL.Hostname()) && strings.HasPrefix(inURL.Path, path.Join(subpath, "plugins")) {
req.Header.Set(model.HeaderAuth, "Bearer "+c.Session().Token)
httpClient = a.HTTPService().MakeClient(true)
} else {
httpClient = a.HTTPService().MakeClient(false)
}
resp, httpErr := httpClient.Do(req)
if httpErr != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+httpErr.Error(), http.StatusBadRequest)
}
if resp.StatusCode != http.StatusOK {
return resp, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
}
return resp, nil
}
type LocalResponseWriter struct {
data []byte
headers http.Header
status int
}
func (w *LocalResponseWriter) Header() http.Header {
if w.headers == nil {
w.headers = make(http.Header)
}
return w.headers
}
func (w *LocalResponseWriter) Write(bytes []byte) (int, error) {
w.data = make([]byte, len(bytes))
copy(w.data, bytes)
return len(w.data), nil
}
func (w *LocalResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode
}
func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
return a.ch.doPluginRequest(c, method, rawURL, values, body)
}
func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
rawURL = strings.TrimPrefix(rawURL, "/")
inURL, err := url.Parse(rawURL)
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
result := strings.Split(inURL.Path, "/")
if len(result) < 2 {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=Unable to find pluginId", http.StatusBadRequest)
}
if result[0] != "plugins" {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=plugins not in path", http.StatusBadRequest)
}
pluginID := result[1]
path := strings.TrimPrefix(inURL.Path, "plugins/"+pluginID)
base, err := url.Parse(path)
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
// merge the rawQuery params (if any) with the function's provided values
rawValues := inURL.Query()
if len(rawValues) != 0 {
if values == nil {
values = make(url.Values)
}
for k, vs := range rawValues {
for _, v := range vs {
values.Add(k, v)
}
}
}
if values != nil {
base.RawQuery = values.Encode()
}
w := &LocalResponseWriter{}
r, err := http.NewRequest(method, base.String(), bytes.NewReader(body))
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
r.Header.Set("Mattermost-User-Id", c.Session().UserId)
r.Header.Set(model.HeaderAuth, "Bearer "+c.Session().Token)
params := make(map[string]string)
params["plugin_id"] = pluginID
r = mux.SetURLVars(r, params)
ch.ServePluginRequest(w, r)
resp := &http.Response{
StatusCode: w.status,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: w.headers,
Body: io.NopCloser(bytes.NewReader(w.data)),
}
if resp.StatusCode == 0 {
resp.StatusCode = http.StatusOK
}
return resp, nil
}
func (a *App) doLocalWarnMetricsRequest(c *request.Context, rawURL string, upstreamRequest *model.PostActionIntegrationRequest) *model.AppError {
_, err := url.Parse(rawURL)
if err != nil {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
warnMetricId := filepath.Base(rawURL)
if warnMetricId == "" {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest)
}
license := a.Srv().License()
if license != nil {
mlog.Debug("License is present, skip this call")
return nil
}
user, appErr := a.GetUser(c.Session().UserId)
if appErr != nil {
return appErr
}
botPost := &model.Post{
UserId: upstreamRequest.Context["bot_user_id"].(string),
ChannelId: upstreamRequest.ChannelId,
HasReactions: true,
}
isE0Edition := (model.BuildEnterpriseReady == "true") // license == nil was already validated upstream
_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, i18n.T, isE0Edition)
botPost.Message = ":white_check_mark: " + warnMetricDisplayTexts.BotSuccessMessage
if isE0Edition {
if appErr = a.RequestLicenseAndAckWarnMetric(c, warnMetricId, true); appErr != nil {
botPost.Message = ":warning: " + i18n.T("api.server.warn_metric.bot_response.start_trial_failure.message")
}
} else {
forceAck := upstreamRequest.Context["force_ack"].(bool)
if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil {
if forceAck {
return appErr
}
mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user)
botPost.Message = ":warning: " + i18n.T("api.server.warn_metric.bot_response.notification_failure.message")
actions := []*model.PostAction{}
actions = append(actions,
&model.PostAction{
Id: "emailUs",
Name: i18n.T("api.server.warn_metric.email_us"),
Type: model.PostActionTypeButton,
Options: []*model.PostActionOptions{
{
Text: "WarnMetricMailtoUrl",
Value: mailtoLinkText,
},
{
Text: "TrackEventId",
Value: warnMetricId,
},
},
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"bot_user_id": botPost.UserId,
"force_ack": true,
},
URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SystemWarnMetricNumberOfActiveUsers500),
},
},
)
attachments := []*model.SlackAttachment{{
AuthorName: "",
Title: "",
Actions: actions,
Text: i18n.T("api.server.warn_metric.bot_response.notification_failure.body"),
}}
model.ParseSlackAttachment(botPost, attachments)
}
}
if _, err := a.CreatePostAsUser(c, botPost, c.Session().Id, true); err != nil {
return err
}
return nil
}
type MailToLinkContent struct {
MetricId string `json:"metric_id"`
MailRecipient string `json:"mail_recipient"`
MailCC string `json:"mail_cc"`
MailSubject string `json:"mail_subject"`
MailBody string `json:"mail_body"`
}
func (mlc *MailToLinkContent) ToJSON() string {
b, _ := json.Marshal(mlc)
return string(b)
}
func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string {
T := i18n.GetUserTranslations(user.Locale)
_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false)
mailBody := warnMetricDisplayTexts.EmailBody
mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]any{"Contact": user.GetFullName()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]any{"Email": user.Email})
mailBody += "\r\n"
registeredUsersCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
mlog.Warn("Error retrieving the number of registered users", mlog.Err(err))
} else {
mailBody += i18n.T("api.server.warn_metric.bot_response.mailto_registered_users_header", map[string]any{"NoRegisteredUsers": registeredUsersCount})
mailBody += "\r\n"
}
mailBody += T("api.server.warn_metric.bot_response.mailto_site_url_header", map[string]any{"SiteUrl": a.GetSiteURL()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_diagnostic_id_header", map[string]any{"DiagnosticId": a.TelemetryId()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_footer")
mailToLinkContent := &MailToLinkContent{
MetricId: warnMetricId,
MailRecipient: model.MmSupportAdvisorAddress,
MailCC: user.Email,
MailSubject: T("api.server.warn_metric.bot_response.mailto_subject"),
MailBody: mailBody,
}
return mailToLinkContent.ToJSON()
}
func (a *App) DoLocalRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
return a.doPluginRequest(c, "POST", rawURL, nil, body)
}
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
clientTriggerId, userID, appErr := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
if appErr != nil {
return appErr
}
request.TriggerId = clientTriggerId
jsonRequest, err := json.Marshal(request)
if err != nil {
a.ch.srv.Log().Warn("Error encoding request", mlog.Err(err))
}
message := model.NewWebSocketEvent(model.WebsocketEventOpenDialog, "", "", userID, nil, "")
message.Add("dialog", string(jsonRequest))
a.Publish(message)
return nil
}
func (a *App) SubmitInteractiveDialog(c *request.Context, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
url := request.URL
request.URL = ""
request.Type = "dialog_submission"
b, err := json.Marshal(request)
if err != nil {
return nil, model.NewAppError("SubmitInteractiveDialog", "app.submit_interactive_dialog.json_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resp, appErr := a.DoActionRequest(c, url, b)
if appErr != nil {
return nil, appErr
}
defer resp.Body.Close()
var response model.SubmitDialogResponse
json.NewDecoder(resp.Body).Decode(&response) // Don't fail, an empty response is acceptable
return &response, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetJob(id string) (*model.Job, *model.AppError) {
job, err := a.Srv().Store().Job().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return job, nil
}
func (a *App) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobs(page*perPage, perPage)
}
func (a *App) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllPage(offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobs", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobsByType(jobType, page*perPage, perPage)
}
func (a *App) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllByTypePage(jobType, offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobsByTypes(jobType, page*perPage, perPage)
}
func (a *App) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllByTypesPage(jobTypes, offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) CreateJob(job *model.Job) (*model.Job, *model.AppError) {
return a.Srv().Jobs.CreateJob(job.Type, job.Data)
}
func (a *App) CancelJob(jobId string) *model.AppError {
return a.Srv().Jobs.RequestCancellation(jobId)
}
func (a *App) SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) {
switch job.Type {
case model.JobTypeBlevePostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionCreatePostBleveIndexesJob), model.PermissionCreatePostBleveIndexesJob
case model.JobTypeDataRetention:
return a.SessionHasPermissionTo(session, model.PermissionCreateDataRetentionJob), model.PermissionCreateDataRetentionJob
case model.JobTypeMessageExport:
return a.SessionHasPermissionTo(session, model.PermissionCreateComplianceExportJob), model.PermissionCreateComplianceExportJob
case model.JobTypeElasticsearchPostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionCreateElasticsearchPostIndexingJob), model.PermissionCreateElasticsearchPostIndexingJob
case model.JobTypeElasticsearchPostAggregation:
return a.SessionHasPermissionTo(session, model.PermissionCreateElasticsearchPostAggregationJob), model.PermissionCreateElasticsearchPostAggregationJob
case model.JobTypeLdapSync:
return a.SessionHasPermissionTo(session, model.PermissionCreateLdapSyncJob), model.PermissionCreateLdapSyncJob
case
model.JobTypeMigrations,
model.JobTypePlugins,
model.JobTypeProductNotices,
model.JobTypeExpiryNotify,
model.JobTypeActiveUsers,
model.JobTypeImportProcess,
model.JobTypeImportDelete,
model.JobTypeExportProcess,
model.JobTypeExportDelete,
model.JobTypeCloud,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionManageJobs), model.PermissionManageJobs
}
return false, nil
}
func (a *App) SessionHasPermissionToReadJob(session model.Session, jobType string) (bool, *model.Permission) {
switch jobType {
case model.JobTypeDataRetention:
return a.SessionHasPermissionTo(session, model.PermissionReadDataRetentionJob), model.PermissionReadDataRetentionJob
case model.JobTypeMessageExport:
return a.SessionHasPermissionTo(session, model.PermissionReadComplianceExportJob), model.PermissionReadComplianceExportJob
case model.JobTypeElasticsearchPostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionReadElasticsearchPostIndexingJob), model.PermissionReadElasticsearchPostIndexingJob
case model.JobTypeElasticsearchPostAggregation:
return a.SessionHasPermissionTo(session, model.PermissionReadElasticsearchPostAggregationJob), model.PermissionReadElasticsearchPostAggregationJob
case model.JobTypeLdapSync:
return a.SessionHasPermissionTo(session, model.PermissionReadLdapSyncJob), model.PermissionReadLdapSyncJob
case
model.JobTypeBlevePostIndexing,
model.JobTypeMigrations,
model.JobTypePlugins,
model.JobTypeProductNotices,
model.JobTypeExpiryNotify,
model.JobTypeActiveUsers,
model.JobTypeImportProcess,
model.JobTypeImportDelete,
model.JobTypeExportProcess,
model.JobTypeExportDelete,
model.JobTypeCloud,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
}
return false, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"io"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// SyncLdap starts an LDAP sync job.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) SyncLdap(includeRemovedMembers bool) {
a.Srv().Go(func() {
if license := a.Srv().License(); license != nil && *license.Features.LDAP {
if !*a.Config().LdapSettings.EnableSync {
mlog.Error("LdapSettings.EnableSync is set to false. Skipping LDAP sync.")
return
}
ldapI := a.Ldap()
if ldapI == nil {
mlog.Error("Not executing ldap sync because ldap is not available")
return
}
ldapI.StartSynchronizeJob(false, includeRemovedMembers)
}
})
}
func (a *App) TestLdap() *model.AppError {
license := a.Srv().License()
if ldapI := a.Ldap(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
if err := ldapI.RunTest(); err != nil {
err.StatusCode = 500
return err
}
} else {
err := model.NewAppError("TestLdap", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
// GetLdapGroup retrieves a single LDAP group by the given LDAP group id.
func (a *App) GetLdapGroup(ldapGroupID string) (*model.Group, *model.AppError) {
var group *model.Group
if a.Ldap() != nil {
var err *model.AppError
group, err = a.Ldap().GetGroup(ldapGroupID)
if err != nil {
return nil, err
}
} else {
ae := model.NewAppError("GetLdapGroup", "ent.ldap.app_error", map[string]any{"ldap_group_id": ldapGroupID}, "", http.StatusNotImplemented)
return nil, ae
}
return group, nil
}
// GetAllLdapGroupsPage retrieves all LDAP groups under the configured base DN using the default or configured group
// filter.
func (a *App) GetAllLdapGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
var groups []*model.Group
var total int
if a.Ldap() != nil {
var err *model.AppError
groups, total, err = a.Ldap().GetAllGroupsPage(page, perPage, opts)
if err != nil {
return nil, 0, err
}
} else {
ae := model.NewAppError("GetAllLdapGroupsPage", "ent.ldap.app_error", nil, "", http.StatusNotImplemented)
return nil, 0, ae
}
return groups, total, nil
}
func (a *App) SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err := a.CheckPasswordAndAllCriteria(user, password, code); err != nil {
return "", err
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return "", err
}
ldapInterface := a.Ldap()
if ldapInterface == nil {
return "", model.NewAppError("SwitchEmailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
}
if err := ldapInterface.SwitchToLdap(user.Id, ldapLoginId, ldapPassword); err != nil {
return "", err
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Could not send sign in method changed e-mail", mlog.Err(err))
}
})
return "/login?extra=signin_change", nil
}
func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.AuthService != model.UserAuthServiceLdap {
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "", http.StatusBadRequest)
}
ldapInterface := a.Ldap()
if ldapInterface == nil || user.AuthData == nil {
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented)
}
if err := ldapInterface.CheckPasswordAuthData(*user.AuthData, ldapPassword); err != nil {
return "", err
}
if err := a.CheckUserMfa(user, code); err != nil {
return "", err
}
if err := a.UpdatePassword(user, newPassword); err != nil {
return "", err
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return "", err
}
T := i18n.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Could not send sign in method changed e-mail", mlog.Err(err))
}
})
return "/login?extra=signin_change", nil
}
func (a *App) MigrateIdLDAP(toAttribute string) *model.AppError {
if ldapI := a.Ldap(); ldapI != nil {
if err := ldapI.MigrateIDAttribute(toAttribute); err != nil {
switch err := err.(type) {
case *model.AppError:
return err
default:
return model.NewAppError("IdMigrateLDAP", "ent.ldap_id_migrate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
return model.NewAppError("IdMigrateLDAP", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
func (a *App) writeLdapFile(filename string, fileData *multipart.FileHeader) *model.AppError {
file, err := fileData.Open()
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(filename, data)
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeLdapFile(model.LdapPublicCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PublicCertificateFile = model.LdapPublicCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeLdapFile(model.LdapPrivateKeyName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PrivateKeyFile = model.LdapPrivateKeyName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) removeLdapFile(filename string) *model.AppError {
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("RemoveLdapFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveLdapPublicCertificate() *model.AppError {
if err := a.removeLdapFile(*a.Config().LdapSettings.PublicCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PublicCertificateFile = ""
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveLdapPrivateCertificate() *model.AppError {
if err := a.removeLdapFile(*a.Config().LdapSettings.PrivateKeyFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PrivateKeyFile = ""
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
)
// ensure the license service wrapper implements `product.LicenseService`
var _ product.LicenseService = (*licenseWrapper)(nil)
// licenseWrapper is an adapter struct that only exposes the
// config related functionality to be passed down to other products.
type licenseWrapper struct {
srv *Server
}
func (w *licenseWrapper) Name() product.ServiceKey {
return product.LicenseKey
}
func (w *licenseWrapper) GetLicense() *model.License {
return w.srv.License()
}
func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
if !termsAccepted {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request.terms-not-accepted", nil, "", http.StatusBadRequest)
}
if users == 0 {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
}
requester, err := w.srv.userService.GetUser(requesterID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("RequestTrialLicense", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("RequestTrialLicense", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
trialLicenseRequest := &model.TrialLicenseRequest{
ServerID: w.srv.TelemetryId(),
Name: requester.GetDisplayName(model.ShowFullName),
Email: requester.Email,
SiteName: *w.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL,
Users: users,
TermsAccepted: termsAccepted,
ReceiveEmailsAccepted: receiveEmailsAccepted,
}
return w.srv.platform.RequestTrialLicense(trialLicenseRequest)
}
// JWTClaims custom JWT claims with the needed information for the
// renewal process
type JWTClaims struct {
LicenseID string `json:"license_id"`
ActiveUsers int64 `json:"active_users"`
jwt.StandardClaims
}
func (s *Server) License() *model.License {
return s.platform.License()
}
func (s *Server) LoadLicense() {
s.platform.LoadLicense()
}
func (s *Server) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
return s.platform.SaveLicense(licenseBytes)
}
func (s *Server) SetLicense(license *model.License) bool {
return s.platform.SetLicense(license)
}
func (s *Server) ValidateAndSetLicenseBytes(b []byte) bool {
return s.platform.ValidateAndSetLicenseBytes(b)
}
func (s *Server) SetClientLicense(m map[string]string) {
s.platform.SetClientLicense(m)
}
func (s *Server) ClientLicense() map[string]string {
return s.platform.ClientLicense()
}
func (s *Server) RemoveLicense() *model.AppError {
return s.platform.RemoveLicense()
}
func (s *Server) AddLicenseListener(listener func(oldLicense, newLicense *model.License)) string {
return s.platform.AddLicenseListener(listener)
}
func (s *Server) RemoveLicenseListener(id string) {
s.platform.RemoveLicenseListener(id)
}
func (s *Server) GetSanitizedClientLicense() map[string]string {
return s.platform.GetSanitizedClientLicense()
}
// GenerateRenewalToken returns a renewal token that expires after duration expiration
func (s *Server) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) {
return s.platform.GenerateRenewalToken(expiration)
}
// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license
func (s *Server) GenerateLicenseRenewalLink() (string, string, *model.AppError) {
return s.platform.GenerateLicenseRenewalLink()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const cwsTokenEnv = "CWS_CLOUD_TOKEN"
func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) {
pem := r.Header.Get("X-SSL-Client-Cert") // mapped to $ssl_client_cert from nginx
subject := r.Header.Get("X-SSL-Client-Cert-Subject-DN") // mapped to $ssl_client_s_dn from nginx
email := ""
if subject != "" {
for _, v := range strings.Split(subject, "/") {
kv := strings.Split(v, "=")
if len(kv) == 2 && kv[0] == "emailAddress" {
email = kv[1]
}
}
}
return pem, subject, email
}
func (a *App) AuthenticateUserForLogin(c *request.Context, id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
// Do statistics
defer func() {
if a.Metrics() != nil {
if user == nil || err != nil {
a.Metrics().IncrementLoginFail()
} else {
a.Metrics().IncrementLogin()
}
}
}()
if password == "" && !IsCWSLogin(a, cwsToken) {
return nil, model.NewAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "", http.StatusBadRequest)
}
// Get the MM user we are trying to login
if user, err = a.GetUserForLogin(id, loginId); err != nil {
return nil, err
}
// CWS login allow to use the one-time token to login the users when they're redirected to their
// installation for the first time
if IsCWSLogin(a, cwsToken) {
if err = checkUserNotBot(user); err != nil {
return nil, err
}
token, err := a.Srv().Store().Token().GetByToken(cwsToken)
if nfErr := new(store.ErrNotFound); err != nil && !errors.As(err, &nfErr) {
mlog.Debug("Error retrieving the cws token from the store", mlog.Err(err))
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// If token is stored in the database that means it was used
if token != nil {
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest)
}
envToken, ok := os.LookupEnv(cwsTokenEnv)
if ok && subtle.ConstantTimeCompare([]byte(envToken), []byte(cwsToken)) == 1 {
token = &model.Token{
Token: cwsToken,
CreateAt: model.GetMillis(),
Type: TokenTypeCWSAccess,
}
err := a.Srv().Store().Token().Save(token)
if err != nil {
mlog.Debug("Error storing the cws token in the store", mlog.Err(err))
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError)
}
return user, nil
}
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest)
}
// If client side cert is enable and it's checking as a primary source
// then trust the proxy and cert that the correct user is supplied and allow
// them access
if *a.Config().ExperimentalSettings.ClientSideCertEnable && *a.Config().ExperimentalSettings.ClientSideCertCheck == model.ClientSideCertCheckPrimaryAuth {
// Unless the user is a bot.
if err = checkUserNotBot(user); err != nil {
return nil, err
}
return user, nil
}
// and then authenticate them
if user, err = a.authenticateUser(c, user, password, mfaToken); err != nil {
return nil, err
}
return user, nil
}
func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) {
enableUsername := *a.Config().EmailSettings.EnableSignInWithUsername
enableEmail := *a.Config().EmailSettings.EnableSignInWithEmail
// If we are given a userID then fail if we can't find a user with that ID
if id != "" {
user, err := a.GetUser(id)
if err != nil {
if err.Id != MissingAccountError {
err.StatusCode = http.StatusInternalServerError
return nil, err
}
err.StatusCode = http.StatusBadRequest
return nil, err
}
return user, nil
}
// Try to get the user by username/email
if user, err := a.Srv().Store().User().GetForLogin(loginId, enableUsername, enableEmail); err == nil {
return user, nil
}
// Try to get the user with LDAP if enabled
if *a.Config().LdapSettings.Enable && a.Ldap() != nil {
if ldapUser, err := a.Ldap().GetUser(loginId); err == nil {
if user, err := a.GetUserByAuth(ldapUser.AuthData, model.UserAuthServiceLdap); err == nil {
return user, nil
}
return ldapUser, nil
}
}
return nil, model.NewAppError("GetUserForLogin", "store.sql_user.get_for_login.app_error", nil, "", http.StatusBadRequest)
}
func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) *model.AppError {
var rejectionReason string
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
rejectionReason = hooks.UserWillLogIn(pluginContext, user)
return rejectionReason == ""
}, plugin.UserWillLogInID)
if rejectionReason != "" {
return model.NewAppError("DoLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest)
}
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceID, IsOAuth: false, Props: map[string]string{
model.UserAuthServiceIsMobile: strconv.FormatBool(isMobile),
model.UserAuthServiceIsSaml: strconv.FormatBool(isSaml),
model.UserAuthServiceIsOAuth: strconv.FormatBool(isOAuthUser),
}}
session.GenerateCSRF()
if deviceID != "" {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthMobileInHours)
// A special case where we logout of all other sessions with the same Id
if err := a.RevokeSessionsForDeviceId(user.Id, deviceID, ""); err != nil {
err.StatusCode = http.StatusInternalServerError
return err
}
} else if isMobile {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthMobileInHours)
} else if isOAuthUser || isSaml {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthSSOInHours)
} else {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthWebInHours)
}
ua := uasurfer.Parse(r.UserAgent())
plat := getPlatformName(ua)
os := getOSName(ua)
bname := getBrowserName(ua, r.UserAgent())
bversion := getBrowserVersion(ua, r.UserAgent())
session.AddProp(model.SessionPropPlatform, plat)
session.AddProp(model.SessionPropOs, os)
session.AddProp(model.SessionPropBrowser, fmt.Sprintf("%v/%v", bname, bversion))
if user.IsGuest() {
session.AddProp(model.SessionPropIsGuest, "true")
} else {
session.AddProp(model.SessionPropIsGuest, "false")
}
var err *model.AppError
if session, err = a.CreateSession(session); err != nil {
err.StatusCode = http.StatusInternalServerError
return err
}
w.Header().Set(model.HeaderToken, session.Token)
c.SetSession(session)
if a.Srv().License() != nil && *a.Srv().License().Features.LDAP && a.Ldap() != nil {
userVal := *user
sessionVal := *session
a.Srv().Go(func() {
a.Ldap().UpdateProfilePictureIfNecessary(c, userVal, sessionVal)
})
}
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLoggedIn(pluginContext, user)
return true
}, plugin.UserHasLoggedInID)
})
return nil
}
func (a *App) AttachCloudSessionCookie(c *request.Context, w http.ResponseWriter, r *http.Request) {
secure := false
if GetProtocol(r) == "https" {
secure = true
}
maxAgeSeconds := *a.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
domain := ""
if siteURL, err := url.Parse(a.GetSiteURL()); err == nil {
domain = siteURL.Hostname()
}
if domain == "" {
return
}
var workspaceName string
if strings.Contains(domain, "localhost") {
workspaceName = "localhost"
} else {
// ensure we have a format for a cloud workspace url i.e. example.cloud.mattermost.com
if len(strings.Split(domain, ".")) != 4 {
return
}
workspaceName = strings.SplitN(domain, ".", 2)[0]
domain = strings.SplitN(domain, ".", 3)[2]
domain = "." + domain
}
cookie := &http.Cookie{
Name: model.SessionCookieCloudUrl,
Value: workspaceName,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
http.SetCookie(w, cookie)
}
func (a *App) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r *http.Request) {
secure := false
if GetProtocol(r) == "https" {
secure = true
}
maxAgeSeconds := *a.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60
domain := a.GetCookieDomain()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: c.Session().Token,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Domain: domain,
Secure: secure,
}
userCookie := &http.Cookie{
Name: model.SessionCookieUser,
Value: c.Session().UserId,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
csrfCookie := &http.Cookie{
Name: model.SessionCookieCsrf,
Value: c.Session().GetCSRF(),
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
http.SetCookie(w, sessionCookie)
http.SetCookie(w, userCookie)
http.SetCookie(w, csrfCookie)
// For context see: https://mattermost.atlassian.net/browse/MM-39583
if a.License().IsCloud() {
a.AttachCloudSessionCookie(c, w, r)
}
}
func GetProtocol(r *http.Request) string {
if r.Header.Get(model.HeaderForwardedProto) == "https" || r.TLS != nil {
return "https"
}
return "http"
}
func IsCWSLogin(a *App, token string) bool {
return a.License().IsCloud() && token != ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"fmt"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete"
const GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete"
const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
const CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete"
const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
const FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete
const remainingSchemaMigrationsKey = "RemainingSchemaMigrations"
const postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete"
// This function migrates the default built in roles from code/config to the database.
func (a *App) DoAdvancedPermissionsMigration() {
a.Srv().doAdvancedPermissionsMigration()
}
func (s *Server) doAdvancedPermissionsMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(model.AdvancedPermissionsMigrationKey); err == nil {
return
}
mlog.Info("Migrating roles to database.")
roles := model.MakeDefaultRoles()
allSucceeded := true
for _, role := range roles {
_, err := s.Store().Role().Save(role)
if err == nil {
continue
}
// If this failed for reasons other than the role already existing, don't mark the migration as done.
fetchedRole, err := s.Store().Role().GetByName(context.Background(), role.Name)
if err != nil {
mlog.Fatal("Failed to migrate role to database.", mlog.Err(err))
allSucceeded = false
continue
}
// If the role already existed, check it is the same and update if not.
if !reflect.DeepEqual(fetchedRole.Permissions, role.Permissions) ||
fetchedRole.DisplayName != role.DisplayName ||
fetchedRole.Description != role.Description ||
fetchedRole.SchemeManaged != role.SchemeManaged {
role.Id = fetchedRole.Id
if _, err = s.Store().Role().Save(role); err != nil {
// Role is not the same, but failed to update.
mlog.Fatal("Failed to migrate role to database.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
config := s.platform.Config()
*config.ServiceSettings.PostEditTimeLimit = -1
if _, _, err := s.platform.SaveConfig(config, true); err != nil {
mlog.Error("Failed to update config in Advanced Permissions Phase 1 Migration.", mlog.Err(err))
}
system := model.System{
Name: model.AdvancedPermissionsMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark advanced permissions migration as completed.", mlog.Err(err))
}
}
func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
if !isComplete {
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.MigrationKeyAdvancedPermissionsPhase2); err != nil {
return err
}
}
a.Srv().phase2PermissionsMigrationComplete = isComplete
return nil
}
func (a *App) DoEmojisPermissionsMigration() {
a.Srv().doEmojisPermissionsMigration()
}
func (s *Server) doEmojisPermissionsMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(EmojisPermissionsMigrationKey); err == nil {
return
}
var role *model.Role
var systemAdminRole *model.Role
var err *model.AppError
mlog.Info("Migrating emojis config to database.")
// Emoji creation is set to all by default
role, err = s.GetRoleByName(context.Background(), model.SystemUserRoleId)
if err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
if role != nil {
role.Permissions = append(role.Permissions, model.PermissionCreateEmojis.Id, model.PermissionDeleteEmojis.Id)
if _, nErr := s.Store().Role().Save(role); nErr != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(nErr))
return
}
}
systemAdminRole, err = s.GetRoleByName(context.Background(), model.SystemAdminRoleId)
if err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
systemAdminRole.Permissions = append(systemAdminRole.Permissions,
model.PermissionCreateEmojis.Id,
model.PermissionDeleteEmojis.Id,
model.PermissionDeleteOthersEmojis.Id,
)
if _, err := s.Store().Role().Save(systemAdminRole); err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
system := model.System{
Name: EmojisPermissionsMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark emojis permissions migration as completed.", mlog.Err(err))
}
}
func (a *App) DoGuestRolesCreationMigration() {
a.Srv().doGuestRolesCreationMigration()
}
func (s *Server) doGuestRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(GuestRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.ChannelGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.ChannelGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.TeamGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.TeamGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
schemes, err := s.Store().Scheme().GetAllPage("", 0, 1000000)
if err != nil {
mlog.Fatal("Failed to get all schemes.", mlog.Err(err))
allSucceeded = false
}
for _, scheme := range schemes {
if scheme.DefaultTeamGuestRole == "" || scheme.DefaultChannelGuestRole == "" {
if scheme.Scope == model.SchemeScopeTeam {
// Team Guest Role
teamGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Team Guest Role for Scheme %s", scheme.Name),
Permissions: roles[model.TeamGuestRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(teamGuestRole); err != nil {
mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultTeamGuestRole = savedRole.Name
}
}
// Channel Guest Role
channelGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Guest Role for Scheme %s", scheme.Name),
Permissions: roles[model.ChannelGuestRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(channelGuestRole); err != nil {
mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultChannelGuestRole = savedRole.Name
}
_, err := s.Store().Scheme().Save(scheme)
if err != nil {
mlog.Fatal("Failed to update custom scheme.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
system := model.System{
Name: GuestRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark guest roles creation migration as completed.", mlog.Err(err))
}
}
func (a *App) DoSystemConsoleRolesCreationMigration() {
a.Srv().doSystemConsoleRolesCreationMigration()
}
func (s *Server) doSystemConsoleRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(SystemConsoleRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemManagerRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemManagerRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemManagerRoleId))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemReadOnlyAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemReadOnlyAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemReadOnlyAdminRoleId))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemUserManagerRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemUserManagerRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemUserManagerRoleId))
allSucceeded = false
}
}
if !allSucceeded {
return
}
system := model.System{
Name: SystemConsoleRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark system console roles creation migration as completed.", mlog.Err(err))
}
}
func (s *Server) doCustomGroupAdminRoleCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(CustomGroupAdminRoleCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemCustomGroupAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemCustomGroupAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemCustomGroupAdminRoleId))
allSucceeded = false
}
}
if !allSucceeded {
return
}
system := model.System{
Name: CustomGroupAdminRoleCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark custom group admin role creation migration as completed.", mlog.Err(err))
}
}
func (s *Server) doContentExtractionConfigDefaultTrueMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(ContentExtractionConfigDefaultTrueMigrationKey); err == nil {
return
}
s.platform.UpdateConfig(func(config *model.Config) {
config.FileSettings.ExtractContent = model.NewBool(true)
})
system := model.System{
Name: ContentExtractionConfigDefaultTrueMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark content extraction config migration as completed.", mlog.Err(err))
}
}
func (s *Server) doPlaybooksRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(PlaybookRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.PlaybookAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new playbook admin role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookMemberRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.PlaybookMemberRoleId]); err != nil {
mlog.Fatal("Failed to create new playbook member role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.RunAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.RunAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new run admin role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.RunMemberRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.RunMemberRoleId]); err != nil {
mlog.Fatal("Failed to create new run member role to database.", mlog.Err(err))
allSucceeded = false
}
}
schemes, err := s.Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1000000)
if err != nil {
mlog.Fatal("Failed to get all schemes.", mlog.Err(err))
allSucceeded = false
}
for _, scheme := range schemes {
if scheme.Scope == model.SchemeScopeTeam {
if scheme.DefaultPlaybookAdminRole == "" {
playbookAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Playbook Admin Role for Scheme %s", scheme.Name),
Permissions: roles[model.PlaybookAdminRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(playbookAdminRole); err != nil {
mlog.Fatal("Failed to create new playbook admin role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultPlaybookAdminRole = savedRole.Name
}
}
if scheme.DefaultPlaybookMemberRole == "" {
playbookMember := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Playbook Member Role for Scheme %s", scheme.Name),
Permissions: roles[model.PlaybookMemberRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(playbookMember); err != nil {
mlog.Fatal("Failed to create new playbook member role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultPlaybookMemberRole = savedRole.Name
}
}
if scheme.DefaultRunAdminRole == "" {
runAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Run Admin Role for Scheme %s", scheme.Name),
Permissions: roles[model.RunAdminRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(runAdminRole); err != nil {
mlog.Fatal("Failed to create new run admin role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultRunAdminRole = savedRole.Name
}
}
if scheme.DefaultRunMemberRole == "" {
runMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Run Member Role for Scheme %s", scheme.Name),
Permissions: roles[model.RunMemberRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(runMemberRole); err != nil {
mlog.Fatal("Failed to create new run member role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultRunMemberRole = savedRole.Name
}
}
_, err := s.Store().Scheme().Save(scheme)
if err != nil {
mlog.Fatal("Failed to update custom scheme.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
system := model.System{
Name: PlaybookRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark playbook roles creation migration as completed.", mlog.Err(err))
}
}
// arbitrary choice, though if there is an longstanding installation with less than 10 messages,
// putting the first admin through onboarding shouldn't be very disruptive.
const existingInstallationPostsThreshold = 10
func (s *Server) doFirstAdminSetupCompleteMigration() {
// Don't run the migration until the flag is turned on.
if !s.platform.Config().FeatureFlags.UseCaseOnboarding {
return
}
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(FirstAdminSetupCompleteKey); err == nil {
return
}
teams, err := s.Store().Team().GetAll()
if err != nil {
// can not confirm that admin has started in this case.
return
}
if len(teams) == 0 {
// No teams, and no existing preference. This is most likely a new instance.
// So do not mark that the admin has already done the first time setup.
return
}
// if there are teams, then if this isn't a new installation, there should be posts
postCount, err := s.Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil || postCount < existingInstallationPostsThreshold {
return
}
system := model.System{
Name: FirstAdminSetupCompleteKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark first admin setup migration as completed.", mlog.Err(err))
}
}
func (s *Server) doRemainingSchemaMigrations() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(remainingSchemaMigrationsKey); err == nil {
return
}
if teams, err := s.Store().Team().GetByEmptyInviteID(); err != nil {
mlog.Error("Error fetching Teams without InviteID", mlog.Err(err))
} else {
for _, team := range teams {
team.InviteId = model.NewId()
if _, err := s.Store().Team().Update(team); err != nil {
mlog.Error("Error updating Team InviteIDs", mlog.String("team_id", team.Id), mlog.Err(err))
}
}
}
system := model.System{
Name: remainingSchemaMigrationsKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark the remaining schema migrations as completed.", mlog.Err(err))
}
}
func (s *Server) doPostPriorityConfigDefaultTrueMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(postPriorityConfigDefaultTrueMigrationKey); err == nil {
return
}
s.platform.UpdateConfig(func(config *model.Config) {
config.ServiceSettings.PostPriority = model.NewBool(true)
})
system := model.System{
Name: postPriorityConfigDefaultTrueMigrationKey,
Value: "true",
}
if err := s.Store().System().SaveOrUpdate(&system); err != nil {
mlog.Fatal("Failed to mark post priority config migration as completed.", mlog.Err(err))
}
}
func (a *App) DoAppMigrations() {
a.Srv().doAppMigrations()
}
func (s *Server) doAppMigrations() {
s.doAdvancedPermissionsMigration()
s.doEmojisPermissionsMigration()
s.doGuestRolesCreationMigration()
s.doSystemConsoleRolesCreationMigration()
s.doCustomGroupAdminRoleCreationMigration()
// This migration always must be the last, because can be based on previous
// migrations. For example, it needs the guest roles migration.
err := s.doPermissionsMigrations()
if err != nil {
mlog.Fatal("(app.App).DoPermissionsMigrations failed", mlog.Err(err))
}
s.doContentExtractionConfigDefaultTrueMigration()
s.doPlaybooksRolesCreationMigration()
s.doFirstAdminSetupCompleteMigration()
s.doRemainingSchemaMigrations()
s.doPostPriorityConfigDefaultTrueMigration()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"net/http"
"sort"
"strings"
"sync"
"unicode"
"unicode/utf8"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) canSendPushNotifications() bool {
if !*a.Config().EmailSettings.SendPushNotifications {
return false
}
pushServer := *a.Config().EmailSettings.PushNotificationServer
if license := a.Srv().License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
mlog.Warn("Push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server")
return false
}
return true
}
func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
// Do not send notifications in archived channels
if channel.DeleteAt > 0 {
return []string{}, nil
}
isCRTAllowed := *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
pchan := make(chan store.StoreResult, 1)
go func() {
props, err := a.Srv().Store().User().GetAllProfilesInChannel(context.Background(), channel.Id, true)
pchan <- store.StoreResult{Data: props, NErr: err}
close(pchan)
}()
cmnchan := make(chan store.StoreResult, 1)
go func() {
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
cmnchan <- store.StoreResult{Data: props, NErr: err}
close(cmnchan)
}()
var gchan chan store.StoreResult
if a.allowGroupMentions(c, post) {
gchan = make(chan store.StoreResult, 1)
go func() {
groupsMap, err := a.getGroupsAllowedForReferenceInChannel(channel, team)
gchan <- store.StoreResult{Data: groupsMap, NErr: err}
close(gchan)
}()
}
var fchan chan store.StoreResult
if len(post.FileIds) != 0 {
fchan = make(chan store.StoreResult, 1)
go func() {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(post.Id, true, false, true)
fchan <- store.StoreResult{Data: fileInfos, NErr: err}
close(fchan)
}()
}
var tchan chan store.StoreResult
if isCRTAllowed && post.RootId != "" {
tchan = make(chan store.StoreResult, 1)
go func() {
followers, err := a.Srv().Store().Thread().GetThreadFollowers(post.RootId, true)
tchan <- store.StoreResult{Data: followers, NErr: err}
close(tchan)
}()
}
result := <-pchan
if result.NErr != nil {
return nil, result.NErr
}
profileMap := result.Data.(map[string]*model.User)
result = <-cmnchan
if result.NErr != nil {
return nil, result.NErr
}
channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
followers := make(model.StringArray, 0)
if tchan != nil {
result = <-tchan
if result.NErr != nil {
return nil, result.NErr
}
followers = result.Data.([]string)
}
groups := make(map[string]*model.Group)
if gchan != nil {
result = <-gchan
if result.NErr != nil {
return nil, result.NErr
}
groups = result.Data.(map[string]*model.Group)
}
mentions := &ExplicitMentions{}
allActivityPushUserIds := []string{}
var allowChannelMentions bool
var keywords map[string][]string
if channel.Type == model.ChannelTypeDirect {
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
_, ok := profileMap[otherUserId]
if ok {
mentions.addMention(otherUserId, DMMention)
}
if post.GetProp("from_webhook") == "true" {
mentions.addMention(post.UserId, DMMention)
}
} else {
allowChannelMentions = a.allowChannelMentions(c, post, len(profileMap))
keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
mentions = getExplicitMentions(post, keywords, groups)
// Add an implicit mention when a user is added to a channel
// even if the user has set 'username mentions' to false in account settings.
if post.Type == model.PostTypeAddToChannel {
addedUserId, ok := post.GetProp(model.PostPropsAddedUserId).(string)
if ok {
mentions.addMention(addedUserId, KeywordMention)
}
}
// Iterate through all groups that were mentioned and insert group members into the list of mentions or potential mentions
for _, group := range mentions.GroupMentions {
anyUsersMentionedByGroup, err := a.insertGroupMentions(group, channel, profileMap, mentions)
if err != nil {
return nil, err
}
if !anyUsersMentionedByGroup {
a.sendNoUsersNotifiedByGroupInChannel(c, sender, post, channel, group)
}
}
// get users that have comment thread mentions enabled
if post.RootId != "" && parentPostList != nil {
for _, threadPost := range parentPostList.Posts {
profile := profileMap[threadPost.UserId]
if profile == nil {
continue
}
// If this is the root post and it was posted by an OAuth bot, don't notify the user
if threadPost.Id == parentPostList.Order[0] && threadPost.IsFromOAuthBot() {
continue
}
if a.IsCRTEnabledForUser(c, profile.Id) {
continue
}
if profile.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyAny || (profile.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyRoot && threadPost.Id == parentPostList.Order[0]) {
mentionType := ThreadMention
if threadPost.Id == parentPostList.Order[0] {
mentionType = CommentMention
}
mentions.addMention(threadPost.UserId, mentionType)
}
}
}
// prevent the user from mentioning themselves
if post.GetProp("from_webhook") != "true" {
mentions.removeMention(post.UserId)
}
go func() {
_, err := a.sendOutOfChannelMentions(c, sender, post, channel, mentions.OtherPotentialMentions)
if err != nil {
mlog.Error("Failed to send warning for out of channel mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
}
}()
// find which users in the channel are set up to always receive mobile notifications
// excludes CRT users since those should be added in notificationsForCRT
for _, profile := range profileMap {
if (profile.NotifyProps[model.PushNotifyProp] == model.UserNotifyAll ||
channelMemberNotifyPropsMap[profile.Id][model.PushNotifyProp] == model.ChannelNotifyAll) &&
(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
!post.IsSystemMessage() &&
!(a.IsCRTEnabledForUser(c, profile.Id) && post.RootId != "") {
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
}
}
}
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
mentionAutofollowChans := []chan *model.AppError{}
threadParticipants := map[string]bool{post.UserId: true}
newParticipants := map[string]bool{}
participantMemberships := map[string]*model.ThreadMembership{}
membershipsMutex := &sync.Mutex{}
followersMutex := &sync.Mutex{}
if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" {
var rootMentions *ExplicitMentions
if parentPostList != nil {
rootPost := parentPostList.Posts[parentPostList.Order[0]]
if rootPost.GetProp("from_webhook") != "true" {
threadParticipants[rootPost.UserId] = true
}
if channel.Type != model.ChannelTypeDirect {
rootMentions = getExplicitMentions(rootPost, keywords, groups)
for id := range rootMentions.Mentions {
threadParticipants[id] = true
}
}
}
for id := range mentions.Mentions {
threadParticipants[id] = true
}
// sema is a counting semaphore to throttle the number of concurrent DB requests.
// A concurrency of 8 should be sufficient.
// We don't want to set a higher limit which can bring down the DB.
sema := make(chan struct{}, 8)
// for each mention, make sure to update thread autofollow (if enabled) and update increment mention count
for id := range threadParticipants {
mac := make(chan *model.AppError, 1)
// Get token.
sema <- struct{}{}
go func(userID string) {
defer func() {
close(mac)
// Release token.
<-sema
}()
mentionType, incrementMentions := mentions.Mentions[userID]
// if the user was not explicitly mentioned, check if they explicitly unfollowed the thread
if !incrementMentions {
membership, err := a.Srv().Store().Thread().GetMembershipForUser(userID, post.RootId)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if membership != nil && !membership.Following {
return
}
}
updateFollowing := *a.Config().ServiceSettings.ThreadAutoFollow
if mentionType == ThreadMention || mentionType == CommentMention {
incrementMentions = false
updateFollowing = false
}
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: incrementMentions,
UpdateFollowing: updateFollowing,
UpdateViewedTimestamp: false,
UpdateParticipants: userID == post.UserId,
}
threadMembership, err := a.Srv().Store().Thread().MaintainMembership(userID, post.RootId, opts)
if err != nil {
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
followersMutex.Lock()
// add new followers to existing followers
if threadMembership.Following && !followers.Contains(userID) {
followers = append(followers, userID)
newParticipants[userID] = true
}
followersMutex.Unlock()
membershipsMutex.Lock()
participantMemberships[userID] = threadMembership
membershipsMutex.Unlock()
mac <- nil
}(id)
mentionAutofollowChans = append(mentionAutofollowChans, mac)
}
}
for id := range mentions.Mentions {
mentionedUsersList = append(mentionedUsersList, id)
}
nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "", post.IsUrgent())
if nErr != nil {
mlog.Warn(
"Failed to update mention count",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(nErr),
)
}
// Log the problems that might have occurred while auto following the thread
for _, mac := range mentionAutofollowChans {
if err := <-mac; err != nil {
mlog.Warn(
"Failed to update thread autofollow from mention",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(err),
)
}
}
notificationsForCRT := &CRTNotifiers{}
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
profile := profileMap[uid]
if profile == nil || !a.IsCRTEnabledForUser(c, uid) {
continue
}
if post.GetProp("from_webhook") != "true" && uid == post.UserId {
continue
}
// add user id to notificationsForCRT depending on threads notify props
notificationsForCRT.addFollowerToNotify(profile, mentions, channelMemberNotifyPropsMap[profile.Id], channel)
}
}
notification := &PostNotification{
Post: post.Clone(),
Channel: channel,
ProfileMap: profileMap,
Sender: sender,
}
if *a.Config().EmailSettings.SendEmailNotifications {
emailRecipients := append(mentionedUsersList, notificationsForCRT.Email...)
emailRecipients = model.RemoveDuplicateStrings(emailRecipients)
for _, id := range emailRecipients {
if profileMap[id] == nil {
continue
}
//If email verification is required and user email is not verified don't send email.
if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
mlog.Debug("Skipped sending notification email, address not verified.", mlog.String("user_email", profileMap[id].Email), mlog.String("user_id", id))
continue
}
if a.userAllowsEmail(c, profileMap[id], channelMemberNotifyPropsMap[id], post) {
senderProfileImage, _, err := a.GetProfileImage(sender)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(err))
}
if err := a.sendNotificationEmail(c, notification, profileMap[id], team, senderProfileImage); err != nil {
mlog.Warn("Unable to send notification email.", mlog.Err(err))
}
}
}
}
// Check for channel-wide mentions in channels that have too many members for those to work
if int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
T := i18n.GetUserTranslations(sender.Locale)
if mentions.HereMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_here", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
if mentions.ChannelMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_channel", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
if mentions.AllMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_all", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
}
if a.canSendPushNotifications() {
for _, id := range mentionedUsersList {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
mentionType := mentions.Mentions[id]
replyToThreadType := ""
if mentionType == ThreadMention {
replyToThreadType = model.CommentsNotifyAny
} else if mentionType == CommentMention {
replyToThreadType = model.CommentsNotifyRoot
}
a.sendPushNotification(
notification,
profileMap[id],
mentionType == KeywordMention || mentionType == ChannelMention || mentionType == DMMention,
mentionType == ChannelMention,
replyToThreadType,
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
for _, id := range allActivityPushUserIds {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
if _, ok := mentions.Mentions[id]; !ok {
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
a.sendPushNotification(
notification,
profileMap[id],
false,
false,
"",
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
for _, id := range notificationsForCRT.Push {
if profileMap[id] == nil {
continue
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if DoesStatusAllowPushNotification(profileMap[id].NotifyProps, status, post.ChannelId) {
a.sendPushNotification(
notification,
profileMap[id],
false,
false,
model.CommentsNotifyCRT,
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
// Note that PreparePostForClient should've already been called by this point
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
return nil, errors.Wrapf(jsonErr, "failed to encode post to JSON")
}
message.Add("post", postJSON)
message.Add("channel_type", channel.Type)
message.Add("channel_display_name", notification.GetChannelName(model.ShowUsername, ""))
message.Add("channel_name", channel.Name)
message.Add("sender_name", notification.GetSenderName(model.ShowUsername, *a.Config().ServiceSettings.EnablePostUsernameOverride))
message.Add("team_id", team.Id)
message.Add("set_online", setOnline)
if len(post.FileIds) != 0 && fchan != nil {
message.Add("otherFile", "true")
var infos []*model.FileInfo
if result := <-fchan; result.NErr != nil {
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(result.NErr))
} else {
infos = result.Data.([]*model.FileInfo)
}
for _, info := range infos {
if info.IsImage() {
message.Add("image", "true")
break
}
}
}
if len(mentionedUsersList) != 0 {
message.Add("mentions", model.ArrayToJSON(mentionedUsersList))
}
if len(notificationsForCRT.Desktop) != 0 {
message.Add("followers", model.ArrayToJSON(notificationsForCRT.Desktop))
}
published, err := a.publishWebsocketEventForPermalinkPost(c, post, message)
if err != nil {
return nil, err
}
if !published {
a.Publish(message)
}
// If this is a reply in a thread, notify participants
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
// A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil {
continue
}
if a.IsCRTEnabledForUser(c, uid) {
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, team.Id, "", uid, nil, "")
threadMembership := participantMemberships[uid]
if threadMembership == nil {
tm, err := a.Srv().Store().Thread().GetMembershipForUser(uid, post.RootId)
if err != nil {
return nil, errors.Wrapf(err, "Missing thread membership for participant in notifications. user_id=%q thread_id=%q", uid, post.RootId)
}
if tm == nil {
continue
}
threadMembership = tm
}
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
if err != nil {
return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid)
}
if userThread != nil {
previousUnreadMentions := int64(0)
previousUnreadReplies := int64(0)
// if it's not a newly followed thread, calculate previous unread values.
if !newParticipants[uid] {
previousUnreadMentions = userThread.UnreadMentions
previousUnreadReplies = max(userThread.UnreadReplies-1, 0)
if mentions.isUserMentioned(uid) {
previousUnreadMentions = max(userThread.UnreadMentions-1, 0)
}
}
// set LastViewed to now for commenter
if uid == post.UserId {
opts := store.ThreadMembershipOpts{
UpdateViewedTimestamp: true,
}
// should set unread mentions, and unread replies to 0
_, err = a.Srv().Store().Thread().MaintainMembership(uid, post.RootId, opts)
if err != nil {
return nil, errors.Wrapf(err, "cannot maintain thread membership %q for user %q", post.RootId, uid)
}
userThread.UnreadMentions = 0
userThread.UnreadReplies = 0
}
a.sanitizeProfiles(userThread.Participants, false)
userThread.Post.SanitizeProps()
sanitizedPost, err := a.SanitizePostMetadataForUser(c, userThread.Post, uid)
if err != nil {
return nil, err
}
userThread.Post = sanitizedPost
payload, jsonErr := json.Marshal(userThread)
if jsonErr != nil {
mlog.Warn("Failed to encode thread to JSON")
}
message.Add("thread", string(payload))
message.Add("previous_unread_mentions", previousUnreadMentions)
message.Add("previous_unread_replies", previousUnreadReplies)
a.Publish(message)
}
}
}
}
return mentionedUsersList, nil
}
func max(a, b int64) int64 {
if a < b {
return b
}
return a
}
func (a *App) userAllowsEmail(c request.CTX, user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool {
// if user is a bot account, then we do not send email
if user.IsBot {
return false
}
userAllowsEmails := user.NotifyProps[model.EmailNotifyProp] != "false"
// if CRT is ON for user and the post is a reply disregard the channelEmail setting
if channelEmail, ok := channelMemberNotificationProps[model.EmailNotifyProp]; ok && !(a.IsCRTEnabledForUser(c, user.Id) && post.RootId != "") {
if channelEmail != model.ChannelNotifyDefault {
userAllowsEmails = channelEmail != "false"
}
}
// Remove the user as recipient when the user has muted the channel.
if channelMuted, ok := channelMemberNotificationProps[model.MarkUnreadNotifyProp]; ok {
if channelMuted == model.ChannelMarkUnreadMention {
mlog.Debug("Channel muted for user", mlog.String("user_id", user.Id), mlog.String("channel_mute", channelMuted))
userAllowsEmails = false
}
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(user.Id); err != nil {
status = &model.Status{
UserId: user.Id,
Status: model.StatusOffline,
Manual: false,
LastActivityAt: 0,
ActiveChannel: "",
}
}
autoResponderRelated := status.Status == model.StatusOutOfOffice || post.Type == model.PostTypeAutoResponder
emailNotificationsAllowedForStatus := status.Status != model.StatusOnline && status.Status != model.StatusDnd
return userAllowsEmails && emailNotificationsAllowedForStatus && user.DeleteAt == 0 && !autoResponderRelated
}
func (a *App) sendNoUsersNotifiedByGroupInChannel(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, group *model.Group) {
T := i18n.GetUserTranslations(sender.Locale)
ephemeralPost := &model.Post{
UserId: sender.Id,
RootId: post.RootId,
ChannelId: channel.Id,
Message: T("api.post.check_for_out_of_channel_group_users.message.none", model.StringInterface{"GroupName": group.Name}),
}
a.SendEphemeralPost(c, post.UserId, ephemeralPost)
}
// sendOutOfChannelMentions sends an ephemeral post to the sender of a post if any of the given potential mentions
// are outside of the post's channel. Returns whether or not an ephemeral post was sent.
func (a *App) sendOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) {
outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(sender, post, channel, potentialMentions)
if err != nil {
return false, err
}
if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
return false, nil
}
a.SendEphemeralPost(c, post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers))
return true, nil
}
func (a *App) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
result := []*model.User{}
for _, user := range otherUsers {
canSee, err := a.UserCanSeeOtherUser(viewer.Id, user.Id)
if err != nil {
return nil, err
}
if canSee {
result = append(result, user)
}
}
return result, nil
}
func (a *App) filterOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, error) {
if post.IsSystemMessage() {
return nil, nil, nil
}
if channel.TeamId == "" || channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
return nil, nil, nil
}
if len(potentialMentions) == 0 {
return nil, nil, nil
}
users, err := a.Srv().Store().User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}})
if err != nil {
return nil, nil, err
}
// Filter out inactive users and bots
allUsers := model.UserSlice(users).FilterByActive(true)
allUsers = allUsers.FilterWithoutBots()
allUsers, appErr := a.FilterUsersByVisible(sender, allUsers)
if appErr != nil {
return nil, nil, appErr
}
if len(allUsers) == 0 {
return nil, nil, nil
}
// Differentiate between users who can and can't be added to the channel
var outOfChannelUsers model.UserSlice
var outOfGroupsUsers model.UserSlice
if channel.IsGroupConstrained() {
nonMemberIDs, err := a.FilterNonGroupChannelMembers(allUsers.IDs(), channel)
if err != nil {
return nil, nil, err
}
outOfChannelUsers = allUsers.FilterWithoutID(nonMemberIDs)
outOfGroupsUsers = allUsers.FilterByID(nonMemberIDs)
} else {
outOfChannelUsers = allUsers
}
return outOfChannelUsers, outOfGroupsUsers, nil
}
func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post {
allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
ocUsers := model.UserSlice(outOfChannelUsers)
ocUsernames := ocUsers.Usernames()
ocUserIDs := ocUsers.IDs()
ogUsers := model.UserSlice(outOfGroupsUsers)
ogUsernames := ogUsers.Usernames()
T := i18n.GetUserTranslations(sender.Locale)
ephemeralPostId := model.NewId()
var message string
if len(outOfChannelUsers) == 1 {
message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]any{
"Username": ocUsernames[0],
})
} else if len(outOfChannelUsers) > 1 {
preliminary, final := splitAtFinal(ocUsernames)
message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]any{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
if len(outOfGroupsUsers) == 1 {
if message != "" {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.one", map[string]any{
"Username": ogUsernames[0],
})
} else if len(outOfGroupsUsers) > 1 {
preliminary, final := splitAtFinal(ogUsernames)
if message != "" {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.multiple", map[string]any{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
props := model.StringInterface{
model.PropsAddChannelMember: model.StringInterface{
"post_id": ephemeralPostId,
"usernames": allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
"not_in_channel_usernames": ocUsernames,
"user_ids": allUsers.IDs(), // Kept for backwards compatibility of mobile app.
"not_in_channel_user_ids": ocUserIDs,
"not_in_groups_usernames": ogUsernames,
"not_in_groups_user_ids": ogUsers.IDs(),
},
}
return &model.Post{
Id: ephemeralPostId,
RootId: post.RootId,
ChannelId: post.ChannelId,
Message: message,
CreateAt: post.CreateAt + 1,
Props: props,
}
}
func splitAtFinal(items []string) (preliminary []string, final string) {
if len(items) == 0 {
return
}
preliminary = items[:len(items)-1]
final = items[len(items)-1]
return
}
type ExplicitMentions struct {
// Mentions contains the ID of each user that was mentioned and how they were mentioned.
Mentions map[string]MentionType
// Contains a map of groups that were mentioned
GroupMentions map[string]*model.Group
// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
// a corresponding keyword.
OtherPotentialMentions []string
// HereMentioned is true if the message contained @here.
HereMentioned bool
// AllMentioned is true if the message contained @all.
AllMentioned bool
// ChannelMentioned is true if the message contained @channel.
ChannelMentioned bool
}
type MentionType int
const (
// Different types of mentions ordered by their priority from lowest to highest
// A placeholder that should never be used in practice
NoMention MentionType = iota
// The post is in a thread that the user has commented on
ThreadMention
// The post is a comment on a thread started by the user
CommentMention
// The post contains an at-channel, at-all, or at-here
ChannelMention
// The post is a DM
DMMention
// The post contains an at-mention for the user
KeywordMention
// The post contains a group mention for the user
GroupMention
)
func (m *ExplicitMentions) isUserMentioned(userID string) bool {
if _, ok := m.Mentions[userID]; ok {
return true
}
if _, ok := m.GroupMentions[userID]; ok {
return true
}
return m.HereMentioned || m.AllMentioned || m.ChannelMentioned
}
func (m *ExplicitMentions) addMention(userID string, mentionType MentionType) {
if m.Mentions == nil {
m.Mentions = make(map[string]MentionType)
}
if currentType, ok := m.Mentions[userID]; ok && currentType >= mentionType {
return
}
m.Mentions[userID] = mentionType
}
func (m *ExplicitMentions) addGroupMention(word string, groups map[string]*model.Group) bool {
if strings.HasPrefix(word, "@") {
word = word[1:]
} else {
// Only allow group mentions when mentioned directly with @group-name
return false
}
group, groupFound := groups[word]
if !groupFound {
group = groups[strings.ToLower(word)]
}
if group == nil {
return false
}
if m.GroupMentions == nil {
m.GroupMentions = make(map[string]*model.Group)
}
if group.Name != nil {
m.GroupMentions[*group.Name] = group
}
return true
}
func (m *ExplicitMentions) addMentions(userIDs []string, mentionType MentionType) {
for _, userID := range userIDs {
m.addMention(userID, mentionType)
}
}
func (m *ExplicitMentions) removeMention(userID string) {
delete(m.Mentions, userID)
}
// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
func getExplicitMentions(post *model.Post, keywords map[string][]string, groups map[string]*model.Group) *ExplicitMentions {
ret := &ExplicitMentions{}
buf := ""
mentionsEnabledFields := getMentionsEnabledFields(post)
for _, message := range mentionsEnabledFields {
markdown.Inspect(message, func(node any) bool {
text, ok := node.(*markdown.Text)
if !ok {
ret.processText(buf, keywords, groups)
buf = ""
return true
}
buf += text.Text
return false
})
}
ret.processText(buf, keywords, groups)
return ret
}
// Given a post returns the values of the fields in which mentions are possible.
// post.message, preText and text in the attachment are enabled.
func getMentionsEnabledFields(post *model.Post) model.StringArray {
ret := []string{}
ret = append(ret, post.Message)
for _, attachment := range post.Attachments() {
if attachment.Pretext != "" {
ret = append(ret, attachment.Pretext)
}
if attachment.Text != "" {
ret = append(ret, attachment.Text)
}
}
return ret
}
// allowChannelMentions returns whether or not the channel mentions are allowed for the given post.
func (a *App) allowChannelMentions(c request.CTX, post *model.Post, numProfiles int) bool {
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseChannelMentions) {
return false
}
if post.Type == model.PostTypeHeaderChange || post.Type == model.PostTypePurposeChange {
return false
}
if int64(numProfiles) >= *a.Config().TeamSettings.MaxNotificationsPerChannel {
return false
}
return true
}
// allowGroupMentions returns whether or not the group mentions are allowed for the given post.
func (a *App) allowGroupMentions(c request.CTX, post *model.Post) bool {
if license := a.Srv().License(); license == nil || (license.SkuShortName != model.LicenseShortSkuProfessional && license.SkuShortName != model.LicenseShortSkuEnterprise) {
return false
}
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseGroupMentions) {
return false
}
if post.Type == model.PostTypeHeaderChange || post.Type == model.PostTypePurposeChange {
return false
}
return true
}
// getGroupsAllowedForReferenceInChannel returns a map of groups allowed for reference in a given channel and team.
func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team *model.Team) (map[string]*model.Group, error) {
var err error
groupsMap := make(map[string]*model.Group)
opts := model.GroupSearchOpts{FilterAllowReference: true, IncludeMemberCount: true}
if channel.IsGroupConstrained() || (team != nil && team.IsGroupConstrained()) {
var groups []*model.GroupWithSchemeAdmin
if channel.IsGroupConstrained() {
groups, err = a.Srv().Store().Group().GetGroupsByChannel(channel.Id, opts)
} else {
groups, err = a.Srv().Store().Group().GetGroupsByTeam(team.Id, opts)
}
if err != nil {
return nil, errors.Wrap(err, "unable to get groups")
}
for _, group := range groups {
if group.Group.Name != nil {
groupsMap[*group.Group.Name] = &group.Group
}
}
return groupsMap, nil
}
groups, err := a.Srv().Store().Group().GetGroups(0, 0, opts, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to get groups")
}
for _, group := range groups {
if group.Name != nil {
groupsMap[*group.Name] = group
}
}
return groupsMap, nil
}
// Given a map of user IDs to profiles, returns a list of mention
// keywords for all users in the channel.
func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
keywords := make(map[string][]string)
for _, profile := range profiles {
addMentionKeywordsForUser(
keywords,
profile,
channelMemberNotifyPropsMap[profile.Id],
a.GetStatusFromCache(profile.Id),
allowChannelMentions,
)
}
return keywords
}
// insertGroupMentions adds group members in the channel to Mentions, adds group members not in the channel to OtherPotentialMentions
// returns false if no group members present in the team that the channel belongs to
func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *ExplicitMentions) (bool, *model.AppError) {
var err error
var groupMembers []*model.User
outOfChannelGroupMembers := []*model.User{}
isGroupOrDirect := channel.IsGroupOrDirect()
if isGroupOrDirect {
groupMembers, err = a.Srv().Store().Group().GetMemberUsers(group.Id)
} else {
groupMembers, err = a.Srv().Store().Group().GetMemberUsersInTeam(group.Id, channel.TeamId)
}
if err != nil {
return false, model.NewAppError("insertGroupMentions", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if mentions.Mentions == nil {
mentions.Mentions = make(map[string]MentionType)
}
for _, member := range groupMembers {
if _, ok := profileMap[member.Id]; ok {
mentions.Mentions[member.Id] = GroupMention
} else {
outOfChannelGroupMembers = append(outOfChannelGroupMembers, member)
}
}
potentialGroupMembersMentioned := []string{}
for _, user := range outOfChannelGroupMembers {
potentialGroupMembersMentioned = append(potentialGroupMembersMentioned, user.Username)
}
if mentions.OtherPotentialMentions == nil {
mentions.OtherPotentialMentions = potentialGroupMembersMentioned
} else {
mentions.OtherPotentialMentions = append(mentions.OtherPotentialMentions, potentialGroupMembersMentioned...)
}
return isGroupOrDirect || len(groupMembers) > 0, nil
}
// addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) map[string][]string {
userMention := "@" + strings.ToLower(profile.Username)
keywords[userMention] = append(keywords[userMention], profile.Id)
// Add all the user's mention keys
for _, k := range profile.GetMentionKeys() {
// note that these are made lower case so that we can do a case insensitive check for them
key := strings.ToLower(k)
if key != "" {
keywords[key] = append(keywords[key], profile.Id)
}
}
// If turned on, add the user's case sensitive first name
if profile.NotifyProps[model.FirstNameNotifyProp] == "true" && profile.FirstName != "" {
keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
}
// Add @channel and @all to keywords if user has them turned on and the server allows them
if allowChannelMentions {
// Ignore channel mentions if channel is muted and channel mention setting is default
ignoreChannelMentions := channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsOn || (channelNotifyProps[model.MarkUnreadNotifyProp] == model.UserNotifyMention && channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsDefault)
if profile.NotifyProps[model.ChannelMentionsNotifyProp] == "true" && !ignoreChannelMentions {
keywords["@channel"] = append(keywords["@channel"], profile.Id)
keywords["@all"] = append(keywords["@all"], profile.Id)
if status != nil && status.Status == model.StatusOnline {
keywords["@here"] = append(keywords["@here"], profile.Id)
}
}
}
return keywords
}
// Represents either an email or push notification and contains the fields required to send it to any user.
type PostNotification struct {
Channel *model.Channel
Post *model.Post
ProfileMap map[string]*model.User
Sender *model.User
}
// Returns the name of the channel for this notification. For direct messages, this is the sender's name
// preceded by an at sign. For group messages, this is a comma-separated list of the members of the
// channel, with an option to exclude the recipient of the message from that list.
func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string {
switch n.Channel.Type {
case model.ChannelTypeDirect:
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
case model.ChannelTypeGroup:
names := []string{}
for _, user := range n.ProfileMap {
if user.Id != excludeId {
names = append(names, user.GetDisplayName(userNameFormat))
}
}
sort.Strings(names)
return strings.Join(names, ", ")
default:
return n.Channel.DisplayName
}
}
// Returns the name of the sender of this notification, accounting for things like system messages
// and whether or not the username has been overridden by an integration.
func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
if n.Post.IsSystemMessage() {
return i18n.T("system.message.name")
}
if overridesAllowed && n.Channel.Type != model.ChannelTypeDirect {
if value := n.Post.GetProps()["override_username"]; value != nil && n.Post.GetProp("from_webhook") == "true" {
if s, ok := value.(string); ok {
return s
}
}
}
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
}
// checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string, groups map[string]*model.Group) bool {
var mentionType MentionType
switch strings.ToLower(word) {
case "@here":
m.HereMentioned = true
mentionType = ChannelMention
case "@channel":
m.ChannelMentioned = true
mentionType = ChannelMention
case "@all":
m.AllMentioned = true
mentionType = ChannelMention
default:
mentionType = KeywordMention
}
m.addGroupMention(word, groups)
if ids, match := keywords[strings.ToLower(word)]; match {
m.addMentions(ids, mentionType)
return true
}
// Case-sensitive check for first name
if ids, match := keywords[word]; match {
m.addMentions(ids, mentionType)
return true
}
return false
}
// isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
ids := []string{}
match := false
var multibyteKeywords []string
for keyword := range keywords {
if len(keyword) != utf8.RuneCountInString(keyword) {
multibyteKeywords = append(multibyteKeywords, keyword)
}
}
if len(word) != utf8.RuneCountInString(word) {
for _, key := range multibyteKeywords {
if strings.Contains(word, key) {
ids, match = keywords[key]
}
}
}
return ids, match
}
// Processes text to filter mentioned users and other potential mentions
func (m *ExplicitMentions) processText(text string, keywords map[string][]string, groups map[string]*model.Group) {
systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
}) {
// skip word with format ':word:' with an assumption that it is an emoji format only
if word[0] == ':' && word[len(word)-1] == ':' {
continue
}
word = strings.TrimLeft(word, ":.-_")
if m.checkForMention(word, keywords, groups) {
continue
}
foundWithoutSuffix := false
wordWithoutSuffix := word
for wordWithoutSuffix != "" && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
if m.checkForMention(wordWithoutSuffix, keywords, groups) {
foundWithoutSuffix = true
break
}
}
if foundWithoutSuffix {
continue
}
if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
// No need to bother about unicode as we are looking for ASCII characters.
last := word[len(word)-1]
switch last {
// If the word is possibly at the end of a sentence, remove that character.
case '.', '-', ':':
word = word[:len(word)-1]
}
m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
} else if strings.ContainsAny(word, ".-:") {
// This word contains a character that may be the end of a sentence, so split further
splitWords := strings.FieldsFunc(word, func(c rune) bool {
return c == '.' || c == '-' || c == ':'
})
for _, splitWord := range splitWords {
if m.checkForMention(splitWord, keywords, groups) {
continue
}
if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
}
}
}
if ids, match := isKeywordMultibyte(keywords, word); match {
m.addMentions(ids, KeywordMention)
}
}
}
func (a *App) GetNotificationNameFormat(user *model.User) string {
if !*a.Config().PrivacySettings.ShowFullName {
return model.ShowUsername
}
data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat)
if err != nil {
return *a.Config().TeamSettings.TeammateNameDisplay
}
return data.Value
}
type CRTNotifiers struct {
// Desktop contains the user IDs of thread followers to receive desktop notification
Desktop model.StringArray
// Email contains the user IDs of thread followers to receive email notification
Email model.StringArray
// Push contains the user IDs of thread followers to receive push notification
Push model.StringArray
}
func (c *CRTNotifiers) addFollowerToNotify(user *model.User, mentions *ExplicitMentions, channelMemberNotificationProps model.StringMap, channel *model.Channel) {
_, userWasMentioned := mentions.Mentions[user.Id]
notifyDesktop, notifyPush, notifyEmail := shouldUserNotifyCRT(user, userWasMentioned)
notifyChannelDesktop, notifyChannelPush := shouldChannelMemberNotifyCRT(channelMemberNotificationProps, userWasMentioned)
// respect the user global notify props when there are no channel specific ones (default)
// otherwise respect the channel member's notify props
if (channelMemberNotificationProps[model.DesktopNotifyProp] == model.ChannelNotifyDefault && notifyDesktop) || notifyChannelDesktop {
c.Desktop = append(c.Desktop, user.Id)
}
if notifyEmail {
c.Email = append(c.Email, user.Id)
}
// respect the user global notify props when there are no channel specific ones (default)
// otherwise respect the channel member's notify props
if (channelMemberNotificationProps[model.PushNotifyProp] == model.ChannelNotifyDefault && notifyPush) || notifyChannelPush {
c.Push = append(c.Push, user.Id)
}
}
// user global settings check for desktop, email, and push notifications
func shouldUserNotifyCRT(user *model.User, isMentioned bool) (notifyDesktop, notifyPush, notifyEmail bool) {
notifyDesktop = false
notifyPush = false
notifyEmail = false
desktop := user.NotifyProps[model.DesktopNotifyProp]
push := user.NotifyProps[model.PushNotifyProp]
shouldEmail := user.NotifyProps[model.EmailNotifyProp] == "true"
desktopThreads := user.NotifyProps[model.DesktopThreadsNotifyProp]
emailThreads := user.NotifyProps[model.EmailThreadsNotifyProp]
pushThreads := user.NotifyProps[model.PushThreadsNotifyProp]
// user should be notified via desktop notification in the case the notify prop is not set as no notify
// and either the user was mentioned or the CRT notify prop for desktop is set to all
if desktop != model.UserNotifyNone && (isMentioned || desktopThreads == model.UserNotifyAll || desktop == model.UserNotifyAll) {
notifyDesktop = true
}
// user should be notified via email when emailing is enabled and
// either the user was mentioned, or the CRT notify prop for email is set to all
if shouldEmail && (isMentioned || emailThreads == model.UserNotifyAll) {
notifyEmail = true
}
// user should be notified via push in the case the notify prop is not set as no notify
// and either the user was mentioned or the CRT push notify prop is set to all
if push != model.UserNotifyNone && (isMentioned || pushThreads == model.UserNotifyAll || push == model.UserNotifyAll) {
notifyPush = true
}
return
}
// channel specific settings check for desktop and push notifications
func shouldChannelMemberNotifyCRT(notifyProps model.StringMap, isMentioned bool) (notifyDesktop, notifyPush bool) {
notifyDesktop = false
notifyPush = false
desktop := notifyProps[model.DesktopNotifyProp]
push := notifyProps[model.PushNotifyProp]
desktopThreads := notifyProps[model.DesktopThreadsNotifyProp]
pushThreads := notifyProps[model.PushThreadsNotifyProp]
// user should be notified via desktop notification in the case the notify prop is not set as no notify or default
// and either the user was mentioned or the CRT notify prop for desktop is set to all
if desktop != model.ChannelNotifyDefault && desktop != model.ChannelNotifyNone && (isMentioned || desktopThreads == model.ChannelNotifyAll || desktop == model.ChannelNotifyAll) {
notifyDesktop = true
}
// user should be notified via push in the case the notify prop is not set as no notify or default
// and either the user was mentioned or the CRT push notify prop is set to all
if push != model.ChannelNotifyDefault && push != model.ChannelNotifyNone && (isMentioned || pushThreads == model.ChannelNotifyAll || push == model.ChannelNotifyAll) {
notifyPush = true
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"html"
"html/template"
"io"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
email "github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotification, user *model.User, team *model.Team, senderProfileImage []byte) error {
channel := notification.Channel
post := notification.Post
if channel.IsGroupOrDirect() {
teams, err := a.Srv().Store().Team().GetTeamsByUserId(user.Id)
if err != nil {
return errors.Wrap(err, "unable to get user teams")
}
// if the recipient isn't in the current user's team, just pick one
found := false
for i := range teams {
if teams[i].Id == team.Id {
found = true
break
}
}
if !found && len(teams) > 0 {
team = teams[0]
} else {
// in case the user hasn't joined any teams we send them to the select_team page
team = &model.Team{Name: "select_team", DisplayName: *a.Config().TeamSettings.SiteName}
}
}
if *a.Config().EmailSettings.EnableEmailBatching {
var sendBatched bool
if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval); err != nil {
// if the call fails, assume that the interval has not been explicitly set and batch the notifications
sendBatched = true
} else {
// if the user has chosen to receive notifications immediately, don't batch them
sendBatched = data.Value != model.PreferenceEmailIntervalNoBatchingSeconds
}
if sendBatched {
if err := a.Srv().EmailService.AddNotificationEmailToBatch(user, post, team); err == nil {
return nil
}
}
// fall back to sending a single email if we can't batch it for some reason
}
translateFunc := i18n.GetUserTranslations(user.Locale)
var useMilitaryTime bool
if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime); err != nil {
useMilitaryTime = true
} else {
useMilitaryTime = data.Value == "true"
}
nameFormat := a.GetNotificationNameFormat(user)
channelName := notification.GetChannelName(nameFormat, "")
senderName := notification.GetSenderName(nameFormat, *a.Config().ServiceSettings.EnablePostUsernameOverride)
emailNotificationContentsType := model.EmailNotificationContentsFull
if license := a.Srv().License(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
var subjectText string
if channel.Type == model.ChannelTypeDirect {
subjectText = getDirectMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, senderName, useMilitaryTime)
} else if channel.Type == model.ChannelTypeGroup {
subjectText = getGroupMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime)
} else if *a.Config().EmailSettings.UseChannelInEmailNotifications {
subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime)
} else {
subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime)
}
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if emailNotificationContentsType == model.EmailNotificationContentsFull && senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
landingURL := a.GetSiteURL() + "/landing#/" + team.Name
var bodyText, err = a.getNotificationEmailBody(c, user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc, senderPhoto)
if err != nil {
return errors.Wrap(err, "unable to render the email notification template")
}
templateString := "<%s@" + utils.GetHostnameFromSiteURL(a.GetSiteURL()) + ">"
messageID := ""
inReplyTo := ""
references := ""
if post.Id != "" {
messageID = fmt.Sprintf(templateString, post.Id)
}
if post.RootId != "" {
referencesVal := fmt.Sprintf(templateString, post.RootId)
inReplyTo = referencesVal
references = referencesVal
}
a.Srv().Go(func() {
if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles, messageID, inReplyTo, references, "Notification"); nErr != nil {
mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr))
}
})
if a.Metrics() != nil {
a.Metrics().IncrementPostSentEmail()
}
return nil
}
/**
* Computes the subject line for direct notification email messages
*/
func getDirectMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"SenderDisplayName": senderName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
return translateFunc("app.notification.subject.direct.full", subjectParameters)
}
/**
* Computes the subject line for group, public, and private email messages
*/
func getNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"TeamName": teamName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
return translateFunc("app.notification.subject.notification.full", subjectParameters)
}
/**
* Computes the subject line for group email messages
*/
func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, channelName string, emailNotificationContentsType string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
subjectParameters["ChannelName"] = channelName
return translateFunc("app.notification.subject.group_message.full", subjectParameters)
}
return translateFunc("app.notification.subject.group_message.generic", subjectParameters)
}
/**
* If the name is longer than i characters, replace remaining characters with ...
*/
func truncateUserNames(name string, i int) string {
runes := []rune(name)
if len(runes) > i {
newString := string(runes[:i])
return newString + "..."
}
return name
}
type postData struct {
SenderName string
ChannelName string
Message template.HTML
MessageURL string
SenderPhoto string
PostPhoto string
Time string
ShowChannelIcon bool
OtherChannelMembersCount int
MessageAttachments []*email.EmailMessageAttachment
}
/**
* Computes the email body for notification messages
*/
func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc, senderPhoto string) (string, error) {
pData := postData{
SenderName: truncateUserNames(senderName, 22),
SenderPhoto: senderPhoto,
}
t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc)
messageTime := map[string]any{
"Hour": t.Hour,
"Minute": t.Minute,
"TimeZone": t.TimeZone,
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
postMessage := a.GetMessageForNotification(post, translateFunc)
postMessage = html.EscapeString(postMessage)
mdPostMessage, mdErr := utils.MarkdownToHTML(postMessage, a.GetSiteURL())
if mdErr != nil {
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(mdErr))
mdPostMessage = postMessage
}
normalizedPostMessage, err := a.generateHyperlinkForChannels(c, mdPostMessage, teamName, landingURL)
if err != nil {
mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err))
normalizedPostMessage = mdPostMessage
}
pData.Message = template.HTML(normalizedPostMessage)
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
pData.MessageAttachments = email.ProcessMessageAttachments(post, a.GetSiteURL())
}
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
data.Props["SiteURL"] = a.GetSiteURL()
if teamName != "select_team" {
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
} else {
data.Props["ButtonURL"] = landingURL
}
data.Props["SenderName"] = senderName
data.Props["Button"] = translateFunc("api.templates.post_body.button")
data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title")
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
if channel.Type == model.ChannelTypeDirect {
// Direct Messages
data.Props["Title"] = translateFunc("app.notification.body.dm.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.dm.subTitle", map[string]any{"SenderName": senderName})
} else if channel.Type == model.ChannelTypeGroup {
// Group Messages
data.Props["Title"] = translateFunc("app.notification.body.group.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.group.subTitle", map[string]any{"SenderName": senderName})
} else {
// mentions
data.Props["Title"] = translateFunc("app.notification.body.mention.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.mention.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName})
pData.ChannelName = channelName
}
// Override title and subtile for replies with CRT enabled
if a.IsCRTEnabledForUser(c, recipient.Id) && post.RootId != "" {
// Title is the same in all cases
data.Props["Title"] = translateFunc("app.notification.body.thread.title", map[string]any{"SenderName": senderName})
if channel.Type == model.ChannelTypeDirect {
// Direct Reply
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_dm.subTitle", map[string]any{"SenderName": senderName})
} else if channel.Type == model.ChannelTypeGroup {
// Group Reply
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_gm.subTitle", map[string]any{"SenderName": senderName})
} else if emailNotificationContentsType == model.EmailNotificationContentsFull {
// Channel Reply with full content
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel_full.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName})
} else {
// Channel Reply with generic content
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel.subTitle", map[string]any{"SenderName": senderName})
}
}
// only include posts in notification email if email notification contents type is set to full
if emailNotificationContentsType == model.EmailNotificationContentsFull {
data.Props["Posts"] = []postData{pData}
} else {
data.Props["Posts"] = []postData{}
}
return a.Srv().TemplatesContainer().RenderToString("messages_notification", data)
}
type formattedPostTime struct {
Time time.Time
Year string
Month string
Day string
Hour string
Minute string
TimeZone string
}
func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bool, translateFunc i18n.TranslateFunc) formattedPostTime {
preferredTimezone := user.GetPreferredTimezone()
postTime := time.Unix(post.CreateAt/1000, 0)
zone, _ := postTime.Zone()
localTime := postTime
if preferredTimezone != "" {
loc, _ := time.LoadLocation(preferredTimezone)
if loc != nil {
localTime = postTime.In(loc)
zone, _ = localTime.Zone()
}
}
hour := localTime.Format("15")
period := ""
if !useMilitaryTime {
hour = localTime.Format("3")
period = " " + localTime.Format("PM")
}
return formattedPostTime{
Time: localTime,
Year: fmt.Sprintf("%d", localTime.Year()),
Month: translateFunc(localTime.Month().String()),
Day: fmt.Sprintf("%d", localTime.Day()),
Hour: hour,
Minute: fmt.Sprintf("%02d"+period, localTime.Minute()),
TimeZone: zone,
}
}
func (a *App) generateHyperlinkForChannels(c request.CTX, postMessage, teamName, teamURL string) (string, *model.AppError) {
team, err := a.GetTeamByName(teamName)
if err != nil {
return "", err
}
channelNames := model.ChannelMentions(postMessage)
if len(channelNames) == 0 {
return postMessage, nil
}
channels, err := a.GetChannelsByNames(c, channelNames, team.Id)
if err != nil {
return "", err
}
visited := make(map[string]bool)
for _, ch := range channels {
if !visited[ch.Id] && ch.Type == model.ChannelTypeOpen {
channelURL := teamURL + "/channels/" + ch.Name
channelHyperLink := fmt.Sprintf("<a href='%s'>%s</a>", channelURL, "~"+ch.Name)
postMessage = strings.Replace(postMessage, "~"+ch.Name, channelHyperLink, -1)
visited[ch.Id] = true
}
}
return postMessage, nil
}
func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
return a.Srv().EmailService.GetMessageForNotification(post, translateFunc)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type notificationType string
const (
notificationTypeClear notificationType = "clear"
notificationTypeMessage notificationType = "message"
notificationTypeUpdateBadge notificationType = "update_badge"
notificationTypeDummy notificationType = "dummy"
)
type PushNotificationsHub struct {
notificationsChan chan PushNotification
app *App // XXX: This will go away once push notifications move to their own package.
sema chan struct{}
stopChan chan struct{}
wg *sync.WaitGroup
semaWg *sync.WaitGroup
buffer int
}
type PushNotification struct {
notificationType notificationType
currentSessionId string
userID string
channelID string
rootID string
post *model.Post
user *model.User
channel *model.Channel
senderName string
channelName string
explicitMention bool
channelWideMention bool
replyToThreadType string
}
func (a *App) sendPushNotificationSync(c request.CTX, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) *model.AppError {
cfg := a.Config()
msg, appErr := a.BuildPushNotificationMessage(
c,
*cfg.EmailSettings.PushNotificationContents,
post,
user,
channel,
channelName,
senderName,
explicitMention,
channelWideMention,
replyToThreadType,
)
if appErr != nil {
return appErr
}
return a.sendPushNotificationToAllSessions(msg, user.Id, "")
}
func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, userID string, skipSessionId string) *model.AppError {
sessions, err := a.getMobileAppSessions(userID)
if err != nil {
return err
}
if msg == nil {
return model.NewAppError(
"pushNotification",
"api.push_notifications.message.parse.app_error",
nil,
"",
http.StatusBadRequest,
)
}
for _, session := range sessions {
// Don't send notifications to this session if it's expired or we want to skip it
if session.IsExpired() || (skipSessionId != "" && skipSessionId == session.Id) {
continue
}
// We made a copy to avoid decoding and parsing all the time
tmpMessage := msg.DeepCopy()
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
tmpMessage.AckId = model.NewId()
err := a.sendToPushProxy(tmpMessage, session)
if err != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", tmpMessage.PostId),
mlog.String("channelId", tmpMessage.ChannelId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", err.Error()),
)
continue
}
a.NotificationsLog().Info("Notification sent",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", tmpMessage.PostId),
mlog.String("channelId", tmpMessage.ChannelId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", model.PushSendSuccess),
)
if a.Metrics() != nil {
a.Metrics().IncrementPostSentPush()
}
}
return nil
}
func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) {
cfg := a.Config()
channel := notification.Channel
post := notification.Post
nameFormat := a.GetNotificationNameFormat(user)
channelName := notification.GetChannelName(nameFormat, user.Id)
senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride)
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeMessage,
post: post,
user: user,
channel: channel,
senderName: senderName,
channelName: channelName,
explicitMention: explicitMention,
channelWideMention: channelWideMention,
replyToThreadType: replyToThreadType,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, explicitMention, channelWideMention,
hasFiles bool, senderName string, channelType model.ChannelType, replyToThreadType string, userLocale i18n.TranslateFunc) string {
// If the post only has images then push an appropriate message
if postMessage == "" && hasFiles {
if channelType == model.ChannelTypeDirect {
return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ")
}
return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only")
}
if contentsConfig == model.FullNotification {
if channelType == model.ChannelTypeDirect && replyToThreadType != model.CommentsNotifyCRT {
return model.ClearMentionTags(postMessage)
}
return senderName + ": " + model.ClearMentionTags(postMessage)
}
if channelType == model.ChannelTypeDirect {
if replyToThreadType == model.CommentsNotifyCRT {
if contentsConfig == model.GenericNoChannelNotification {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread_dm")
}
return userLocale("api.post.send_notifications_and_forget.push_message")
}
if replyToThreadType == model.CommentsNotifyCRT {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
if channelWideMention {
return senderName + userLocale("api.post.send_notification_and_forget.push_channel_mention")
}
if explicitMention {
return senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention")
}
if replyToThreadType == model.CommentsNotifyRoot {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post")
}
if replyToThreadType == model.CommentsNotifyAny {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread")
}
if replyToThreadType == model.UserNotifyAll {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message")
}
func (a *App) getUserBadgeCount(userID string, isCRTEnabled bool) (int, *model.AppError) {
unreadCount, err := a.Srv().Store().User().GetUnreadCount(userID, isCRTEnabled)
if err != nil {
return 0, model.NewAppError("getUserBadgeCount", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
badgeCount := int(unreadCount)
if isCRTEnabled {
threadUnreadMentions, err := a.Srv().Store().Thread().GetTotalUnreadMentions(userID, "", model.GetUserThreadsOpts{})
if err != nil {
return 0, model.NewAppError("getUserBadgeCount", "app.user.get_thread_count_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
badgeCount += int(threadUnreadMentions)
}
return badgeCount, nil
}
func (a *App) clearPushNotificationSync(c request.CTX, currentSessionId, userID, channelID, rootID string) *model.AppError {
isCRTEnabled := a.IsCRTEnabledForUser(c, userID)
badgeCount, err := a.getUserBadgeCount(userID, isCRTEnabled)
if err != nil {
return model.NewAppError("clearPushNotificationSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Type: model.PushTypeClear,
Version: model.PushMessageV2,
ChannelId: channelID,
RootId: rootID,
ContentAvailable: 1,
Badge: badgeCount,
IsCRTEnabled: isCRTEnabled,
}
return a.sendPushNotificationToAllSessions(msg, userID, currentSessionId)
}
func (a *App) clearPushNotification(currentSessionId, userID, channelID, rootID string) {
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeClear,
currentSessionId: currentSessionId,
userID: userID,
channelID: channelID,
rootID: rootID,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (a *App) updateMobileAppBadgeSync(c request.CTX, userID string) *model.AppError {
badgeCount, err := a.getUserBadgeCount(userID, a.IsCRTEnabledForUser(c, userID))
if err != nil {
return model.NewAppError("updateMobileAppBadgeSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Type: model.PushTypeUpdateBadge,
Version: model.PushMessageV2,
Sound: "none",
ContentAvailable: 1,
Badge: badgeCount,
}
return a.sendPushNotificationToAllSessions(msg, userID, "")
}
func (a *App) UpdateMobileAppBadge(userID string) {
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeUpdateBadge,
userID: userID,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (s *Server) createPushNotificationsHub(c request.CTX) {
buffer := *s.platform.Config().EmailSettings.PushNotificationBuffer
hub := PushNotificationsHub{
notificationsChan: make(chan PushNotification, buffer),
app: New(ServerConnector(s.Channels())),
wg: new(sync.WaitGroup),
semaWg: new(sync.WaitGroup),
sema: make(chan struct{}, runtime.NumCPU()*8), // numCPU * 8 is a good amount of concurrency.
stopChan: make(chan struct{}),
buffer: buffer,
}
go hub.start(c)
s.PushNotificationsHub = hub
}
func (hub *PushNotificationsHub) start(c request.CTX) {
hub.wg.Add(1)
defer hub.wg.Done()
for {
select {
case notification := <-hub.notificationsChan:
// We just ignore dummy notifications.
// These are used to pump out any remaining notifications
// before we stop the hub.
if notification.notificationType == notificationTypeDummy {
continue
}
// Adding to the waitgroup first.
hub.semaWg.Add(1)
// Get token.
hub.sema <- struct{}{}
go func(notification PushNotification) {
defer func() {
// Release token.
<-hub.sema
// Now marking waitgroup as done.
hub.semaWg.Done()
}()
var err *model.AppError
switch notification.notificationType {
case notificationTypeClear:
err = hub.app.clearPushNotificationSync(c, notification.currentSessionId, notification.userID, notification.channelID, notification.rootID)
case notificationTypeMessage:
err = hub.app.sendPushNotificationSync(
c,
notification.post,
notification.user,
notification.channel,
notification.channelName,
notification.senderName,
notification.explicitMention,
notification.channelWideMention,
notification.replyToThreadType,
)
case notificationTypeUpdateBadge:
err = hub.app.updateMobileAppBadgeSync(c, notification.userID)
default:
mlog.Debug("Invalid notification type", mlog.String("notification_type", string(notification.notificationType)))
}
if err != nil {
mlog.Error("Unable to send push notification", mlog.String("notification_type", string(notification.notificationType)), mlog.Err(err))
}
}(notification)
case <-hub.stopChan:
return
}
}
}
func (hub *PushNotificationsHub) stop() {
// Drain the channel.
for i := 0; i < hub.buffer+1; i++ {
hub.notificationsChan <- PushNotification{
notificationType: notificationTypeDummy,
}
}
close(hub.stopChan)
// We need to wait for the outer for loop to exit first.
// We cannot just send struct{}{} to stopChan because there are
// other listeners to the channel. And sending just once
// will cause a race.
hub.wg.Wait()
// And then we wait for the semaphore to finish.
hub.semaWg.Wait()
}
func (s *Server) StopPushNotificationsHubWorkers() {
s.PushNotificationsHub.stop()
}
func (a *App) rawSendToPushProxy(msg *model.PushNotification) (model.PushResponse, error) {
msgJSON, err := json.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("failed to encode to JSON: %w", err)
}
url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.APIURLSuffixV1 + "/send_push"
request, err := http.NewRequest("POST", url, bytes.NewReader(msgJSON))
if err != nil {
return nil, err
}
resp, err := a.Srv().pushNotificationClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var pushResponse model.PushResponse
if err := json.NewDecoder(resp.Body).Decode(&pushResponse); err != nil {
return nil, fmt.Errorf("failed to decode from JSON: %w", err)
}
return pushResponse, nil
}
func (a *App) sendToPushProxy(msg *model.PushNotification, session *model.Session) error {
msg.ServerId = a.TelemetryId()
a.NotificationsLog().Info("Notification will be sent",
mlog.String("ackId", msg.AckId),
mlog.String("type", msg.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", msg.PostId),
mlog.String("status", model.PushSendPrepare),
)
pushResponse, err := a.rawSendToPushProxy(msg)
if err != nil {
return err
}
switch pushResponse[model.PushStatus] {
case model.PushStatusRemove:
a.AttachDeviceId(session.Id, "", session.ExpiresAt)
a.ClearSessionCacheForUser(session.UserId)
return errors.New("device was reported as removed")
case model.PushStatusFail:
return errors.New(pushResponse[model.PushStatusErrorMsg])
}
return nil
}
func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error {
if ack == nil {
return nil
}
a.NotificationsLog().Info("Notification received",
mlog.String("ackId", ack.Id),
mlog.String("type", ack.NotificationType),
mlog.String("deviceType", ack.ClientPlatform),
mlog.Int64("receivedAt", ack.ClientReceivedAt),
mlog.String("status", model.PushReceived),
)
ackJSON, err := json.Marshal(ack)
if err != nil {
return fmt.Errorf("failed to encode to JSON: %w", err)
}
request, err := http.NewRequest(
"POST",
strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.APIURLSuffixV1+"/ack",
bytes.NewReader(ackJSON),
)
if err != nil {
return err
}
resp, err := a.Srv().pushNotificationClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
// Reading the body to completion.
_, err = io.Copy(io.Discard, resp.Body)
return err
}
func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppError) {
sessions, err := a.Srv().Store().Session().GetSessionsWithActiveDeviceIds(userID)
if err != nil {
return nil, model.NewAppError("getMobileAppSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return sessions, nil
}
func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
}
func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
userNotifyProps := user.NotifyProps
userNotify := userNotifyProps[model.PushNotifyProp]
channelNotify, ok := channelNotifyProps[model.PushNotifyProp]
if !ok || channelNotify == "" {
channelNotify = model.ChannelNotifyDefault
}
// If the channel is muted do not send push notifications
if channelNotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
return false
}
if post.IsSystemMessage() {
return false
}
if channelNotify == model.UserNotifyNone {
return false
}
if channelNotify == model.ChannelNotifyMention && !wasMentioned {
return false
}
if userNotify == model.UserNotifyMention && channelNotify == model.ChannelNotifyDefault && !wasMentioned {
return false
}
if (userNotify == model.UserNotifyAll || channelNotify == model.ChannelNotifyAll) &&
(post.UserId != user.Id || post.GetProp("from_webhook") == "true") {
return true
}
if userNotify == model.UserNotifyNone &&
channelNotify == model.ChannelNotifyDefault {
return false
}
return true
}
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelID string) bool {
// If User status is DND or OOO return false right away
if status.Status == model.StatusDnd || status.Status == model.StatusOutOfOffice {
return false
}
pushStatus, ok := userNotifyProps[model.PushStatusNotifyProp]
if (pushStatus == model.StatusOnline || !ok) && (status.ActiveChannel != channelID || model.GetMillis()-status.LastActivityAt > model.StatusChannelTimeout) {
return true
}
if pushStatus == model.StatusAway && (status.Status == model.StatusAway || status.Status == model.StatusOffline) {
return true
}
if pushStatus == model.StatusOffline && status.Status == model.StatusOffline {
return true
}
return false
}
func (a *App) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
var msg *model.PushNotification
notificationInterface := a.ch.Notification
if (notificationInterface == nil || notificationInterface.CheckLicense() != nil) && contentsConfig == model.IdLoadedNotification {
contentsConfig = model.GenericNotification
}
if contentsConfig == model.IdLoadedNotification {
msg = a.buildIdLoadedPushNotificationMessage(c, channel, post, user)
} else {
msg = a.buildFullPushNotificationMessage(c, contentsConfig, post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType)
}
badgeCount, err := a.getUserBadgeCount(user.Id, a.IsCRTEnabledForUser(c, user.Id))
if err != nil {
return nil, model.NewAppError("BuildPushNotificationMessage", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg.Badge = badgeCount
return msg, nil
}
func (a *App) SendTestPushNotification(deviceID string) string {
if !a.canSendPushNotifications() {
return "false"
}
msg := &model.PushNotification{
Version: "2",
Type: model.PushTypeTest,
ServerId: a.TelemetryId(),
Badge: -1,
}
msg.SetDeviceIdAndPlatform(deviceID)
pushResponse, err := a.rawSendToPushProxy(msg)
if err != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("type", msg.Type),
mlog.String("deviceId", msg.DeviceId),
mlog.String("status", err.Error()),
)
return "unknown"
}
switch pushResponse[model.PushStatus] {
case model.PushStatusRemove:
return "false"
case model.PushStatusFail:
a.NotificationsLog().Error("Notification error",
mlog.String("type", msg.Type),
mlog.String("deviceId", msg.DeviceId),
mlog.String("status", pushResponse[model.PushStatusErrorMsg]),
)
return "unknown"
}
return "true"
}
func (a *App) buildIdLoadedPushNotificationMessage(c request.CTX, channel *model.Channel, post *model.Post, user *model.User) *model.PushNotification {
userLocale := i18n.GetUserTranslations(user.Locale)
msg := &model.PushNotification{
PostId: post.Id,
ChannelId: post.ChannelId,
RootId: post.RootId,
IsCRTEnabled: a.IsCRTEnabledForUser(c, user.Id),
Category: model.CategoryCanReply,
Version: model.PushMessageV2,
TeamId: channel.TeamId,
Type: model.PushTypeMessage,
IsIdLoaded: true,
SenderId: user.Id,
Message: userLocale("api.push_notification.id_loaded.default_message"),
}
return msg
}
func (a *App) buildFullPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) *model.PushNotification {
msg := &model.PushNotification{
Category: model.CategoryCanReply,
Version: model.PushMessageV2,
Type: model.PushTypeMessage,
TeamId: channel.TeamId,
ChannelId: channel.Id,
PostId: post.Id,
RootId: post.RootId,
SenderId: post.UserId,
IsCRTEnabled: false,
IsIdLoaded: false,
}
userLocale := i18n.GetUserTranslations(user.Locale)
cfg := a.Config()
if contentsConfig != model.GenericNoChannelNotification || channel.Type == model.ChannelTypeDirect {
msg.ChannelName = channelName
}
if a.IsCRTEnabledForUser(c, user.Id) {
msg.IsCRTEnabled = true
if post.RootId != "" {
if contentsConfig != model.GenericNoChannelNotification {
props := map[string]any{"channelName": channelName}
msg.ChannelName = userLocale("api.push_notification.title.collapsed_threads", props)
if channel.Type == model.ChannelTypeDirect {
msg.ChannelName = userLocale("api.push_notification.title.collapsed_threads_dm")
}
}
}
}
msg.SenderName = senderName
if ou, ok := post.GetProp("override_username").(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride {
msg.OverrideUsername = ou
msg.SenderName = ou
}
if oi, ok := post.GetProp("override_icon_url").(string); ok && *cfg.ServiceSettings.EnablePostIconOverride {
msg.OverrideIconURL = oi
}
if fw, ok := post.GetProp("from_webhook").(string); ok {
msg.FromWebhook = fw
}
postMessage := post.Message
stripped, err := utils.StripMarkdown(postMessage)
if err != nil {
mlog.Warn("Failed parse to markdown", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
postMessage = stripped
}
for _, attachment := range post.Attachments() {
if attachment.Fallback != "" {
postMessage += "\n" + attachment.Fallback
}
}
hasFiles := post.FileIds != nil && len(post.FileIds) > 0
msg.Message = a.getPushNotificationMessage(
contentsConfig,
postMessage,
explicitMention,
channelWideMention,
hasFiles,
msg.SenderName,
channel.Type,
replyToThreadType,
userLocale,
)
return msg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const lastTrialNotificationTimeStamp = "LAST_TRIAL_NOTIFICATION_TIMESTAMP"
const lastUpgradeNotificationTimeStamp = "LAST_UPGRADE_NOTIFICATION_TIMESTAMP"
const defaultNotifyAdminCoolOffDays = 14
func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
requiredFeature := notifyData.RequiredFeature
requiredPlan := notifyData.RequiredPlan
trial := notifyData.TrialNotification
isUserAlreadyNotified := a.UserAlreadyNotifiedOnRequiredFeature(userId, requiredFeature)
if isUserAlreadyNotified {
return model.NewAppError("app.SaveAdminNotification", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden)
}
_, appErr := a.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: userId,
RequiredPlan: requiredPlan,
RequiredFeature: requiredFeature,
Trial: trial,
})
if appErr != nil {
return appErr
}
return nil
}
func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError {
ctx := request.EmptyContext(a.Srv().Log())
currentSKU := "starter"
license := a.Srv().License()
if license != nil {
currentSKU = license.SkuShortName
}
workspaceName := ""
return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial)
}
func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
d, err := a.Srv().Store().NotifyAdmin().Save(data)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return d, nil
}
func filterNotificationData(data []*model.NotifyAdminData, test func(*model.NotifyAdminData) bool) (ret []*model.NotifyAdminData) {
for _, d := range data {
if test(d) {
ret = append(ret, d)
}
}
return
}
func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
if !a.CanNotifyAdmin(trial) {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, "Cannot notify yet", http.StatusForbidden)
}
sysadmins, appErr := a.GetUsersFromProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 100,
Role: model.SystemAdminRoleId,
Inactive: false,
})
if appErr != nil {
return appErr
}
systemBot, appErr := a.GetSystemBot()
if appErr != nil {
return appErr
}
now := model.GetMillis()
data, err := a.Srv().Store().NotifyAdmin().Get(trial)
if err != nil {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, err.Error(), http.StatusInternalServerError)
}
data = filterNotificationData(data, func(nad *model.NotifyAdminData) bool { return nad.RequiredPlan != currentSKU })
if len(data) == 0 {
a.Log().Warn("No notification data available")
return nil
}
userBasedPaidFeatureData, userBasedPluginData := a.groupNotifyAdminByUser(data)
featureBasedData := a.groupNotifyAdminByPaidFeature(data)
pluginBasedData := a.groupNotifyAdminByPlugin(data)
for _, admin := range sysadmins {
if len(userBasedPaidFeatureData) > 0 && len(featureBasedData) > 0 {
a.upgradePlanAdminNotifyPost(c, workspaceName, userBasedPaidFeatureData, featureBasedData, systemBot, admin, trial)
}
if len(userBasedPluginData) > 0 {
a.pluginInstallAdminNotifyPost(c, userBasedPluginData, pluginBasedData, systemBot, admin)
}
}
a.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
return nil
}
func (a *App) pluginInstallAdminNotifyPost(c *request.Context, userBasedData map[string][]*model.NotifyAdminData, pluginBasedPluginData map[string][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User) {
props := make(model.StringInterface)
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_plugins_by_plugin_ids"] = pluginBasedPluginData
props["requested_plugins_by_user_ids"] = userBasedData
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) upgradePlanAdminNotifyPost(c *request.Context, workspaceName string, userBasedData map[string][]*model.NotifyAdminData, featureBasedData map[model.MattermostFeature][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User, trial bool) {
props := make(model.StringInterface)
T := i18n.GetUserTranslations(admin.Locale)
message := T("app.cloud.upgrade_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.upgrade_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName}) // todo (allan): investigate if translations library can do this
}
if trial {
message = T("app.cloud.trial_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.trial_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
}
}
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
Message: message,
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_features"] = featureBasedData
props["trial"] = trial
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
data, err := a.Srv().Store().NotifyAdmin().GetDataByUserIdAndFeature(user, feature)
if err != nil {
return false
}
if len(data) > 0 {
return true // if we find data, it means this user already notified on the need for this feature
}
return false
}
func (a *App) CanNotifyAdmin(trial bool) bool {
systemVarName := lastUpgradeNotificationTimeStamp
if trial {
systemVarName = lastTrialNotificationTimeStamp
}
sysVal, sysValErr := a.Srv().Store().System().GetByName(systemVarName)
if sysValErr != nil {
var nfErr *store.ErrNotFound
if errors.As(sysValErr, &nfErr) { // if no timestamps have been recorded before, system is free to notify
return true
}
a.Log().Error("Cannot notify", mlog.Err(sysValErr))
return false
}
lastNotificationTimestamp, err := strconv.ParseFloat(sysVal.Value, 64)
if err != nil {
a.Log().Error("Cannot notify", mlog.Err(err))
return false
}
coolOffPeriodDaysEnv := os.Getenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
coolOffPeriodDays, parseError := strconv.ParseFloat(coolOffPeriodDaysEnv, 64)
if parseError != nil {
coolOffPeriodDays = defaultNotifyAdminCoolOffDays
}
daysToMillis := coolOffPeriodDays * 24 * 60 * 60 * 1000
timeDiff := model.GetMillis() - int64(lastNotificationTimestamp)
return timeDiff >= int64(daysToMillis)
}
func (a *App) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
systemVarName := lastUpgradeNotificationTimeStamp
if trial {
systemVarName = lastTrialNotificationTimeStamp
}
val := strconv.FormatInt(model.GetMillis(), 10)
sysVar := &model.System{Name: systemVarName, Value: val}
if err := a.Srv().Store().System().SaveOrUpdate(sysVar); err != nil {
a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
}
// All the requested features notifications are now sent in a post and can safely be removed except
// the plugin notify admin. We keep it as we do not want the same user to send the notification for the same plugin.
// We update the NotifyAdmin SentAt to keep track of it.
for pluginId := range pluginBasedData {
notifications := pluginBasedData[pluginId]
for _, notification := range notifications {
requiredFeature := notification.RequiredFeature
requiredPlan := notification.RequiredPlan
userId := notification.UserId
if err := a.Srv().Store().NotifyAdmin().Update(userId, requiredPlan, requiredFeature, now); err != nil {
a.Log().Error("Unable to update SentAt for work template feature", mlog.Err(err))
}
}
}
if err := a.Srv().Store().NotifyAdmin().DeleteBefore(trial, now); err != nil {
a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
}
}
func (a *App) groupNotifyAdminByUser(data []*model.NotifyAdminData) (map[string][]*model.NotifyAdminData, map[string][]*model.NotifyAdminData) {
userBasedPaidFeatureData := make(map[string][]*model.NotifyAdminData)
userBasedPluginData := make(map[string][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
userBasedPluginData[d.UserId] = append(userBasedPluginData[d.UserId], d)
} else {
userBasedPaidFeatureData[d.UserId] = append(userBasedPaidFeatureData[d.UserId], d)
}
}
return userBasedPaidFeatureData, userBasedPluginData
}
func (a *App) groupNotifyAdminByPaidFeature(data []*model.NotifyAdminData) map[model.MattermostFeature][]*model.NotifyAdminData {
myMap := make(map[model.MattermostFeature][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
continue
}
myMap[d.RequiredFeature] = append(myMap[d.RequiredFeature], d)
}
return myMap
}
func (a *App) groupNotifyAdminByPlugin(data []*model.NotifyAdminData) map[string][]*model.NotifyAdminData {
myMap := make(map[string][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
plugins := strings.Split(d.RequiredPlan, ",")
for _, plugin := range plugins {
myMap[plugin] = append(myMap[plugin], d)
}
}
}
return myMap
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
OAuthCookieMaxAgeSeconds = 30 * 60 // 30 minutes
CookieOAuth = "MMOAUTH"
OpenIDScope = "openid"
)
func (a *App) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
oauthApp, err := a.Srv().Store().OAuth().SaveApp(app)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.Srv().Store().OAuth().GetApp(appID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) UpdateOAuthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("UpdateOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
updatedApp.Id = oldApp.Id
updatedApp.CreatorId = oldApp.CreatorId
updatedApp.CreateAt = oldApp.CreateAt
updatedApp.ClientSecret = oldApp.ClientSecret
oauthApp, err := a.Srv().Store().OAuth().UpdateApp(updatedApp)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) DeleteOAuthApp(appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().OAuth().DeleteApp(appID); err != nil {
return model.NewAppError("DeleteOAuthApp", "app.oauth.delete_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().InvalidateAllCaches(); err != nil {
mlog.Warn("error in invalidating cache", mlog.Err(err))
}
return nil
}
func (a *App) GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetApps(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthApps", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetAppByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthAppsByCreator", "app.oauth.get_app_by_user.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
session, err := a.GetOAuthAccessTokenForImplicitFlow(userID, authRequest)
if err != nil {
return "", err
}
values := &url.Values{}
values.Add("access_token", session.Token)
values.Add("token_type", "bearer")
values.Add("expires_in", strconv.FormatInt((session.ExpiresAt-model.GetMillis())/1000, 10))
values.Add("scope", authRequest.Scope)
values.Add("state", authRequest.State)
return fmt.Sprintf("%s#%s", authRequest.RedirectURI, values.Encode()), nil
}
func (a *App) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
authData := &model.AuthData{UserId: userID, ClientId: authRequest.ClientId, CreateAt: model.GetMillis(), RedirectUri: authRequest.RedirectURI, State: authRequest.State, Scope: authRequest.Scope}
authData.Code = model.NewId() + model.NewId()
// parse authRequest.RedirectURI to handle query parameters see: https://mattermost.atlassian.net/browse/MM-46216
uri, err := url.Parse(authRequest.RedirectURI)
if err != nil {
return authRequest.RedirectURI + "?error=redirect_uri_parse_error&state=" + authRequest.State, nil
}
queryParams := uri.Query()
if _, err := a.Srv().Store().OAuth().SaveAuthData(authData); err != nil {
queryParams.Set("error", "server_error")
queryParams.Set("state", authRequest.State)
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
queryParams.Set("code", url.QueryEscape(authData.Code))
queryParams.Set("state", url.QueryEscape(authData.State))
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
func (a *App) AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if authRequest.Scope == "" {
authRequest.Scope = model.DefaultScope
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(authRequest.ClientId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
}
var redirectURI string
var err *model.AppError
switch authRequest.ResponseType {
case model.AuthCodeResponseType:
redirectURI, err = a.GetOAuthCodeRedirect(userID, authRequest)
case model.ImplicitResponseType:
redirectURI, err = a.GetOAuthImplicitRedirect(userID, authRequest)
default:
return authRequest.RedirectURI + "?error=unsupported_response_type&state=" + authRequest.State, nil
}
if err != nil {
mlog.Warn("error getting oauth redirect uri", mlog.Err(err))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
// This saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: userID,
Category: model.PreferenceCategoryAuthorizedOAuthApp,
Name: authRequest.ClientId,
Value: authRequest.Scope,
}
if nErr := a.Srv().Store().Preference().Save(model.Preferences{authorizedApp}); nErr != nil {
mlog.Warn("error saving store preference", mlog.Err(nErr))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
return redirectURI, nil
}
func (a *App) GetOAuthAccessTokenForImplicitFlow(userID string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.GetOAuthApp(authRequest.ClientId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
session, err := a.newSession(oauthApp, user)
if err != nil {
return nil, err
}
accessData := &model.AccessData{ClientId: authRequest.ClientId, UserId: user.Id, Token: session.Token, RefreshToken: "", RedirectUri: authRequest.RedirectURI, ExpiresAt: session.ExpiresAt, Scope: authRequest.Scope}
if _, err := a.Srv().Store().OAuth().SaveAccessData(accessData); err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
return session, nil
}
func (a *App) GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken string) (*model.AccessResponse, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
if oauthApp.ClientSecret != secret {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusForbidden)
}
var accessData *model.AccessData
var accessRsp *model.AccessResponse
var user *model.User
if grantType == model.AccessTokenGrantType {
var authData *model.AuthData
authData, nErr = a.Srv().Store().OAuth().GetAuthData(code)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest)
}
if authData.IsExpired() {
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
if authData.RedirectUri != redirectURI {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
user, nErr = a.Srv().Store().User().Get(context.Background(), authData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
accessData, nErr = a.Srv().Store().OAuth().GetPreviousAccessData(user.Id, clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusBadRequest)
}
if accessData != nil {
if accessData.IsExpired() {
var access *model.AccessResponse
access, err := a.newSessionUpdateToken(oauthApp, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
} else {
// Return the same token and no need to create a new session
accessRsp = &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.AccessTokenType,
RefreshToken: accessData.RefreshToken,
ExpiresInSeconds: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
}
}
} else {
var session *model.Session
// Create a new session and return new access token
session, err := a.newSession(oauthApp, user)
if err != nil {
return nil, err
}
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectURI, ExpiresAt: session.ExpiresAt, Scope: authData.Scope}
if _, nErr = a.Srv().Store().OAuth().SaveAccessData(accessData); nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp = &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.AccessTokenType,
RefreshToken: accessData.RefreshToken,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
}
}
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
} else {
// When grantType is refresh_token
accessData, nErr = a.Srv().Store().OAuth().GetAccessDataByRefreshToken(refreshToken)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound)
}
user, nErr := a.Srv().Store().User().Get(context.Background(), accessData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
access, err := a.newSessionUpdateToken(oauthApp, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
}
return accessRsp, nil
}
func (a *App) newSession(app *model.OAuthApp, user *model.User) (*model.Session, *model.AppError) {
// Set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.GenerateCSRF()
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthSSOInHours)
session.AddProp(model.SessionPropPlatform, app.Name)
session.AddProp(model.SessionPropOAuthAppID, app.Id)
session.AddProp(model.SessionPropMattermostAppID, app.MattermostAppID)
session.AddProp(model.SessionPropOs, "OAuth2")
session.AddProp(model.SessionPropBrowser, "OAuth2")
session, err := a.Srv().Store().Session().Save(session)
if err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError)
}
a.ch.srv.platform.AddSessionToCache(session)
return session, nil
}
func (a *App) newSessionUpdateToken(app *model.OAuthApp, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
// Remove the previous session
if err := a.Srv().Store().Session().Remove(accessData.Token); err != nil {
mlog.Warn("error removing access data token from session", mlog.Err(err))
}
session, err := a.newSession(app, user)
if err != nil {
return nil, err
}
accessData.Token = session.Token
accessData.RefreshToken = model.NewId()
accessData.ExpiresAt = session.ExpiresAt
if _, err := a.Srv().Store().OAuth().UpdateAccessData(accessData); err != nil {
return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
RefreshToken: accessData.RefreshToken,
TokenType: model.AccessTokenType,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
}
return accessRsp, nil
}
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = action
if teamID != "" {
stateProps["team_id"] = teamID
}
if redirectTo != "" {
stateProps["redirect_to"] = redirectTo
}
stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint)
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionSignup
if teamID != "" {
stateProps["team_id"] = teamID
}
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetAuthorizedAppsForUser(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
apps, err := a.Srv().Store().OAuth().GetAuthorizedApps(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
return apps, nil
}
func (a *App) DeauthorizeOAuthAppForUser(userID, appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// Revoke app sessions
accessData, err := a.Srv().Store().OAuth().GetAccessDataByUserForApp(userID, appID)
if err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.get_access_data_by_user_for_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, ad := range accessData {
if err := a.RevokeAccessToken(ad.Token); err != nil {
return err
}
if err := a.Srv().Store().OAuth().RemoveAccessData(ad.Token); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_access_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().OAuth().RemoveAuthDataByClientId(appID, userID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_auth_data_by_client_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Deauthorize the app
if err := a.Srv().Store().Preference().Delete(userID, model.PreferenceCategoryAuthorizedOAuthApp, appID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.preference.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
if _, err := a.Srv().Store().OAuth().UpdateApp(app); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return app, nil
}
func (a *App) RevokeAccessToken(token string) *model.AppError {
if err := a.ch.srv.platform.RevokeAccessToken(token); err != nil {
switch {
case errors.Is(err, platform.GetTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, platform.DeleteTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) CompleteOAuth(c *request.Context, service string, body io.ReadCloser, teamID string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) {
defer body.Close()
action := props["action"]
switch action {
case model.OAuthActionSignup:
return a.CreateOAuthUser(c, service, body, teamID, tokenUser)
case model.OAuthActionLogin:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
case model.OAuthActionEmailToSSO:
return a.CompleteSwitchWithOAuth(service, body, props["email"], tokenUser)
case model.OAuthActionSSOToEmail:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
default:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
}
}
func (a *App) getSSOProvider(service string) (einterfaces.OAuthProvider, *model.AppError) {
sso := a.Config().GetSSOService(service)
if sso == nil || !*sso.Enable {
return nil, model.NewAppError("getSSOProvider", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
providerType := service
if strings.Contains(*sso.Scope, OpenIDScope) {
providerType = model.ServiceOpenid
}
provider := einterfaces.GetOAuthProvider(providerType)
if provider == nil {
return nil, model.NewAppError("getSSOProvider", "api.user.login_by_oauth.not_available.app_error",
map[string]any{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
}
return provider, nil
}
func (a *App) LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
buf := bytes.Buffer{}
if _, err := buf.ReadFrom(userData); err != nil {
return nil, model.NewAppError("LoginByOAuth2", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
authUser, err1 := provider.GetUserFromJSON(bytes.NewReader(buf.Bytes()), tokenUser)
if err1 != nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *authUser.AuthData == "" {
return nil, model.NewAppError("LoginByOAuth3", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, err := a.GetUserByAuth(model.NewString(*authUser.AuthData), service)
if err != nil {
if err.Id == MissingAuthAccountError {
user, err = a.CreateOAuthUser(c, service, bytes.NewReader(buf.Bytes()), teamID, tokenUser)
} else {
return nil, err
}
} else {
// OAuth doesn't run through CheckUserPreflightAuthenticationCriteria, so prevent bot login
// here manually. Technically, the auth data above will fail to match a bot in the first
// place, but explicit is always better.
if user.IsBot {
return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden)
}
if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service, tokenUser); err != nil {
return nil, err
}
if teamID != "" {
err = a.AddUserToTeamByTeamId(c, teamID, user)
}
}
if err != nil {
return nil, err
}
return user, nil
}
func (a *App) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
if email == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest)
}
ssoUser, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *ssoUser.AuthData == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, nErr := a.Srv().Store().User().GetByEmail(email)
if nErr != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return nil, err
}
if _, nErr := a.Srv().Store().User().UpdateAuthData(user.Id, service, ssoUser.AuthData, ssoUser.Email, true); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
return user, nil
}
func (a *App) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
token := model.NewToken(model.TokenTypeOAuth, extra)
if err := a.Srv().Store().Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateOAuthStateToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return token, nil
}
func (a *App) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
mToken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if mToken.Type != model.TokenTypeOAuth {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest)
}
return mToken, nil
}
func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return "", e
}
sso, e2 := provider.GetSSOSettings(a.Config(), service)
if e2 != nil {
return "", model.NewAppError("GetAuthorizationCode.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
secure := false
if GetProtocol(r) == "https" {
secure = true
}
cookieValue := model.NewId()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(OAuthCookieMaxAgeSeconds), 0)
oauthCookie := &http.Cookie{
Name: CookieOAuth,
Value: cookieValue,
Path: subpath,
MaxAge: OAuthCookieMaxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, oauthCookie)
clientId := *sso.Id
endpoint := *sso.AuthEndpoint
scope := *sso.Scope
tokenExtra := generateOAuthStateTokenExtra(props["email"], props["action"], cookieValue)
stateToken, err := a.CreateOAuthStateToken(tokenExtra)
if err != nil {
return "", err
}
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJSON(props)))
siteURL := a.GetSiteURL()
if strings.TrimSpace(siteURL) == "" {
siteURL = GetProtocol(r) + "://" + r.Host
}
redirectURI := siteURL + "/signup/" + service + "/complete"
authURL := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectURI) + "&state=" + url.QueryEscape(state)
if scope != "" {
authURL += "&scope=" + utils.URLEncode(scope)
}
if loginHint != "" {
authURL += "&login_hint=" + utils.URLEncode(loginHint)
}
return authURL, nil
}
func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectURI string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, "", nil, nil, e
}
sso, e2 := provider.GetSSOSettings(a.Config(), service)
if e2 != nil {
return nil, "", nil, nil, model.NewAppError("AuthorizeOAuthUser.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
b, strErr := b64.StdEncoding.DecodeString(state)
if strErr != nil {
return nil, "", nil, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(strErr)
}
stateStr := string(b)
stateProps := model.MapFromJSON(strings.NewReader(stateStr))
expectedToken, appErr := a.GetOAuthStateToken(stateProps["token"])
if appErr != nil {
return nil, "", stateProps, nil, appErr
}
stateEmail := stateProps["email"]
stateAction := stateProps["action"]
if stateAction == model.OAuthActionEmailToSSO && stateEmail == "" {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
cookie, cookieErr := r.Cookie(CookieOAuth)
if cookieErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
expectedTokenExtra := generateOAuthStateTokenExtra(stateEmail, stateAction, cookie.Value)
if expectedTokenExtra != expectedToken.Extra {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
appErr = a.DeleteToken(expectedToken)
if appErr != nil {
mlog.Warn("error deleting token", mlog.Err(appErr))
}
subpath, _ := utils.GetSubpathFromConfig(a.Config())
httpCookie := &http.Cookie{
Name: CookieOAuth,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, httpCookie)
teamID := stateProps["team_id"]
p := url.Values{}
p.Set("client_id", *sso.Id)
p.Set("client_secret", *sso.Secret)
p.Set("code", code)
p.Set("grant_type", model.AccessTokenGrantType)
p.Set("redirect_uri", redirectURI)
req, requestErr := http.NewRequest("POST", *sso.TokenEndpoint, strings.NewReader(p.Encode()))
if requestErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer resp.Body.Close()
var buf bytes.Buffer
tee := io.TeeReader(resp.Body, &buf)
var ar *model.AccessResponse
err = json.NewDecoder(tee).Decode(&ar)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d, error=%v", buf.String(), resp.StatusCode, err), http.StatusInternalServerError).Wrap(err)
}
if strings.ToLower(ar.TokenType) != model.AccessTokenType {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError)
}
if ar.AccessToken == "" {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError)
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
var userFromToken *model.User
if ar.IdToken != "" {
userFromToken, err = provider.GetUserFromIdToken(ar.IdToken)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
req, requestErr = http.NewRequest("GET", *sso.UserAPIEndpoint, strings.NewReader(""))
if requestErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
resp, err = a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
} else if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
mlog.Error("Error getting OAuth user", mlog.Int("response", resp.StatusCode), mlog.String("body_string", bodyString))
if service == model.ServiceGitlab && resp.StatusCode == http.StatusForbidden && strings.Contains(bodyString, "Terms of Service") {
url, err := url.Parse(*sso.UserAPIEndpoint)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(errors.Wrapf(err, "error parsing %s", *sso.UserAPIEndpoint))
}
// Return a nicer error when the user hasn't accepted GitLab's terms of service
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", map[string]any{"URL": url.Hostname()}, "", http.StatusBadRequest)
}
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError)
}
// Note that resp.Body is not closed here, so it must be closed by the caller
return resp.Body, teamID, stateProps, userFromToken, nil
}
func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToOAuth", "api.user.email_to_oauth.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err = a.CheckPasswordAndAllCriteria(user, password, code); err != nil {
return "", err
}
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionEmailToSSO
stateProps["email"] = email
if service == model.UserAuthServiceSaml {
return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAuthActionEmailToSSO + "&email=" + utils.URLEncode(email), nil
}
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.Id != requesterId {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.context.app_error", nil, "", http.StatusForbidden)
}
if err := a.UpdatePassword(user, password); err != nil {
return "", err
}
T := i18n.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
if err := a.RevokeAllSessions(requesterId); err != nil {
return "", err
}
return "/login?extra=signin_change", nil
}
func generateOAuthStateTokenExtra(email, action, cookie string) string {
return email + ":" + action + ":" + cookie
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
firstAdminCompleteSetupObj := model.System{
Name: model.SystemFirstAdminSetupComplete,
Value: "true",
}
if err := a.Srv().Store().System().SaveOrUpdate(&firstAdminCompleteSetupObj); err != nil {
return model.NewAppError("setFirstAdminCompleteSetup", "api.error_set_first_admin_complete_setup", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
if pluginsEnvironment == nil {
return a.markAdminOnboardingComplete(c)
}
pluginContext := pluginContext(c)
for _, pluginID := range request.InstallPlugins {
go func(id string) {
installRequest := &model.InstallMarketplacePluginRequest{
Id: id,
}
_, appErr := a.Channels().InstallMarketplacePlugin(installRequest)
if appErr != nil {
mlog.Error("Failed to install plugin for onboarding", mlog.String("id", id), mlog.Err(appErr))
return
}
appErr = a.EnablePlugin(id)
if appErr != nil {
mlog.Error("Failed to enable plugin for onboarding", mlog.String("id", id), mlog.Err(appErr))
return
}
hooks, err := a.ch.HooksForPluginOrProduct(id)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err))
return
}
event := model.OnInstallEvent{
UserId: c.Session().UserId,
}
if err = hooks.OnInstall(pluginContext, event); err != nil {
mlog.Error("Plugin OnInstall hook failed", mlog.String("plugin_id", id), mlog.Err(err))
}
}(pluginID)
}
return a.markAdminOnboardingComplete(c)
}
func (a *App) GetOnboarding() (*model.System, *model.AppError) {
firstAdminCompleteSetupObj, err := a.Srv().Store().System().GetByName(model.SystemFirstAdminSetupComplete)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return &model.System{
Name: model.SystemFirstAdminSetupComplete,
Value: "false",
}, nil
default:
return nil, model.NewAppError("getFirstAdminCompleteSetup", "api.error_get_first_admin_complete_setup", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return firstAdminCompleteSetupObj, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"html"
"io"
"net/url"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"golang.org/x/net/html/charset"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxOpenGraphResponseSize = 1024 * 1024 * 50
openGraphMetadataCacheSize = 10000
)
func (a *App) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
var ogJSONGeneric []byte
err := a.Srv().openGraphDataCache.Get(requestURL, &ogJSONGeneric)
if err == nil {
return ogJSONGeneric, nil
}
res, err := a.HTTPService().MakeClient(false).Get(requestURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
graph := a.parseOpenGraphMetadata(requestURL, res.Body, res.Header.Get("Content-Type"))
ogJSON, err := graph.ToJSON()
if err != nil {
return nil, err
}
err = a.Srv().openGraphDataCache.SetWithExpiry(requestURL, ogJSON, 1*time.Hour)
if err != nil {
return nil, err
}
return ogJSON, nil
}
func (a *App) parseOpenGraphMetadata(requestURL string, body io.Reader, contentType string) *opengraph.OpenGraph {
og := opengraph.NewOpenGraph()
body = forceHTMLEncodingToUTF8(io.LimitReader(body, MaxOpenGraphResponseSize), contentType)
if err := og.ProcessHTML(body); err != nil {
mlog.Warn("parseOpenGraphMetadata processing failed", mlog.String("requestURL", requestURL), mlog.Err(err))
}
makeOpenGraphURLsAbsolute(og, requestURL)
openGraphDecodeHTMLEntities(og)
// If image proxy enabled modify open graph data to feed though proxy
if toProxyURL := a.ImageProxyAdder(); toProxyURL != nil {
og = openGraphDataWithProxyAddedToImageURLs(og, toProxyURL)
}
// The URL should be the link the user provided in their message, not a redirected one.
if og.URL != "" {
og.URL = requestURL
}
return og
}
func forceHTMLEncodingToUTF8(body io.Reader, contentType string) io.Reader {
r, err := charset.NewReader(body, contentType)
if err != nil {
mlog.Warn("forceHTMLEncodingToUTF8 failed to convert", mlog.String("contentType", contentType), mlog.Err(err))
return body
}
return r
}
func makeOpenGraphURLsAbsolute(og *opengraph.OpenGraph, requestURL string) {
parsedRequestURL, err := url.Parse(requestURL)
if err != nil {
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse url", mlog.String("requestURL", requestURL), mlog.Err(err))
return
}
makeURLAbsolute := func(resultURL string) string {
if resultURL == "" {
return resultURL
}
parsedResultURL, err := url.Parse(resultURL)
if err != nil {
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse result", mlog.String("requestURL", requestURL), mlog.Err(err))
return resultURL
}
if parsedResultURL.IsAbs() {
return resultURL
}
return parsedRequestURL.ResolveReference(parsedResultURL).String()
}
og.URL = makeURLAbsolute(og.URL)
for _, image := range og.Images {
image.URL = makeURLAbsolute(image.URL)
image.SecureURL = makeURLAbsolute(image.SecureURL)
}
for _, audio := range og.Audios {
audio.URL = makeURLAbsolute(audio.URL)
audio.SecureURL = makeURLAbsolute(audio.SecureURL)
}
for _, video := range og.Videos {
video.URL = makeURLAbsolute(video.URL)
video.SecureURL = makeURLAbsolute(video.SecureURL)
}
}
func openGraphDataWithProxyAddedToImageURLs(ogdata *opengraph.OpenGraph, toProxyURL func(string) string) *opengraph.OpenGraph {
for _, image := range ogdata.Images {
var url string
if image.SecureURL != "" {
url = image.SecureURL
} else {
url = image.URL
}
image.URL = ""
image.SecureURL = toProxyURL(url)
}
return ogdata
}
func openGraphDecodeHTMLEntities(og *opengraph.OpenGraph) {
og.Title = html.UnescapeString(og.Title)
og.Description = html.UnescapeString(og.Description)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make app-layers"
// DO NOT EDIT
package opentracing
import (
"archive/zip"
"bytes"
"context"
"crypto/ecdsa"
"io"
"mime/multipart"
"net/http"
"net/url"
"reflect"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
)
type OpenTracingAppLayer struct {
app app.AppIface
srv *app.Server
log *mlog.Logger
notificationsLog *mlog.Logger
accountMigration einterfaces.AccountMigrationInterface
cluster einterfaces.ClusterInterface
compliance einterfaces.ComplianceInterface
dataRetention einterfaces.DataRetentionInterface
searchEngine *searchengine.Broker
ldap einterfaces.LdapInterface
messageExport einterfaces.MessageExportInterface
metrics einterfaces.MetricsInterface
notification einterfaces.NotificationInterface
saml einterfaces.SamlInterface
httpService httpservice.HTTPService
imageProxy *imageproxy.ImageProxy
timezones *timezones.Timezones
ctx context.Context
}
func (a *OpenTracingAppLayer) ActivateMfa(userID string, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ActivateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ActivateMfa(userID, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts app.ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddChannelMember(c, userID, channel, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelsToRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddChannelsToRetentionPolicy(policyID, channelIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddConfigListener(listener func(*model.Config, *model.Config)) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddConfigListener")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddConfigListener(listener)
return resultVar0
}
func (a *OpenTracingAppLayer) AddCursorIdsForPostList(originalList *model.PostList, afterPost string, beforePost string, since int64, page int, perPage int, collapsedThreads bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddCursorIdsForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AddCursorIdsForPostList(originalList, afterPost, beforePost, since, page, perPage, collapsedThreads)
}
func (a *OpenTracingAppLayer) AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddDirectChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddDirectChannels(c, teamID, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddLdapPrivateCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddLdapPublicCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddPublicKey(name string, key io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddPublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddPublicKey(name, key)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddRemoteCluster(rc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlIdpCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlIdpCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlPrivateCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlPublicCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSessionToCache(session *model.Session) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSessionToCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AddSessionToCache(session)
}
func (a *OpenTracingAppLayer) AddTeamMember(c request.CTX, teamID string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMember(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMemberByInviteId(c *request.Context, inviteId string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMemberByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMemberByInviteId(c, inviteId, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMemberByToken(c *request.Context, userID string, tokenID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMemberByToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMemberByToken(c, userID, tokenID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMembers(c *request.Context, teamID string, userIDs []string, userRequestorId string, graceful bool) ([]*model.TeamMemberWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMembers(c, teamID, userIDs, userRequestorId, graceful)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamsToRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddTeamsToRetentionPolicy(policyID, teamIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddUserToChannel(c, user, channel, skipTeamMemberIntegrityCheck)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddUserToTeam(c request.CTX, teamID string, userID string, userRequestorId string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeam(c, teamID, userID, userRequestorId)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AddUserToTeamByInviteId(c *request.Context, inviteId string, userID string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeamByInviteId(c, inviteId, userID)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AddUserToTeamByTeamId(c *request.Context, teamID string, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByTeamId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddUserToTeamByTeamId(c, teamID, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddUserToTeamByToken(c *request.Context, userID string, tokenID string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeamByToken(c, userID, tokenID)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AdjustImage(file)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustInProductLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustInProductLimits(limits, subscription)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustTeamsFromProductLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustTeamsFromProductLimits(teamLimits)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AllowOAuthAppAccessToUser(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AppendFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AppendFile(fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AsymmetricSigningKey() *ecdsa.PrivateKey {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AsymmetricSigningKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AsymmetricSigningKey()
return resultVar0
}
func (a *OpenTracingAppLayer) AttachCloudSessionCookie(c *request.Context, w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachCloudSessionCookie")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AttachCloudSessionCookie(c, w, r)
}
func (a *OpenTracingAppLayer) AttachDeviceId(sessionID string, deviceID string, expiresAt int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachDeviceId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AttachDeviceId(sessionID, deviceID, expiresAt)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachSessionCookies")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AttachSessionCookies(c, w, r)
}
func (a *OpenTracingAppLayer) AuthenticateUserForLogin(c *request.Context, id string, loginId string, password string, mfaToken string, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthenticateUserForLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AuthenticateUserForLogin(c, id, loginId, password, mfaToken, cwsToken, ldapOnly)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service string, code string, state string, redirectURI string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthorizeOAuthUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3, resultVar4 := a.app.AuthorizeOAuthUser(w, r, service, code, state, redirectURI)
if resultVar4 != nil {
span.LogFields(spanlog.Error(resultVar4))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3, resultVar4
}
func (a *OpenTracingAppLayer) AutocompleteChannels(c request.CTX, userID string, term string) (model.ChannelListWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannels(c, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteChannelsForSearch(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannelsForSearch")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannelsForSearch(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteChannelsForTeam(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannelsForTeam(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteUsersInChannel(teamID, channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteUsersInTeam(teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteUsersInTeam(teamID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildPostReactions(ctx request.CTX, postID string) (*[]app.ReactionImportData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildPostReactions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildPostReactions(ctx, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildPushNotificationMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildPushNotificationMessage(c, contentsConfig, post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildSamlMetadataObject")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildSamlMetadataObject(idpMetadata)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkExport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.BulkExport(ctx, writer, outPath, job, opts)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImport(c, jsonlReader, attachmentsReader, dryRun, workers)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImportWithPath")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImportWithPath(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CanNotifyAdmin(trial bool) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CanNotifyAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CanNotifyAdmin(trial)
return resultVar0
}
func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CancelJob(jobId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersMinusGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersToAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersToRemove")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ChannelMembersToRemove(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Channels() *app.Channels {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Channels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Channels()
return resultVar0
}
func (a *OpenTracingAppLayer) CheckCanInviteToSharedChannel(channelId string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckCanInviteToSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckCanInviteToSharedChannel(channelId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, string, string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckForClientSideCert")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.CheckForClientSideCert(r)
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) CheckIntegrity() <-chan model.IntegrityCheckResult {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckIntegrity")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckIntegrity()
return resultVar0
}
func (a *OpenTracingAppLayer) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckMandatoryS3Fields")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckMandatoryS3Fields(settings)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckPasswordAndAllCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckPasswordAndAllCriteria(user, password, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckPostReminders() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckPostReminders")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.CheckPostReminders()
}
func (a *OpenTracingAppLayer) CheckProviderAttributes(user *model.User, patch *model.UserPatch) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckProviderAttributes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckProviderAttributes(user, patch)
return resultVar0
}
func (a *OpenTracingAppLayer) CheckRolesExist(roleNames []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckRolesExist")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckRolesExist(roleNames)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserAllAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserAllAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserAllAuthenticationCriteria(user, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserMfa(user *model.User, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserMfa(user, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserPostflightAuthenticationCriteria(user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserPostflightAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserPostflightAuthenticationCriteria(user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserPreflightAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserPreflightAuthenticationCriteria(user, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckWebConn(userID string, connectionID string) *platform.CheckConnResult {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckWebConn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckWebConn(userID, connectionID)
return resultVar0
}
func (a *OpenTracingAppLayer) ClearChannelMembersCache(c request.CTX, channelID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearChannelMembersCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClearChannelMembersCache(c, channelID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ClearLatestVersionCache() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearLatestVersionCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearLatestVersionCache()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForAllUsers() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForAllUsers()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForAllUsersSkipClusterSend() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForAllUsersSkipClusterSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForAllUsersSkipClusterSend()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForUser(userID)
}
func (a *OpenTracingAppLayer) ClearSessionCacheForUserSkipClusterSend(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForUserSkipClusterSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForUserSkipClusterSend(userID)
}
func (a *OpenTracingAppLayer) ClearTeamMembersCache(teamID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearTeamMembersCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClearTeamMembersCache(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ClientConfig() map[string]string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClientConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClientConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) ClientConfigHash() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClientConfigHash")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClientConfigHash()
return resultVar0
}
func (a *OpenTracingAppLayer) Cloud() einterfaces.CloudInterface {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Cloud")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Cloud()
return resultVar0
}
func (a *OpenTracingAppLayer) CommandsForTeam(teamID string) []*model.Command {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CommandsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CommandsForTeam(teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) CompareAndDeletePluginKey(pluginID string, key string, oldValue []byte) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompareAndDeletePluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompareAndDeletePluginKey(pluginID, key, oldValue)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompareAndSetPluginKey(pluginID string, key string, oldValue []byte, newValue []byte) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompareAndSetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompareAndSetPluginKey(pluginID, key, oldValue, newValue)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompleteOAuth(c *request.Context, service string, body io.ReadCloser, teamID string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompleteOAuth(c, service, body, teamID, props, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOnboarding")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CompleteOnboarding(c, request)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteSwitchWithOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompleteSwitchWithOAuth(service, userData, email, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ComputeLastAccessibleFileTime() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ComputeLastAccessibleFileTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ComputeLastAccessibleFileTime()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ComputeLastAccessiblePostTime() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ComputeLastAccessiblePostTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ComputeLastAccessiblePostTime()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) Config() *model.Config {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Config")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Config()
return resultVar0
}
func (a *OpenTracingAppLayer) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertBotToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ConvertBotToUser(c, bot, userPatch, sysadmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertUserToBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ConvertUserToBot(user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CopyFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CopyFileInfos(userID, fileIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateBot(c, bot)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannel(c, channel, addMember)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelScheme(c request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannelWithUser(c, channel, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommand(cmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommandPost(c request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommandPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("teamID", teamID)
span.SetTag("skipSlackParsing", skipSlackParsing)
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommandPost(c, post, teamID, response, skipSlackParsing)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommandWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommandWebhook(commandID, args)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDefaultMemberships")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CreateDefaultMemberships(c, params)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateEmoji(c, sessionUserId, emoji, multiPartImageData)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroup(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroupChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroupChannel(c, userIDs, creatorId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroupWithUserIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroupWithUserIds(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGuest(c request.CTX, user *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGuest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGuest(c, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateIncomingWebhookForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateIncomingWebhookForChannel(creatorId, channel, hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateJob(job *model.Job) (*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateJob(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthApp(app)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthStateToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthStateToken(extra)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthUser(c, service, userData, teamID, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOutgoingWebhook(hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePasswordRecoveryToken(userID string, email string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePasswordRecoveryToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePasswordRecoveryToken(userID, email)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks bool, setOnline bool) (savedPost *model.Post, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePost(c, post, channel, triggerWebhooks, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePostAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePostAsUser(c, post, currentSessionId, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePostMissingChannel(c request.CTX, post *model.Post, triggerWebhooks bool, setOnline bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePostMissingChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePostMissingChannel(c, post, triggerWebhooks, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateRetentionPolicy(policy)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRole(role *model.Role) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateRole(role)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateScheme(scheme)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateSession(session)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateSidebarCategory(c request.CTX, userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateSidebarCategory(c, userID, teamID, newCategory)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTeam(c, team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTeamWithUser(c *request.Context, team *model.Team, userID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTeamWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTeamWithUser(c, team, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTermsOfService(text string, userID string) (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTermsOfService(text, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUploadSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUploadSession(c, us)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUser(c, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserAccessToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserAsAdmin(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserAsAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserAsAdmin(c, user, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserFromSignup(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserFromSignup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserFromSignup(c, user, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserWithInviteId(c request.CTX, user *model.User, inviteId string, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserWithInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserWithInviteId(c, user, inviteId, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserWithToken(c request.CTX, user *model.User, token *model.Token) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserWithToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserWithToken(c, user, token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateWebhookPost(c request.CTX, userID string, channel *model.Channel, text string, overrideUsername string, overrideIconURL string, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateWebhookPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateWebhookPost(c, userID, channel, text, overrideUsername, overrideIconURL, overrideIconEmoji, props, postType, postRootId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateZipFileAndAddFiles(fileBackend filestore.FileBackend, fileDatas []model.FileData, zipFileName string, directory string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateZipFileAndAddFiles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CreateZipFileAndAddFiles(fileBackend, fileDatas, zipFileName, directory)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DBHealthCheckDelete() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DBHealthCheckDelete")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DBHealthCheckDelete()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DBHealthCheckWrite() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DBHealthCheckWrite")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DBHealthCheckWrite()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeactivateGuests(c *request.Context) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeactivateGuests")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeactivateGuests(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeactivateMfa(userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeactivateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeactivateMfa(userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeauthorizeOAuthAppForUser(userID string, appID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeauthorizeOAuthAppForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeauthorizeOAuthAppForUser(userID, appID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DefaultChannelNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DefaultChannelNames(c)
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAcknowledgementForPost(c *request.Context, postID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAcknowledgementForPost(c, postID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAllExpiredPluginKeys() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllExpiredPluginKeys")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAllExpiredPluginKeys()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAllKeysForPlugin(pluginID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllKeysForPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAllKeysForPlugin(pluginID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteBrandImage() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteBrandImage()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteChannel(c, channel, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteCommand(commandID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteCommand(commandID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteDraft(userID string, channelID string, rootID string, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteDraft(userID, channelID, rootID, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteEmoji(c, emoji)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteEphemeralPost(userID string, postID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DeleteEphemeralPost(userID, postID)
}
func (a *OpenTracingAppLayer) DeleteExport(name string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteExport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteExport(name)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroup(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupConstrainedMemberships(c *request.Context) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupConstrainedMemberships")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteGroupConstrainedMemberships(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupMember(groupID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupMembers(groupID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupSyncable(groupID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteIncomingWebhook(hookID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteIncomingWebhook(hookID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteOAuthApp(appID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteOAuthApp(appID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteOutgoingWebhook(hookID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteOutgoingWebhook(hookID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePluginKey(pluginID string, key string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePluginKey(pluginID, key)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePost(c request.CTX, postID string, deleteByID string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeletePost(c, postID, deleteByID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeletePreferences(userID string, preferences model.Preferences) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePreferences")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePreferences(userID, preferences)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePublicKey(name string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePublicKey(name)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteReactionForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteReactionForPost(c, reaction)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteRemoteCluster(remoteClusterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteRetentionPolicy(policyID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteRetentionPolicy(policyID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteScheme(schemeId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannelRemote(id string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteSharedChannelRemote(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSidebarCategory(c request.CTX, userID string, teamID string, categoryId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteSidebarCategory(c, userID, teamID, categoryId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteToken(token *model.Token) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DemoteUserToGuest(c request.CTX, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DemoteUserToGuest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DemoteUserToGuest(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisableAutoResponder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisableAutoResponder(c, userID, asAdmin)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisablePlugin(id string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisablePlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisablePlugin(id)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisableUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisableUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoActionRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoActionRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoActionRequest(c, rawURL, body)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoAdvancedPermissionsMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoAdvancedPermissionsMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoAdvancedPermissionsMigration()
}
func (a *OpenTracingAppLayer) DoAppMigrations() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoAppMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoAppMigrations()
}
func (a *OpenTracingAppLayer) DoCheckForAdminNotifications(trial bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCheckForAdminNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoCheckForAdminNotifications(trial)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.DoCommandRequest(cmd, p)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) DoEmojisPermissionsMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoEmojisPermissionsMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoEmojisPermissionsMigration()
}
func (a *OpenTracingAppLayer) DoGuestRolesCreationMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoGuestRolesCreationMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoGuestRolesCreationMigration()
}
func (a *OpenTracingAppLayer) DoLocalRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoLocalRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoLocalRequest(c, rawURL, body)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile bool, isOAuthUser bool, isSaml bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoLogin(c, w, r, user, deviceID, isMobile, isOAuthUser, isSaml)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoPermissionsMigrations() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPermissionsMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoPermissionsMigrations()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoPostAction(c *request.Context, postID string, actionId string, userID string, selectedOption string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostAction")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoPostAction(c, postID, actionId, userID, selectedOption)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoPostActionWithCookie(c *request.Context, postID string, actionId string, userID string, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostActionWithCookie")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoPostActionWithCookie(c, postID, actionId, userID, selectedOption, cookie)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoSystemConsoleRolesCreationMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoSystemConsoleRolesCreationMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoSystemConsoleRolesCreationMigration()
}
func (a *OpenTracingAppLayer) DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoUploadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoUploadFile(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoUploadFileExpectModification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.DoUploadFileExpectModification(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) DoubleCheckPassword(user *model.User, password string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoubleCheckPassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoubleCheckPassword(user, password)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DownloadFromURL(downloadURL string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DownloadFromURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DownloadFromURL(downloadURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) EnablePlugin(id string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnablePlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnablePlugin(id)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnableUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnableUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnsureBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.EnsureBot(c, productID, bot)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnvironmentConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnvironmentConfig(filter)
return resultVar0
}
func (a *OpenTracingAppLayer) ExecuteCommand(c request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExecuteCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("args", args)
defer span.Finish()
resultVar0, resultVar1 := a.app.ExecuteCommand(c, args)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*app.WorkTemplateExecutionResult, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExecuteWorkTemplate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ExecuteWorkTemplate(c, wtcr, installPlugins)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ExportPermissions(w io.Writer) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExportPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExportPermissions(w)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ExtendSessionExpiryIfNeeded(session *model.Session) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExtendSessionExpiryIfNeeded")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExtendSessionExpiryIfNeeded(session)
return resultVar0
}
func (a *OpenTracingAppLayer) ExtractContentFromFileInfo(fileInfo *model.FileInfo) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExtractContentFromFileInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExtractContentFromFileInfo(fileInfo)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FetchSamlMetadataFromIdp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FetchSamlMetadataFromIdp(url)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileBackend() filestore.FileBackend {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileBackend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FileBackend()
return resultVar0
}
func (a *OpenTracingAppLayer) FileExists(path string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileExists")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileExists(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileModTime(path string) (time.Time, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileModTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileModTime(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileReader")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileReader(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileSize(path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileSize")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileSize(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FillInChannelProps(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInChannelProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInChannelProps(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInChannelsProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInChannelsProps(c, channelList)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FillInPostProps(c request.CTX, post *model.Post, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInPostProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInPostProps(c, post, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterNonGroupChannelMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterNonGroupChannelMembers(userIDs, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterNonGroupTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterNonGroupTeamMembers(userIDs, team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterUsersByVisible")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterUsersByVisible(viewer, otherUsers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FindTeamByName(name string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FindTeamByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FindTeamByName(name)
return resultVar0
}
func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FinishSendAdminNotifyPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
}
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GenerateMfaSecret(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GeneratePublicLink")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GeneratePublicLink(siteURL, info)
return resultVar0
}
func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GenerateSupportPacket()
return resultVar0
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPostList(postList)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetActivePluginManifests()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllChannels(c request.CTX, page int, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllChannels(c, page, perPage, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllChannelsCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllChannelsCount(c, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllLdapGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllLdapGroupsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetAllLdapGroupsPage(page, perPage, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetAllPrivateTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllPrivateTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllPrivateTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllPublicTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllPublicTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllPublicTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRemoteClusters")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllRemoteClusters(filter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllRoles() ([]*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllRoles()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeamsPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeamsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeamsPage(offset, limit, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeamsPageWithCount(offset int, limit int, opts *model.TeamSearch) (*model.TeamsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeamsPageWithCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeamsPageWithCount(offset, limit, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAnalytics")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAnalytics(name, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAppliedSchemaMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAppliedSchemaMigrations()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAudits(userID string, limit int) (model.Audits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAudits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAudits(userID, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuditsPage(userID string, page int, perPage int) (model.Audits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuditsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuditsPage(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuthorizationCode")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuthorizationCode(w, r, service, props, loginHint)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuthorizedAppsForUser(userID string, page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuthorizedAppsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuthorizedAppsForUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBot(botUserId, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBots")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBots(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBrandImage() ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBrandImage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBulkReactionsForPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBulkReactionsForPosts(postIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannel(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelByName(c request.CTX, channelName string, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelByName(c, channelName, teamID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelByNameForTeamName(c request.CTX, channelName string, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelByNameForTeamName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelByNameForTeamName(c, channelName, teamName, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelCounts(c request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelCounts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelCounts(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelFileCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelFileCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelFileCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelGroupUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelGroupUsers(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelGuestCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelGuestCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelGuestCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMember(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMemberCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMemberCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMemberCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersByIds(c request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersByIds(c, channelID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersForUser(c request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersForUser(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersForUserWithPagination(c request.CTX, userID string, page int, perPage int) ([]*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersForUserWithPagination(c, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersPage(c request.CTX, channelID string, page int, perPage int) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersPage(c, channelID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersTimezones(c request.CTX, channelID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersTimezones")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersTimezones(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersWithTeamDataForUserWithPagination(c request.CTX, userID string, page int, perPage int) (model.ChannelMembersWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersWithTeamDataForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersWithTeamDataForUserWithPagination(c, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelModerationsForChannel(c request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelModerationsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelModerationsForChannel(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelPinnedPostCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelPinnedPostCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelPinnedPostCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelPoliciesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelPoliciesForUser(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelUnread(c request.CTX, channelID string, userID string) (*model.ChannelUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelUnread(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannels(c request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannels(c, channelIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsByNames(c request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsByNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsByNames(c, channelNames, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForRetentionPolicy(policyID string, offset int, limit int) (*model.ChannelsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForRetentionPolicy(policyID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForScheme(scheme, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForSchemePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForSchemePage(scheme, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForTeamForUser(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUserWithCursor")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForTeamForUserWithCursor(c, teamID, userID, opts, afterChannelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForUser(c, userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsUserNotIn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsUserNotIn(c, teamID, userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCloudSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCloudSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCloudSession(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetClusterId() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetClusterId()
return resultVar0
}
func (a *OpenTracingAppLayer) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterPluginStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetClusterPluginStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetClusterStatus() []*model.ClusterInfo {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetClusterStatus()
return resultVar0
}
func (a *OpenTracingAppLayer) GetCommand(commandID string) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCommand(commandID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCommonTeamIDsForTwoUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceFile(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceReport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceReport(reportId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceReports(page int, perPage int) (model.Compliances, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceReports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceReports(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetConfigFile(name string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetConfigFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetConfigFile(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCookieDomain() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCookieDomain")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetCookieDomain()
return resultVar0
}
func (a *OpenTracingAppLayer) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCustomStatus(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDefaultProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDefaultProfileImage(user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDeletedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDeletedChannels(c, teamID, offset, limit, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDraft(userID string, channelID string, rootID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDraft(userID, channelID, rootID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraftsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDraftsForUser(userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEditHistoryForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEditHistoryForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmoji(c, emojiId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiByName(c, emojiName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetEmojiImage(c, emojiId)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetEmojiList(c request.CTX, page int, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiList(c, page, perPage, sort)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiStaticURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiStaticURL(c, emojiName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEnvironmentConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetEnvironmentConfig(filter)
return resultVar0
}
func (a *OpenTracingAppLayer) GetFile(fileID string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFile(fileID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfo(fileID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfos(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfos(page, perPage, opt)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfosForPost(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfosForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetFileInfosForPost(postID, fromMaster, includeDeleted)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetFileInfosForPostWithMigration(postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfosForPostWithMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfosForPostWithMigration(postID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFilteredUsersStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFilteredUsersStats(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPosts(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPostsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPostsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGlobalRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGlobalRetentionPolicy()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroup(id, opts, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupByName(name, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupByRemoteID")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupByRemoteID(remoteID, groupSource)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupChannel(c, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupMemberCount(groupID, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupMemberUsers(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersPage(groupID, page, perPage, viewRestrictions)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersSortedPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupSyncable(groupID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncables")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupSyncables(groupID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroups")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroups(page, perPage, opts, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsAssociatedToChannelsByTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupsByChannel(channelID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByIDs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsByIDs(groupIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsBySource")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsBySource(groupSource)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupsByTeam(teamID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsByUserId(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetHubForUserId(userID string) *platform.Hub {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetHubForUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetHubForUserId(userID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhook(hookID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksForTeamPage(teamID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksForTeamPageByUser(teamID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksPage(page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksPageByUser(userID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksPageByUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJob(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobs(offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByType")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByType(jobType, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypePage(jobType, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypes(jobTypes, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypesPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypesPage(jobType, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetKnownUsers(userID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetKnownUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetKnownUsers(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLastAccessibleFileTime() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLastAccessibleFileTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLastAccessibleFileTime()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLastAccessiblePostTime() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLastAccessiblePostTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLastAccessiblePostTime()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLatestTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLatestTermsOfService()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLatestVersion")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLatestVersion(latestVersionUrl)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLdapGroup(ldapGroupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLdapGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLdapGroup(ldapGroupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLogs(page int, perPage int) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLogs(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLogsSkipSend(page int, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogsSkipSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLogsSkipSend(page, perPage, logFilter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMarketplacePlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMarketplacePlugins(filter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMemberCountsByGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMessageForNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetMessageForNotification(post, translateFunc)
return resultVar0
}
func (a *OpenTracingAppLayer) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMultipleEmojiByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMultipleEmojiByName(c, names)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewTeamMembersSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetNewTeamMembersSince(c, teamID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetNewUsersForTeamPage(teamID string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewUsersForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetNewUsersForTeamPage(teamID, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetNextPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNextPostIdFromPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetNextPostIdFromPostList(postList, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetNotificationNameFormat(user *model.User) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNotificationNameFormat")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetNotificationNameFormat(user)
return resultVar0
}
func (a *OpenTracingAppLayer) GetNumberOfChannelsOnTeam(c request.CTX, teamID string) (int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNumberOfChannelsOnTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetNumberOfChannelsOnTeam(c, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAccessTokenForCodeFlow(clientId string, grantType string, redirectURI string, code string, secret string, refreshToken string) (*model.AccessResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAccessTokenForCodeFlow")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAccessTokenForImplicitFlow(userID string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAccessTokenForImplicitFlow")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAccessTokenForImplicitFlow(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthApp(appID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthApps(page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthApps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthApps(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAppsByCreator(userID string, page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAppsByCreator")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAppsByCreator(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthCodeRedirect")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthCodeRedirect(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthImplicitRedirect")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthImplicitRedirect(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthLoginEndpoint")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthSignupEndpoint")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthStateToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthStateToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOnboarding() (*model.System, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOnboarding")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOnboarding()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOpenGraphMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOpenGraphMetadata(requestURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateDirectChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOrCreateDirectChannel(c, userID, otherUserID, channelOptions...)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateTrueUpReviewStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOrCreateTrueUpReviewStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhook(hookID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForChannelPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForTeamPage(teamID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForTeamPageByUser(teamID string, userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForTeamPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksPage(page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksPageByUser(userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksPageByUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPasswordRecoveryToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPasswordRecoveryToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPermalinkPost(c request.CTX, postID string, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPermalinkPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPermalinkPost(c, postID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPinnedPosts(c request.CTX, channelID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPinnedPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPinnedPosts(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginKey(pluginID, key)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginStatus(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPlugins() (*model.PluginsResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPlugins()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginsEnvironment() *plugin.Environment {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginsEnvironment")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPluginsEnvironment()
return resultVar0
}
func (a *OpenTracingAppLayer) GetPostAfterTime(channelID string, time int64, collapsedThreads bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostAfterTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostAfterTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIdAfterTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIdAfterTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIdBeforeTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIdBeforeTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIfAuthorized")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIfAuthorized(c, postID, session, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostInfo(c, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostThread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostThread(postID, opts, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPosts(channelID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsAfterPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsAfterPost(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsAroundPost(before bool, options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsAroundPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsAroundPost(before, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsBeforePost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsBeforePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsBeforePost(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetPostsByIds(postIDs)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetPostsEtag(channelID string, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPostsEtag(channelID, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetPostsForChannelAroundLastUnread(c request.CTX, channelID string, userID string, limitBefore int, limitAfter int, skipFetchThreads bool, collapsedThreads bool, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsForChannelAroundLastUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsForChannelAroundLastUnread(c, channelID, userID, limitBefore, limitAfter, skipFetchThreads, collapsedThreads, collapsedThreadsExtended)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsPage(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsSince(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsUsage() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryAndNameForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferenceByCategoryAndNameForUser(userID, category, preferenceName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferenceByCategoryForUser(userID, category)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferencesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferencesForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrevPostIdFromPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPrevPostIdFromPostList(postList, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPriorityForPost(postId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPriorityForPostList(list)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrivateChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPrivateChannelsForTeam(c, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetProductNotices(c *request.Context, userID string, teamID string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetProductNotices(c, userID, teamID, client, clientVersion, locale)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetProfileImage(user)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicChannelsByIdsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicChannelsByIdsForTeam(c, teamID, channelIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPublicChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicChannelsForTeam(c, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPublicKey(name string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicKey(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetReactionsForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetReactionsForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentSearchesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentSearchesForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentlyActiveUsersForTeam(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeamPage(teamID string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentlyActiveUsersForTeamPage(teamID, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteCluster(remoteClusterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterForUser(remoteID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterService()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterSession(token, remoteId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPolicies(offset int, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPolicies")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPolicies(offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPoliciesCount() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPoliciesCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPoliciesCount()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPolicy(policyID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRole(id string) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRole(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRoleByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRoleByName(ctx, name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRolesByNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRolesByNames(names)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSamlCertificateStatus() *model.SamlCertificateStatus {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlCertificateStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSamlCertificateStatus()
return resultVar0
}
func (a *OpenTracingAppLayer) GetSamlMetadata() (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSamlMetadata()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSamlMetadataFromIdp(idpMetadataURL string) (*model.SamlMetadataResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlMetadataFromIdp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSamlMetadataFromIdp(idpMetadataURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSanitizeOptions(asAdmin bool) map[string]bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSanitizeOptions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSanitizeOptions(asAdmin)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSanitizedConfig() *model.Config {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSanitizedConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSanitizedConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) GetScheme(id string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetScheme(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemeByName(name string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemeByName(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemeRolesForChannel(c request.CTX, channelID string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeRolesForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetSchemeRolesForChannel(c, channelID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeRolesForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetSchemeRolesForTeam(teamID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemes(scope, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemesPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemesPage(scope, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSession(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSessionById(sessionID string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessionById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSessionById(sessionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSessionLengthInMillis(session *model.Session) int64 {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessionLengthInMillis")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSessionLengthInMillis(session)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSessions(userID string) ([]*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSessions(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemote(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemoteByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemoteByIds(channelID, remoteID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemotes(opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotesStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemotesStatus(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannels(page, perPage, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelsCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelsCount(opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategories(c, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategoriesForTeamForUser(c request.CTX, userID string, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategoriesForTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategoriesForTeamForUser(c, userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategory(c, categoryId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategoryOrder(c request.CTX, userID string, teamID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategoryOrder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategoryOrder(c, userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSinglePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSinglePost(postID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSiteURL() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSiteURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSiteURL()
return resultVar0
}
func (a *OpenTracingAppLayer) GetStatus(userID string) (*model.Status, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetStatus(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetStatusFromCache(userID string) *model.Status {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStatusFromCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetStatusFromCache(userID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetStorageUsage() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStorageUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetStorageUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSuggestions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSuggestions(c, commandArgs, commands, roleID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSystemBot() (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSystemBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSystemBot()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeam(teamID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeam(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamByInviteId(inviteId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamByName(name string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamByName(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamGroupUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamGroupUsers(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamIcon(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamIdFromQuery")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamIdFromQuery(query)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMember(teamID string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMember(teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembers(teamID, offset, limit, teamMembersGetOptions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersByIds(teamID, userIDs, restrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersForUser(userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersForUser(userID, excludeTeamID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersForUserWithPagination(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamPoliciesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamPoliciesForUser(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamSchemeChannelRoles(c request.CTX, teamID string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamSchemeChannelRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetTeamSchemeChannelRoles(c, teamID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetTeamStats(teamID string, restrictions *model.ViewUsersRestrictions) (*model.TeamStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamStats(teamID, restrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamUnread(teamID string, userID string) (*model.TeamUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamUnread(teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeams(teamIDs []string) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeams(teamIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForRetentionPolicy(policyID string, offset int, limit int) (*model.TeamsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForRetentionPolicy(policyID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForScheme(scheme, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForSchemePage(scheme *model.Scheme, page int, perPage int) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForSchemePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForSchemePage(scheme, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsUnreadForUser(excludeTeamId string, userID string, includeCollapsedThreads bool) ([]*model.TeamUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsUnreadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsUnreadForUser(excludeTeamId, userID, includeCollapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsUsage() (*model.TeamsUsage, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTermsOfService(id string) (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTermsOfService(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadForUser(threadMembership, extended)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadMembershipForUser(userId string, threadId string) (*model.ThreadMembership, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadMembershipForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadMembershipForUser(userId, threadId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadMembershipsForUser(userID string, teamID string) ([]*model.ThreadMembership, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadMembershipsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadMembershipsForUser(userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadsForUser(userID string, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadsForUser(userID, teamID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTokenById(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTokenById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTokenById(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForUserSince(c request.CTX, userID string, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForUserSince(c, userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopDMsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopDMsForUserSince(userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForTeamSince(teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForUserSince(userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTotalUsersStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTotalUsersStats(viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTrueUpProfile() (map[string]any, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTrueUpProfile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTrueUpProfile()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUploadSession(c, uploadId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSessionsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUploadSessionsForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUser(userID string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessToken(tokenID string, sanitize bool) (*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessToken(tokenID, sanitize)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessTokens(page int, perPage int) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessTokens")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessTokens(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessTokensForUser(userID string, page int, perPage int) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessTokensForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessTokensForUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByAuth(authData, authService)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByEmail(email string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByEmail(email)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByUsername(username string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByUsername")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByUsername(username)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserForLogin(id string, loginId string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserForLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserForLogin(id, loginId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserStatusesByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserStatusesByIds(userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserTermsOfService(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsers(userIDs []string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsers(userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByGroupChannelIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByGroupChannelIds(c, channelIDs, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByIds(userIDs, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByUsernames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByUsernames(usernames, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersEtag(restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersEtag(restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersFromProfiles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersFromProfiles(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannel(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelByAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelByAdmin(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelByStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelByStatus(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelMap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelMap(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPageByAdmin(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPageByAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPageByAdmin(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPageByStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPageByStatus(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInTeam(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeamEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersInTeamEtag(teamID, restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInTeamPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannelMap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannelMap(teamID, channelID, groupConstrained, offset, limit, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannelPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannelPage(teamID, channelID, groupConstrained, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInGroupPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInGroupPage(groupID, page, perPage, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeamEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersNotInTeamEtag(teamID, restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInTeamPage(teamID, groupConstrained, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithInvalidEmails(page int, perPage int) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithInvalidEmails")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithInvalidEmails(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithoutTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithoutTeam(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithoutTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithoutTeamPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetVerifyEmailToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetVerifyEmailToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetViewUsersRestrictions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetViewUsersRestrictions(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWarnMetricsBot() (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWarnMetricsBot()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWarnMetricsStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplateCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplateCategories(t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.Handle404(w, r)
}
func (a *OpenTracingAppLayer) HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandResponse")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HandleCommandResponse(c, command, args, response, builtIn)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HandleCommandResponsePost(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandResponsePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HandleCommandResponsePost(c, command, args, response, builtIn)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HandleCommandWebhook(c *request.Context, hookID string, response *model.CommandResponse) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HandleCommandWebhook(c, hookID, response)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleImages")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HandleImages(previewPathList, thumbnailPathList, fileData)
}
func (a *OpenTracingAppLayer) HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HandleIncomingWebhook(c, hookID, req)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleMessageExportConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HandleMessageExportConfig(cfg, appCfg)
}
func (a *OpenTracingAppLayer) HasBoardProduct() (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasBoardProduct")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasBoardProduct()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionTo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionTo(askingUserId, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToChannel(c, askingUserId, channelID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToChannelByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToChannelByPost(askingUserId, postID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToReadChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToReadChannel(c, userID, channel)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToTeam(askingUserId, teamID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToUser(askingUserId string, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToUser(askingUserId, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) HasRemote(channelID string, remoteID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasRemote(channelID, remoteID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HasSharedChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HooksManager() *product.HooksManager {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HooksManager")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HooksManager()
return resultVar0
}
func (a *OpenTracingAppLayer) HubRegister(webConn *platform.WebConn) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubRegister")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HubRegister(webConn)
}
func (a *OpenTracingAppLayer) HubUnregister(webConn *platform.WebConn) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubUnregister")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HubUnregister(webConn)
}
func (a *OpenTracingAppLayer) ImageProxyAdder() func(string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImageProxyAdder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImageProxyAdder()
return resultVar0
}
func (a *OpenTracingAppLayer) ImageProxyRemover() (f func(string) string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImageProxyRemover")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImageProxyRemover()
return resultVar0
}
func (a *OpenTracingAppLayer) ImportPermissions(jsonl io.Reader) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImportPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImportPermissions(jsonl)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InitPlugins(c *request.Context, pluginDir string, webappPluginDir string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InitPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.InitPlugins(c, pluginDir, webappPluginDir)
}
func (a *OpenTracingAppLayer) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InstallPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InstallPlugin(pluginFile, replace)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) InvalidateAllEmailInvites() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllEmailInvites")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InvalidateAllEmailInvites()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InvalidateAllResendInviteEmailJobs() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllResendInviteEmailJobs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InvalidateAllResendInviteEmailJobs()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InvalidateCacheForUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateCacheForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.InvalidateCacheForUser(userID)
}
func (a *OpenTracingAppLayer) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteGuestsToChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InviteGuestsToChannels(teamID, guestsInvite, senderId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteGuestsToChannelsGracefully")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InviteGuestsToChannelsGracefully(teamID, guestsInvite, senderId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) InviteNewUsersToTeam(emailList []string, teamID string, senderId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InviteNewUsersToTeam(emailList, teamID, senderId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID string, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeamGracefully")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InviteNewUsersToTeamGracefully(memberInvite, teamID, senderId, reminderInterval)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsCRTEnabledForUser(c request.CTX, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsCRTEnabledForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsCRTEnabledForUser(c, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) IsFirstAdmin(user *model.User) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsFirstAdmin(user)
return resultVar0
}
func (a *OpenTracingAppLayer) IsFirstUserAccount() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstUserAccount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsFirstUserAccount()
return resultVar0
}
func (a *OpenTracingAppLayer) IsLeader() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsLeader")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsLeader()
return resultVar0
}
func (a *OpenTracingAppLayer) IsPasswordValid(password string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPasswordValid")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPasswordValid(password)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPhase2MigrationCompleted")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPhase2MigrationCompleted()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) IsPluginActive(pluginName string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPluginActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.IsPluginActive(pluginName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsUserSignUpAllowed()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.JoinChannel(c, channel, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinDefaultChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.JoinDefaultChannels(c, teamID, user, shouldBeAdmin, userRequestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinUserToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.JoinUserToTeam(c, team, user, userRequestorId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LeaveChannel(c, channelID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LeaveTeam(c, team, user, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) License() *model.License {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.License")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.License()
return resultVar0
}
func (a *OpenTracingAppLayer) LimitedClientConfig() map[string]string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LimitedClientConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LimitedClientConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListAllCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListAllCommands(teamID, T)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListAutocompleteCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("teamID", teamID)
defer span.Finish()
resultVar0, resultVar1 := a.app.ListAutocompleteCommands(teamID, T)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListDirectory(path string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListDirectory(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListDirectoryRecursively(path string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectoryRecursively")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListDirectoryRecursively(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListExports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListExports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListExports()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListImports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListImports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListImports()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListPluginKeys(pluginID string, page int, perPage int) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListPluginKeys")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListPluginKeys(pluginID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListTeamCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListTeamCommands(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) LogAuditRec(rec *audit.Record, err error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LogAuditRec")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.LogAuditRec(rec, err)
}
func (a *OpenTracingAppLayer) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level, err error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LogAuditRecWithLevel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.LogAuditRecWithLevel(rec, level, err)
}
func (a *OpenTracingAppLayer) LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LoginByOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.LoginByOAuth(c, service, userData, teamID, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MakeAuditRecord(event string, initialStatus string) *audit.Record {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MakeAuditRecord")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MakeAuditRecord(event, initialStatus)
return resultVar0
}
func (a *OpenTracingAppLayer) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MakePermissionError")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MakePermissionError(s, permissions)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelAsUnreadFromPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(c, postID, userID, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelsAsViewed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelsAsViewed(c, channelIDs, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MaxPostSize() int {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MaxPostSize")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MaxPostSize()
return resultVar0
}
func (a *OpenTracingAppLayer) MentionsToPublicChannels(c request.CTX, message string, teamID string) model.ChannelMentionMap {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MentionsToPublicChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MentionsToPublicChannels(c, message, teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) MentionsToTeamMembers(c request.CTX, message string, teamID string) model.UserMentionMap {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MentionsToTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MentionsToTeamMembers(c, message, teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MigrateFilenamesToFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MigrateFilenamesToFileInfos(post)
return resultVar0
}
func (a *OpenTracingAppLayer) MigrateIdLDAP(toAttribute string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MigrateIdLDAP")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MigrateIdLDAP(toAttribute)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveChannel(c, team, channel, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveCommand(team, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveFile(oldPath string, newPath string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveFile(oldPath, newPath)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewPluginAPI")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NewPluginAPI(c, manifest)
return resultVar0
}
func (a *OpenTracingAppLayer) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewWebConn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NewWebConn(cfg)
return resultVar0
}
func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NotifyAndSetWarnMetricAck(warnMetricId, sender, forceAck, isBot)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NotifySelfHostedSignupProgress(progress string, userId string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySelfHostedSignupProgress")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.NotifySelfHostedSignupProgress(progress, userId)
}
func (a *OpenTracingAppLayer) NotifySessionsExpired() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NotifySessionsExpired()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NotifySharedChannelUserUpdate(user *model.User) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySharedChannelUserUpdate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.NotifySharedChannelUserUpdate(user)
}
func (a *OpenTracingAppLayer) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.OpenInteractiveDialog(request)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) OriginChecker() func(*http.Request) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OriginChecker")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.OriginChecker()
return resultVar0
}
func (a *OpenTracingAppLayer) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OverrideIconURLIfEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.OverrideIconURLIfEmoji(c, post)
}
func (a *OpenTracingAppLayer) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchBot(botUserId, botPatch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchChannel(c, channel, patch, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannelModerationsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchChannelModerationsForChannel(c, channel, channelModerationsPatch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchPost(c *request.Context, postID string, patch *model.PostPatch) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchPost(c, postID, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchRetentionPolicy(patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchRole(role, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchScheme(scheme, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchTeam(teamID, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchUser(c request.CTX, userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchUser(c, userID, patch, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteAllUsers(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteBot(botUserId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteBot(botUserId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteChannel(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteTeam(c request.CTX, team *model.Team) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteTeam(c, team)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteTeamId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteTeamId(c, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteUser(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PopulateWebConnConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PopulateWebConnConfig(s, cfg, seqVal)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PostActionCookieSecret() []byte {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostActionCookieSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostActionCookieSecret()
return resultVar0
}
func (a *OpenTracingAppLayer) PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostAddToChannelMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostAddToChannelMessage(c, user, addedUser, channel, postRootId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostCountsByDuration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PostCountsByDuration(c, channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostPatchWithProxyRemovedFromImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostPatchWithProxyRemovedFromImageURLs(patch)
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelDisplayNameMessage(c request.CTX, userID string, channel *model.Channel, oldChannelDisplayName string, newChannelDisplayName string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelDisplayNameMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, newChannelDisplayName)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelHeaderMessage(c request.CTX, userID string, channel *model.Channel, oldChannelHeader string, newChannelHeader string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelHeaderMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, newChannelHeader)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelPurposeMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, newChannelPurpose)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostWithProxyAddedToImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostWithProxyAddedToImageURLs(post)
return resultVar0
}
func (a *OpenTracingAppLayer) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostWithProxyRemovedFromImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostWithProxyRemovedFromImageURLs(post)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClient")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClientWithEmbedsAndImages")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost, includePriority)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostListForClient")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostListForClient(c, originalList)
return resultVar0
}
func (a *OpenTracingAppLayer) ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessSlackAttachments")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ProcessSlackAttachments(attachments)
return resultVar0
}
func (a *OpenTracingAppLayer) ProcessSlackText(text string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessSlackText")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ProcessSlackText(text)
return resultVar0
}
func (a *OpenTracingAppLayer) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PromoteGuestToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PromoteGuestToUser(c, user, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) Publish(message *model.WebSocketEvent) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Publish")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.Publish(message)
}
func (a *OpenTracingAppLayer) PublishUserTyping(userID string, channelID string, parentId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PublishUserTyping")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PublishUserTyping(userID, channelID, parentId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PurgeBleveIndexes() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeBleveIndexes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PurgeBleveIndexes()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PurgeElasticsearchIndexes() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeElasticsearchIndexes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PurgeElasticsearchIndexes()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) QueryLogs(page int, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.QueryLogs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.QueryLogs(page, perPage, logFilter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ReadFile(path string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ReadFile(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RecycleDatabaseConnection() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RecycleDatabaseConnection")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.RecycleDatabaseConnection()
}
func (a *OpenTracingAppLayer) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenCommandToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenCommandToken(cmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenOutgoingWebhookToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenOutgoingWebhookToken(hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenerateOAuthAppSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenerateOAuthAppSecret(app)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenerateTeamInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenerateTeamInviteId(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegisterCollectionAndTopic(pluginID string, collectionType string, topicType string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterCollectionAndTopic")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterCollectionAndTopic(pluginID, collectionType, topicType)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RegisterPluginCommand(pluginID string, command *model.Command) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterPluginCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterPluginCommand(pluginID, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RegisterProductCommand(ProductID string, command *model.Command) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterProductCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterProductCommand(ProductID, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ReloadConfig() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReloadConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ReloadConfig()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveAllDeactivatedMembersFromChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveAllDeactivatedMembersFromChannel(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveChannelsFromRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveChannelsFromRetentionPolicy(policyID, channelIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveConfigListener(id string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveConfigListener")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.RemoveConfigListener(id)
}
func (a *OpenTracingAppLayer) RemoveCustomStatus(c request.CTX, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveCustomStatus(c, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveDirectory(path string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveDirectory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveDirectory(path)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveFile(path string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveFile(path)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveLdapPrivateCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveLdapPrivateCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveLdapPublicCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveLdapPublicCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveRecentCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveRecentCustomStatus(userID, status)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlIdpCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlIdpCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlIdpCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlPrivateCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlPrivateCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlPublicCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlPublicCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveTeamIcon(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveTeamIcon(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveTeamsFromRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveTeamsFromRetentionPolicy(policyID, teamIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUserFromChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUserFromChannel(c, userIDToRemove, removerUserId, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUserFromTeam(c request.CTX, teamID string, userID string, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUserFromTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUserFromTeam(c, teamID, userID, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUsersFromChannelNotMemberOfTeam(c request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUsersFromChannelNotMemberOfTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUsersFromChannelNotMemberOfTeam(c, remover, channel, team)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RenameChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RenameChannel(c, channel, newChannelName, newDisplayName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RenameTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RenameTeam(team, newTeamName, newDisplayName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RequestLicenseAndAckWarnMetric(c *request.Context, warnMetricId string, isBot bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RequestLicenseAndAckWarnMetric")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RequestLicenseAndAckWarnMetric(c, warnMetricId, isBot)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetPasswordFromToken(c request.CTX, userSuppliedTokenString string, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetPasswordFromToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResetPasswordFromToken(c, userSuppliedTokenString, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetPermissionsSystem() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetPermissionsSystem")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResetPermissionsSystem()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (numAffected int, appErr *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetSamlAuthDataToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ResetSamlAuthDataToEmail(includeDeleted, dryRun, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestoreChannel(c, channel, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestoreGroup(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreTeam(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RestoreTeam(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RestrictUsersGetByPermissions(userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestrictUsersGetByPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestrictUsersGetByPermissions(userID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestrictUsersSearchByPermissions(userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestrictUsersSearchByPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestrictUsersSearchByPermissions(userID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ReturnSessionToPool(session *model.Session) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReturnSessionToPool")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ReturnSessionToPool(session)
}
func (a *OpenTracingAppLayer) RevokeAccessToken(token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeAllSessions(userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeAllSessions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeAllSessions(userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSession(session *model.Session) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSession(session)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionById(sessionID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionById(sessionID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionsForDeviceId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionsForDeviceId(userID, deviceID, currentSessionId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionsFromAllUsers() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionsFromAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionsFromAllUsers()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RolesGrantPermission(roleNames []string, permissionId string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RolesGrantPermission")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RolesGrantPermission(roleNames, permissionId)
return resultVar0
}
func (a *OpenTracingAppLayer) SanitizePostListMetadataForUser(c request.CTX, postList *model.PostList, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizePostListMetadataForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SanitizePostListMetadataForUser(c, postList, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userID string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizePostMetadataForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SanitizePostMetadataForUser(c, post, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SanitizeProfile(user *model.User, asAdmin bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeProfile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SanitizeProfile(user, asAdmin)
}
func (a *OpenTracingAppLayer) SanitizeTeam(session model.Session, team *model.Team) *model.Team {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SanitizeTeam(session, team)
return resultVar0
}
func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SanitizeTeams(session, teams)
return resultVar0
}
func (a *OpenTracingAppLayer) SaveAcknowledgementForPost(c *request.Context, postID string, userID string) (*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveAcknowledgementForPost(c, postID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveAdminNotification(userId, notifyData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotifyData")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveAdminNotifyData(data)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveBrandImage(imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveComplianceReport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveComplianceReport(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SaveConfig(newCfg, sendConfigChangeClusterMessage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveReactionForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveReactionForPost(c, reaction)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveSharedChannel(c, sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveSharedChannelRemote(remote)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveUserTermsOfService(userID string, termsOfServiceId string, accepted bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveUserTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveUserTermsOfService(userID, termsOfServiceId, accepted)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SchemesIterator(scope string, batchSize int) func() []*model.Scheme {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SchemesIterator")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SchemesIterator(scope, batchSize)
return resultVar0
}
func (a *OpenTracingAppLayer) SearchAllChannels(c request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchAllChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SearchAllChannels(c, term, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchAllTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SearchAllTeams(searchOpts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SearchArchivedChannels(c request.CTX, teamID string, term string, userID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchArchivedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchArchivedChannels(c, teamID, term, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannels(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannels(c, teamID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannelsForUser(c request.CTX, userID string, teamID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannelsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannelsForUser(c, userID, teamID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannelsUserNotIn(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannelsUserNotIn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannelsUserNotIn(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchEmoji(c request.CTX, name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchEmoji(c, name, prefixOnly, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchEngine() *searchengine.Broker {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchEngine")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SearchEngine()
return resultVar0
}
func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.FileInfoList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchFilesInTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchGroupChannels(c request.CTX, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchGroupChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchGroupChannels(c, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPostsInTeam(teamID, paramsList)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPrivateTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPrivateTeams(searchOpts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPublicTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPublicTeams(searchOpts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUserAccessTokens")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUserAccessTokens(term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsers(props, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInChannel(channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInGroup(groupID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInTeam(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInTeam(teamID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInChannel(teamID, channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInGroup(groupID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInTeam(notInTeamId, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersWithoutTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersWithoutTeam(term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendAckToPushProxy(ack *model.PushNotificationAck) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAckToPushProxy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendAckToPushProxy(ack)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAutoResponse")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendAutoResponse(c, channel, receiver, post)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAutoResponseIfNecessary")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendAutoResponseIfNecessary(c, channel, sender, post)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendDelinquencyEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendDelinquencyEmail(emailToSend)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendEmailVerification(user *model.User, newEmail string, redirect string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEmailVerification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendEmailVerification(user, newEmail, redirect)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendEphemeralPost(c, userID, post)
return resultVar0
}
func (a *OpenTracingAppLayer) SendNoCardPaymentFailedEmail() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNoCardPaymentFailedEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendNoCardPaymentFailedEmail()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendNotifications(c, post, team, channel, sender, parentPostList, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifyAdminPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendNotifyAdminPosts(c, workspaceName, currentSKU, trial)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPasswordReset")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendPasswordReset(email, siteURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPaymentFailedEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendPaymentFailedEmail(failedPayment)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendSubscriptionHistoryEvent(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendTestPushNotification(deviceID)
return resultVar0
}
func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendUpgradeConfirmationEmail(isYearly)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId string, destinationPluginId string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ServeInterPluginRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ServeInterPluginRequest(w, r, sourcePluginId, destinationPluginId)
}
func (a *OpenTracingAppLayer) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionTo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionTo(session, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToAny")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToAny(session, permissions)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToCategory(c request.CTX, session model.Session, userID string, teamID string, categoryId string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToCategory(c, session, userID, teamID, categoryId)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannel(c request.CTX, session model.Session, channelID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannel(c, session, channelID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannelByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannelByPost(session, postID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannels(c request.CTX, session model.Session, channelIDs []string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannels(c, session, channelIDs, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToCreateJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SessionHasPermissionToCreateJob(session, job)
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToGroup(session, groupID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToManageBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToManageBot(session, botUserId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToReadJob(session model.Session, jobType string) (bool, *model.Permission) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToReadJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SessionHasPermissionToReadJob(session, jobType)
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToTeam(session, teamID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToTeams(c request.CTX, session model.Session, teamIDs []string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToTeams(c, session, teamIDs, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToUser(session model.Session, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToUser(session, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToUserOrBot(session model.Session, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToUserOrBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToUserOrBot(session, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionIsRegistered(session model.Session) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionIsRegistered")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionIsRegistered(session)
return resultVar0
}
func (a *OpenTracingAppLayer) SetActiveChannel(c request.CTX, userID string, channelID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetActiveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetActiveChannel(c, userID, channelID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetAutoResponderStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetAutoResponderStatus(user, oldNotifyProps)
}
func (a *OpenTracingAppLayer) SetChannels(ch *app.Channels) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetChannels(ch)
}
func (a *OpenTracingAppLayer) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetCustomStatus(c, userID, cs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetDefaultProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetDefaultProfileImage(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPhase2PermissionsMigrationStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPhase2PermissionsMigrationStatus(isComplete)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKey(pluginID string, key string, value []byte) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPluginKey(pluginID, key, value)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKeyWithExpiry")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPluginKeyWithExpiry(pluginID, key, value, expireInSeconds)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKeyWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKeyWithOptions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SetPluginKeyWithOptions(pluginID, key, value, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SetPostReminder(postID string, userID string, targetTime int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPostReminder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPostReminder(postID, userID, targetTime)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImage(c request.CTX, userID string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImage(c, userID, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImageFromFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImageFromFile(c, userID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImageFromMultiPartFile(c request.CTX, userID string, file multipart.File) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImageFromMultiPartFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImageFromMultiPartFile(c, userID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetRemoteClusterLastPingAt")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetRemoteClusterLastPingAt(remoteClusterId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSamlIdpCertificateFromMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetSamlIdpCertificateFromMetadata(data)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetSearchEngine(se *searchengine.Broker) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSearchEngine")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetSearchEngine(se)
}
func (a *OpenTracingAppLayer) SetSessionExpireInHours(session *model.Session, hours int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSessionExpireInHours")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetSessionExpireInHours(session, hours)
}
func (a *OpenTracingAppLayer) SetStatusAwayIfNeeded(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusAwayIfNeeded")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusAwayIfNeeded(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusDoNotDisturb(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusDoNotDisturb")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusDoNotDisturb(userID)
}
func (a *OpenTracingAppLayer) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusDoNotDisturbTimed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusDoNotDisturbTimed(userId, endtime)
}
func (a *OpenTracingAppLayer) SetStatusLastActivityAt(userID string, activityAt int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusLastActivityAt")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusLastActivityAt(userID, activityAt)
}
func (a *OpenTracingAppLayer) SetStatusOffline(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOffline")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOffline(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusOnline(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOnline")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOnline(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusOutOfOffice(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOutOfOffice")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOutOfOffice(userID)
}
func (a *OpenTracingAppLayer) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIcon(teamID, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIconFromFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIconFromFile(team, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIconFromMultiPartFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIconFromMultiPartFile(teamID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SlackImport(c *request.Context, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SlackImport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SlackImport(c, fileData, fileSize, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteAllTeamsExcept")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SoftDeleteAllTeamsExcept(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SoftDeleteTeam(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SoftDeleteTeam(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SubmitInteractiveDialog(c *request.Context, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SubmitInteractiveDialog")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SubmitInteractiveDialog(c, request)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchEmailToLdap(email string, password string, code string, ldapLoginId string, ldapPassword string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchEmailToLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email string, password string, code string, service string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchEmailToOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchEmailToOAuth(w, r, email, password, code, service)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchLdapToEmail(ldapPassword string, code string, email string, newPassword string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchLdapToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchLdapToEmail(ldapPassword, code, email, newPassword)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchOAuthToEmail(email string, password string, requesterId string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchOAuthToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchOAuthToEmail(email, password, requesterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SyncLdap(includeRemovedMembers bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SyncLdap(includeRemovedMembers)
}
func (a *OpenTracingAppLayer) SyncPlugins() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SyncPlugins()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SyncRolesAndMembership(c request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncRolesAndMembership")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SyncRolesAndMembership(c, syncableID, syncableType, includeRemovedMembers)
}
func (a *OpenTracingAppLayer) SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncSyncableRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SyncSyncableRoles(syncableID, syncableType)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersMinusGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersToAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersToRemove")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.TeamMembersToRemove(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TelemetryId() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TelemetryId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TelemetryId()
return resultVar0
}
func (a *OpenTracingAppLayer) TestElasticsearch(cfg *model.Config) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestElasticsearch")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestElasticsearch(cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestEmail(userID string, cfg *model.Config) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestEmail(userID, cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestFileStoreConnection() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestFileStoreConnection")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestFileStoreConnection()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestFileStoreConnectionWithConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestFileStoreConnectionWithConfig(cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestLdap() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestLdap()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestSiteURL(siteURL string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestSiteURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestSiteURL(siteURL)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ToggleMuteChannel(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ToggleMuteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ToggleMuteChannel(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TotalWebsocketConnections() int {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TotalWebsocketConnections")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TotalWebsocketConnections()
return resultVar0
}
func (a *OpenTracingAppLayer) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TriggerWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.TriggerWebhook(c, payload, hook, post, channel)
}
func (a *OpenTracingAppLayer) UnregisterPluginCommand(pluginID string, teamID string, trigger string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UnregisterPluginCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UnregisterPluginCommand(pluginID, teamID, trigger)
}
func (a *OpenTracingAppLayer) UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateActive(c, user, active)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateBotActive(c request.CTX, botUserId string, active bool) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateBotActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateBotActive(c, botUserId, active)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateBotOwner(botUserId string, newOwnerId string) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateBotOwner")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateBotOwner(botUserId, newOwnerId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannel(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberNotifyProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberNotifyProps(c, data, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberRoles(c, channelID, userID, newRoles)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberSchemeRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberSchemeRoles(c, channelID, userID, isSchemeGuest, isSchemeUser, isSchemeAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelPrivacy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelPrivacy(c, oldChannel, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateCommand(oldCmd *model.Command, updatedCmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateCommand(oldCmd, updatedCmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateConfig(f func(*model.Config)) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateConfig(f)
}
func (a *OpenTracingAppLayer) UpdateDNDStatusOfUsers() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDNDStatusOfUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateDNDStatusOfUsers()
}
func (a *OpenTracingAppLayer) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateEphemeralPost(c, userID, post)
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateExpiredDNDStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateExpiredDNDStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateGroup(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateGroupSyncable(groupSyncable)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateHashedPassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateHashedPassword(user, newHashedPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateHashedPasswordByUserId(userID string, newHashedPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateHashedPasswordByUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateHashedPasswordByUserId(userID, newHashedPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateIncomingWebhook(oldHook *model.IncomingWebhook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateIncomingWebhook(oldHook, updatedHook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateMfa(c request.CTX, activate bool, userID string, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateMfa(c, activate, userID, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateMobileAppBadge(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateMobileAppBadge")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateMobileAppBadge(userID)
}
func (a *OpenTracingAppLayer) UpdateOAuthApp(oldApp *model.OAuthApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateOAuthApp(oldApp, updatedApp)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OAuthProvider, service string, tokenUser *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOAuthUserAttrs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateOAuthUserAttrs(userData, user, provider, service, tokenUser)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateOutgoingWebhook(c request.CTX, oldHook *model.OutgoingWebhook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateOutgoingWebhook(c, oldHook, updatedHook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdatePassword(user *model.User, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePassword(user, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordAsUser(c request.CTX, userID string, currentPassword string, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordAsUser(c, userID, currentPassword, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordByUserIdSendEmail(c request.CTX, userID string, newPassword string, method string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordByUserIdSendEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordByUserIdSendEmail(c, userID, newPassword, method)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword string, method string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordSendEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordSendEmail(c, user, newPassword, method)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdatePost(c, post, safeUpdate)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePreferences")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePreferences(userID, preferences)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateProductNotices() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateProductNotices()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRemoteCluster(rc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteClusterTopics")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRemoteClusterTopics(remoteClusterId, topics)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRole(role)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateScheme(scheme)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateSharedChannel(sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannelRemoteCursor")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateSharedChannelRemoteCursor(id, cursor)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateSidebarCategories(c request.CTX, userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSidebarCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateSidebarCategories(c, userID, teamID, categories)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSidebarCategoryOrder(c request.CTX, userID string, teamID string, categoryOrder []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSidebarCategoryOrder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateSidebarCategoryOrder(c, userID, teamID, categoryOrder)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeam(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamMemberRoles(teamID string, userID string, newRoles string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamMemberRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamMemberRoles(teamID, userID, newRoles)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamMemberSchemeRoles(teamID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamMemberSchemeRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamMemberSchemeRoles(teamID, userID, isSchemeGuest, isSchemeUser, isSchemeAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamPrivacy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateTeamPrivacy(teamID, teamType, allowOpenInvite)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamScheme(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, teamID string, threadID string, state bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadFollowForUser(userID, teamID, threadID, state)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID string, teamID string, threadID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUserFromChannelAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadFollowForUserFromChannelAdd(c, userID, teamID, threadID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateThreadReadForUser(c request.CTX, currentSessionId string, userID string, teamID string, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateThreadReadForUser(c, currentSessionId, userID, teamID, threadID, timestamp)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadReadForUserByPost(c request.CTX, currentSessionId string, userID string, teamID string, threadID string, postID string) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUserByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateThreadReadForUserByPost(c, currentSessionId, userID, teamID, threadID, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadsReadForUser(userID string, teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadsReadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadsReadForUser(userID, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateUser(c request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUser(c, user, sendNotifications)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateUserActive(c, userID, active)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserAsUser(c, user, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserAuth(userID, userAuth)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserRoles(c request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserRoles(c, userID, newRoles, sendWebSocketEvent)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserRolesWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserRolesWithUser(c, user, newRoles, sendWebSocketEvent)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateViewedProductNotices(userID string, noticeIds []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateViewedProductNotices(userID, noticeIds)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateViewedProductNoticesForNewUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNoticesForNewUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateViewedProductNoticesForNewUser(userID)
}
func (a *OpenTracingAppLayer) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateWebConnUserActivity")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateWebConnUserActivity(session, activityAt)
}
func (a *OpenTracingAppLayer) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadData")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadData(c, us, rd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadEmojiImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UploadEmojiImage(c, id, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UploadFile(c request.CTX, data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadFile(c, data, channelID, filename)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UploadFileX(c *request.Context, channelID string, name string, input io.Reader, opts ...func(*app.UploadFileTask)) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFileX")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadFileX(c, channelID, name, input, opts...)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupMember(groupID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupMembers(groupID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupSyncable(groupSyncable)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserAlreadyNotifiedOnRequiredFeature")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UserAlreadyNotifiedOnRequiredFeature(user, feature)
return resultVar0
}
func (a *OpenTracingAppLayer) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserCanSeeOtherUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UserCanSeeOtherUser(userID, otherUserId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UserIsFirstAdmin(user *model.User) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsFirstAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UserIsFirstAdmin(user)
return resultVar0
}
func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsInAdminRoleGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UserIsInAdminRoleGroup(userID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ValidateUserPermissionsOnChannels(c, userId, channelIds)
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyEmailFromToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyEmailFromToken(c, userSuppliedTokenString)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyPlugin(plugin io.ReadSeeker, signature io.ReadSeeker) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyPlugin(plugin, signature)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyUserEmail(userID string, email string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyUserEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyUserEmail(userID, email)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ViewChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ViewChannel(c, view, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.WriteFile(fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFileContext")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.WriteFileContext(ctx, fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer {
newApp := OpenTracingAppLayer{
app: childApp,
ctx: ctx,
}
newApp.srv = childApp.Srv()
newApp.log = childApp.Log()
newApp.notificationsLog = childApp.NotificationsLog()
newApp.accountMigration = childApp.AccountMigration()
newApp.cluster = childApp.Cluster()
newApp.compliance = childApp.Compliance()
newApp.dataRetention = childApp.DataRetention()
newApp.searchEngine = childApp.SearchEngine()
newApp.ldap = childApp.Ldap()
newApp.messageExport = childApp.MessageExport()
newApp.metrics = childApp.Metrics()
newApp.notification = childApp.Notification()
newApp.saml = childApp.Saml()
newApp.httpService = childApp.HTTPService()
newApp.imageProxy = childApp.ImageProxy()
newApp.timezones = childApp.Timezones()
return &newApp
}
func (a *OpenTracingAppLayer) Srv() *app.Server {
return a.srv
}
func (a *OpenTracingAppLayer) Log() *mlog.Logger {
return a.log
}
func (a *OpenTracingAppLayer) NotificationsLog() *mlog.Logger {
return a.notificationsLog
}
func (a *OpenTracingAppLayer) AccountMigration() einterfaces.AccountMigrationInterface {
return a.accountMigration
}
func (a *OpenTracingAppLayer) Cluster() einterfaces.ClusterInterface {
return a.cluster
}
func (a *OpenTracingAppLayer) Compliance() einterfaces.ComplianceInterface {
return a.compliance
}
func (a *OpenTracingAppLayer) DataRetention() einterfaces.DataRetentionInterface {
return a.dataRetention
}
func (a *OpenTracingAppLayer) Ldap() einterfaces.LdapInterface {
return a.ldap
}
func (a *OpenTracingAppLayer) MessageExport() einterfaces.MessageExportInterface {
return a.messageExport
}
func (a *OpenTracingAppLayer) Metrics() einterfaces.MetricsInterface {
return a.metrics
}
func (a *OpenTracingAppLayer) Notification() einterfaces.NotificationInterface {
return a.notification
}
func (a *OpenTracingAppLayer) Saml() einterfaces.SamlInterface {
return a.saml
}
func (a *OpenTracingAppLayer) HTTPService() httpservice.HTTPService {
return a.httpService
}
func (a *OpenTracingAppLayer) ImageProxy() *imageproxy.ImageProxy {
return a.imageProxy
}
func (a *OpenTracingAppLayer) Timezones() *timezones.Timezones {
return a.timezones
}
func (a *OpenTracingAppLayer) SetServer(srv *app.Server) {
a.srv = srv
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Option func(s *Server) error
// By default, the app will use the store specified by the configuration. This allows you to
// construct an app with a different store.
//
// The override parameter must be either a store.Store or func(App) store.Store().
func StoreOverride(override any) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StoreOverride(override))
return nil
}
}
func StoreOverrideWithCache(override store.Store) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StoreOverrideWithCache(override))
return nil
}
}
// Config applies the given config dsn, whether a path to config.json
// or a database connection string. It receives as well a set of
// custom defaults that will be applied for any unset property of the
// config loaded from the dsn on top of the normal defaults
func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.Config(dsn, readOnly, configDefaults))
return nil
}
}
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
func ConfigStore(configStore *config.Store) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.ConfigStore(configStore))
return nil
}
}
func SetFileStore(filestore filestore.FileBackend) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetFileStore(filestore))
return nil
}
}
func RunEssentialJobs(s *Server) error {
s.runEssentialJobs = true
return nil
}
func JoinCluster(s *Server) error {
s.joinCluster = true
return nil
}
func StartMetrics(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StartMetrics())
return nil
}
func WithLicense(license *model.License) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, func(p *platform.PlatformService) error {
p.SetLicense(license)
return nil
})
return nil
}
}
// SetLogger requires platform service to be initialized before calling.
// If not, logger should be set after platform service are initialized.
func SetLogger(logger *mlog.Logger) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetLogger(logger))
return nil
}
}
func SkipPostInitialization() Option {
return func(s *Server) error {
s.skipPostInit = true
return nil
}
}
type AppOption func(a *App)
type AppOptionCreator func() []AppOption
func ServerConnector(ch *Channels) AppOption {
return func(a *App) {
a.ch = ch
}
}
func SetCluster(impl einterfaces.ClusterInterface) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetCluster(impl))
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
const permissionsExportBatchSize = 100
const systemSchemeName = "00000000-0000-0000-0000-000000000000" // Prevents collisions with user-created schemes.
// Ensure permissions service wrapper implements `product.PermissionService`
var _ product.PermissionService = (*permissionsServiceWrapper)(nil)
// permissionsServiceWrapper provides an implementation of `product.PermissionService` for use by products.
type permissionsServiceWrapper struct {
app AppIface
}
func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool {
return s.app.HasPermissionTo(userID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToTeam(userID string, teamID string, permission *model.Permission) bool {
return s.app.HasPermissionToTeam(userID, teamID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToChannel(askingUserID string, channelID string, permission *model.Permission) bool {
return s.app.HasPermissionToChannel(request.EmptyContext(s.app.Log()), askingUserID, channelID, permission)
}
func (s *permissionsServiceWrapper) RolesGrantPermission(roleNames []string, permissionId string) bool {
return s.app.RolesGrantPermission(roleNames, permissionId)
}
func (a *App) ResetPermissionsSystem() *model.AppError {
// Reset all Teams to not have a scheme.
if err := a.Srv().Store().Team().ResetAllTeamSchemes(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.team.reset_all_team_schemes.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Channels to not have a scheme.
if err := a.Srv().Store().Channel().ResetAllChannelSchemes(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.channel.reset_all_channel_schemes.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to Users.
if err := a.Srv().Store().User().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.user.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to TeamMembers.
if err := a.Srv().Store().Team().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.team.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to ChannelMembers.
if err := a.Srv().Store().Channel().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.channel.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Purge all schemes from the database.
if err := a.Srv().Store().Scheme().PermanentDeleteAll(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.scheme.permanent_delete_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Purge all roles from the database.
if err := a.Srv().Store().Role().PermanentDeleteAll(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.role.permanent_delete_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the advanced permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.AdvancedPermissionsMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the emoji permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(EmojisPermissionsMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the guest roles permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(GuestRolesCreationMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Now that the permissions system has been reset, re-run the migration to reinitialise it.
a.DoAppMigrations()
return nil
}
func (a *App) ExportPermissions(w io.Writer) error {
next := a.SchemesIterator("", permissionsExportBatchSize)
var schemeBatch []*model.Scheme
for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() {
for _, scheme := range schemeBatch {
roleNames := []string{
scheme.DefaultTeamAdminRole,
scheme.DefaultTeamUserRole,
scheme.DefaultTeamGuestRole,
scheme.DefaultChannelAdminRole,
scheme.DefaultChannelUserRole,
scheme.DefaultChannelGuestRole,
}
roles := []*model.Role{}
for _, roleName := range roleNames {
if roleName == "" {
continue
}
role, err := a.GetRoleByName(context.Background(), roleName)
if err != nil {
return err
}
roles = append(roles, role)
}
schemeExport, err := json.Marshal(&model.SchemeConveyor{
Name: scheme.Name,
DisplayName: scheme.DisplayName,
Description: scheme.Description,
Scope: scheme.Scope,
TeamAdmin: scheme.DefaultTeamAdminRole,
TeamUser: scheme.DefaultTeamUserRole,
TeamGuest: scheme.DefaultTeamGuestRole,
ChannelAdmin: scheme.DefaultChannelAdminRole,
ChannelUser: scheme.DefaultChannelUserRole,
ChannelGuest: scheme.DefaultChannelGuestRole,
Roles: roles,
})
if err != nil {
return err
}
schemeExport = append(schemeExport, []byte("\n")...)
_, err = w.Write(schemeExport)
if err != nil {
return err
}
}
}
defaultRoleNames := []string{}
for _, dr := range model.MakeDefaultRoles() {
defaultRoleNames = append(defaultRoleNames, dr.Name)
}
roles, appErr := a.GetRolesByNames(defaultRoleNames)
if appErr != nil {
return errors.New(appErr.Message)
}
schemeExport, err := json.Marshal(&model.SchemeConveyor{
Name: systemSchemeName,
Roles: roles,
})
if err != nil {
return err
}
schemeExport = append(schemeExport, []byte("\n")...)
_, err = w.Write(schemeExport)
return err
}
func (a *App) ImportPermissions(jsonl io.Reader) error {
createdSchemeIDs := []string{}
scanner := bufio.NewScanner(jsonl)
for scanner.Scan() {
var schemeConveyor *model.SchemeConveyor
err := json.Unmarshal(scanner.Bytes(), &schemeConveyor)
if err != nil {
rollback(a, createdSchemeIDs)
return err
}
if schemeConveyor.Name == systemSchemeName {
for _, roleIn := range schemeConveyor.Roles {
dbRole, err := a.GetRoleByName(context.Background(), roleIn.Name)
if err != nil {
rollback(a, createdSchemeIDs)
return errors.New(err.Message)
}
_, err = a.PatchRole(dbRole, &model.RolePatch{
Permissions: &roleIn.Permissions,
})
if err != nil {
rollback(a, createdSchemeIDs)
return err
}
}
continue
}
// Create the new Scheme. The new Roles are created automatically.
var appErr *model.AppError
schemeCreated, appErr := a.CreateScheme(schemeConveyor.Scheme())
if appErr != nil {
rollback(a, createdSchemeIDs)
return errors.New(appErr.Message)
}
createdSchemeIDs = append(createdSchemeIDs, schemeCreated.Id)
schemeIn := schemeConveyor.Scheme()
roleNameTuples := [][]string{
{schemeCreated.DefaultTeamAdminRole, schemeIn.DefaultTeamAdminRole},
{schemeCreated.DefaultTeamUserRole, schemeIn.DefaultTeamUserRole},
{schemeCreated.DefaultTeamGuestRole, schemeIn.DefaultTeamGuestRole},
{schemeCreated.DefaultChannelAdminRole, schemeIn.DefaultChannelAdminRole},
{schemeCreated.DefaultChannelUserRole, schemeIn.DefaultChannelUserRole},
{schemeCreated.DefaultChannelGuestRole, schemeIn.DefaultChannelGuestRole},
}
for _, roleNameTuple := range roleNameTuples {
if roleNameTuple[0] == "" || roleNameTuple[1] == "" {
continue
}
err = updateRole(a, schemeConveyor, roleNameTuple[0], roleNameTuple[1])
if err != nil {
// Delete the new Schemes. The new Roles are deleted automatically.
rollback(a, createdSchemeIDs)
return err
}
}
}
if err := scanner.Err(); err != nil {
rollback(a, createdSchemeIDs)
return err
}
return nil
}
func rollback(a *App, createdSchemeIDs []string) {
for _, schemeID := range createdSchemeIDs {
a.DeleteScheme(schemeID)
}
}
func updateRole(a *App, sc *model.SchemeConveyor, roleCreatedName, defaultRoleName string) error {
var err *model.AppError
roleCreated, err := a.GetRoleByName(context.Background(), roleCreatedName)
if err != nil {
return errors.New(err.Message)
}
var roleIn *model.Role
for _, role := range sc.Roles {
if role.Name == defaultRoleName {
roleIn = role
break
}
}
roleCreated.DisplayName = roleIn.DisplayName
roleCreated.Description = roleIn.Description
roleCreated.Permissions = roleIn.Permissions
_, err = a.UpdateRole(roleCreated)
if err != nil {
return errors.New(fmt.Sprintf("%v: %v\n", err.Message, err.DetailedError))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type permissionTransformation struct {
On func(*model.Role, map[string]map[string]bool) bool
Add []string
Remove []string
}
type permissionsMap []permissionTransformation
const (
PermissionManageSystem = "manage_system"
PermissionManageTeam = "manage_team"
PermissionManageEmojis = "manage_emojis"
PermissionManageOthersEmojis = "manage_others_emojis"
PermissionCreateEmojis = "create_emojis"
PermissionDeleteEmojis = "delete_emojis"
PermissionDeleteOthersEmojis = "delete_others_emojis"
PermissionManageWebhooks = "manage_webhooks"
PermissionManageOthersWebhooks = "manage_others_webhooks"
PermissionManageIncomingWebhooks = "manage_incoming_webhooks"
PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks"
PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks"
PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks"
PermissionListPublicTeams = "list_public_teams"
PermissionListPrivateTeams = "list_private_teams"
PermissionJoinPublicTeams = "join_public_teams"
PermissionJoinPrivateTeams = "join_private_teams"
PermissionPermanentDeleteUser = "permanent_delete_user"
PermissionCreateBot = "create_bot"
PermissionReadBots = "read_bots"
PermissionReadOthersBots = "read_others_bots"
PermissionManageBots = "manage_bots"
PermissionManageOthersBots = "manage_others_bots"
PermissionDeletePublicChannel = "delete_public_channel"
PermissionDeletePrivateChannel = "delete_private_channel"
PermissionManagePublicChannelProperties = "manage_public_channel_properties"
PermissionManagePrivateChannelProperties = "manage_private_channel_properties"
PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private"
PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public"
PermissionViewMembers = "view_members"
PermissionInviteUser = "invite_user"
PermissionInviteGuest = "invite_guest"
PermissionPromoteGuest = "promote_guest"
PermissionDemoteToGuest = "demote_to_guest"
PermissionUseChannelMentions = "use_channel_mentions"
PermissionCreatePost = "create_post"
PermissionCreatePost_PUBLIC = "create_post_public"
PermissionUseGroupMentions = "use_group_mentions"
PermissionAddReaction = "add_reaction"
PermissionRemoveReaction = "remove_reaction"
PermissionManagePublicChannelMembers = "manage_public_channel_members"
PermissionManagePrivateChannelMembers = "manage_private_channel_members"
PermissionReadJobs = "read_jobs"
PermissionManageJobs = "manage_jobs"
PermissionReadOtherUsersTeams = "read_other_users_teams"
PermissionEditOtherUsers = "edit_other_users"
PermissionReadPublicChannelGroups = "read_public_channel_groups"
PermissionReadPrivateChannelGroups = "read_private_channel_groups"
PermissionEditBrand = "edit_brand"
PermissionManageSharedChannels = "manage_shared_channels"
PermissionManageSecureConnections = "manage_secure_connections"
PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections`
)
// Deprecated: This function should only be used if a case arises where team and/or channel scheme roles do not need to be migrated.
// Otherwise, use isRole.
func isExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name == roleName
}
}
// isRole returns true if roleName matches a role's name field or if the a team
// or channel scheme role matches a "common name". A common name is one of the following role
// that is common among the system scheme and the team and/or channel schemes:
//
// TeamAdmin,
// TeamUser,
// TeamGuest,
// ChannelAdmin,
// ChannelUser,
// ChannelGuest,
// PlaybookAdmin,
// PlaybookMember,
// RunAdmin,
// RunMember
func isRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
if role.Name == roleName {
return true
}
return isSchemeRoleAssociatedToCommonName(roleName, role)
}
}
// Deprecated: use isNotRole instead.
func isNotExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name != roleName
}
}
func isNotRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name != roleName && !isSchemeRoleAssociatedToCommonName(roleName, role)
}
}
func isSchemeRoleAssociatedToCommonName(roleName string, role *model.Role) bool {
roleIDToSchemeRoleDisplayName := map[string]string{
model.TeamAdminRoleId: sqlstore.SchemeRoleDisplayNameTeamAdmin,
model.TeamUserRoleId: sqlstore.SchemeRoleDisplayNameTeamUser,
model.TeamGuestRoleId: sqlstore.SchemeRoleDisplayNameTeamGuest,
model.ChannelAdminRoleId: sqlstore.SchemeRoleDisplayNameChannelAdmin,
model.ChannelUserRoleId: sqlstore.SchemeRoleDisplayNameChannelUser,
model.ChannelGuestRoleId: sqlstore.SchemeRoleDisplayNameChannelGuest,
model.PlaybookAdminRoleId: sqlstore.SchemeRoleDisplayNamePlaybookAdmin,
model.PlaybookMemberRoleId: sqlstore.SchemeRoleDisplayNamePlaybookMember,
model.RunAdminRoleId: sqlstore.SchemeRoleDisplayNameRunAdmin,
model.RunMemberRoleId: sqlstore.SchemeRoleDisplayNameRunMember,
}
displayName, ok := roleIDToSchemeRoleDisplayName[roleName]
if !ok {
return false
}
return strings.HasPrefix(role.DisplayName, displayName)
}
func isNotSchemeRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return !strings.Contains(role.DisplayName, roleName)
}
}
func permissionExists(permission string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
val, ok := permissionsMap[role.Name][permission]
return ok && val
}
}
func permissionNotExists(permission string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
val, ok := permissionsMap[role.Name][permission]
return !(ok && val)
}
}
func onOtherRole(otherRole string, function func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return function(&model.Role{Name: otherRole}, permissionsMap)
}
}
func permissionOr(funcs ...func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
for _, f := range funcs {
if f(role, permissionsMap) {
return true
}
}
return false
}
}
func permissionAnd(funcs ...func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
for _, f := range funcs {
if !f(role, permissionsMap) {
return false
}
}
return true
}
}
func applyPermissionsMap(role *model.Role, roleMap map[string]map[string]bool, migrationMap permissionsMap) []string {
var result []string
roleName := role.Name
for _, transformation := range migrationMap {
if transformation.On(role, roleMap) {
for _, permission := range transformation.Add {
roleMap[roleName][permission] = true
}
for _, permission := range transformation.Remove {
roleMap[roleName][permission] = false
}
}
}
for key, active := range roleMap[roleName] {
if active {
result = append(result, key)
}
}
return result
}
func (s *Server) doPermissionsMigration(key string, migrationMap permissionsMap, roles []*model.Role) *model.AppError {
if _, err := s.Store().System().GetByName(key); err == nil {
return nil
}
roleMap := make(map[string]map[string]bool)
for _, role := range roles {
roleMap[role.Name] = make(map[string]bool)
for _, permission := range role.Permissions {
roleMap[role.Name][permission] = true
}
}
for _, role := range roles {
role.Permissions = applyPermissionsMap(role, roleMap, migrationMap)
if _, err := s.Store().Role().Save(role); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("doPermissionsMigration", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("doPermissionsMigration", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if err := s.Store().System().SaveOrUpdate(&model.System{Name: key, Value: "true"}); err != nil {
return model.NewAppError("doPermissionsMigration", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) getEmojisPermissionsSplitMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageEmojis),
Add: []string{PermissionCreateEmojis, PermissionDeleteEmojis},
Remove: []string{PermissionManageEmojis},
},
permissionTransformation{
On: permissionExists(PermissionManageOthersEmojis),
Add: []string{PermissionDeleteOthersEmojis},
Remove: []string{PermissionManageOthersEmojis},
},
}, nil
}
func (a *App) getWebhooksPermissionsSplitMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageWebhooks),
Add: []string{PermissionManageIncomingWebhooks, PermissionManageOutgoingWebhooks},
Remove: []string{PermissionManageWebhooks},
},
permissionTransformation{
On: permissionExists(PermissionManageOthersWebhooks),
Add: []string{PermissionManageOthersIncomingWebhooks, PermissionManageOthersOutgoingWebhooks},
Remove: []string{PermissionManageOthersWebhooks},
},
}, nil
}
func (a *App) getListJoinPublicPrivateTeamsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionListPrivateTeams, PermissionJoinPrivateTeams},
Remove: []string{},
},
permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: []string{PermissionListPublicTeams, PermissionJoinPublicTeams},
Remove: []string{},
},
}, nil
}
func (a *App) removePermanentDeleteUserMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionPermanentDeleteUser),
Remove: []string{PermissionPermanentDeleteUser},
},
}, nil
}
func (a *App) getAddBotPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionCreateBot, PermissionReadBots, PermissionReadOthersBots, PermissionManageBots, PermissionManageOthersBots},
Remove: []string{},
},
}, nil
}
func (a *App) applyChannelManageDeleteToChannelUser() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePrivateChannelProperties))),
Add: []string{PermissionManagePrivateChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePrivateChannel))),
Add: []string{PermissionDeletePrivateChannel},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePublicChannelProperties))),
Add: []string{PermissionManagePublicChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePublicChannel))),
Add: []string{PermissionDeletePublicChannel},
},
}, nil
}
func (a *App) removeChannelManageDeleteFromTeamUser() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePrivateChannelProperties)),
Remove: []string{PermissionManagePrivateChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePrivateChannel)),
Remove: []string{model.PermissionDeletePrivateChannel.Id},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePublicChannelProperties)),
Remove: []string{PermissionManagePublicChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePublicChannel)),
Remove: []string{PermissionDeletePublicChannel},
},
}, nil
}
func (a *App) getViewMembersPermissionMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: []string{PermissionViewMembers},
},
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionViewMembers},
},
}, nil
}
func (a *App) getAddManageGuestsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionPromoteGuest, PermissionDemoteToGuest, PermissionInviteGuest},
},
}, nil
}
func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
transformations := permissionsMap{}
var allTeamSchemes []*model.Scheme
next := a.SchemesIterator(model.SchemeScopeTeam, 100)
var schemeBatch []*model.Scheme
for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() {
allTeamSchemes = append(allTeamSchemes, schemeBatch...)
}
moderatedPermissionsMinusCreatePost := []string{
PermissionAddReaction,
PermissionRemoveReaction,
PermissionManagePublicChannelMembers,
PermissionManagePrivateChannelMembers,
PermissionUseChannelMentions,
}
teamAndChannelAdminConditionalTransformations := func(teamAdminID, channelAdminID, channelUserID, channelGuestID string) []permissionTransformation {
transformations := []permissionTransformation{}
for _, perm := range moderatedPermissionsMinusCreatePost {
// add each moderated permission to the channel admin if channel user or guest has the permission
trans := permissionTransformation{
On: permissionAnd(
isExactRole(channelAdminID),
permissionOr(
onOtherRole(channelUserID, permissionExists(perm)),
onOtherRole(channelGuestID, permissionExists(perm)),
),
),
Add: []string{perm},
}
transformations = append(transformations, trans)
// add each moderated permission to the team admin if channel admin, user, or guest has the permission
trans = permissionTransformation{
On: permissionAnd(
isExactRole(teamAdminID),
permissionOr(
onOtherRole(channelAdminID, permissionExists(perm)),
onOtherRole(channelUserID, permissionExists(perm)),
onOtherRole(channelGuestID, permissionExists(perm)),
),
),
Add: []string{perm},
}
transformations = append(transformations, trans)
}
return transformations
}
for _, ts := range allTeamSchemes {
// ensure all team scheme channel admins have create_post because it's not exposed via the UI
trans := permissionTransformation{
On: isExactRole(ts.DefaultChannelAdminRole),
Add: []string{PermissionCreatePost},
}
transformations = append(transformations, trans)
// ensure all team scheme team admins have create_post because it's not exposed via the UI
trans = permissionTransformation{
On: isExactRole(ts.DefaultTeamAdminRole),
Add: []string{PermissionCreatePost},
}
transformations = append(transformations, trans)
// conditionally add all other moderated permissions to team and channel admins
transformations = append(transformations, teamAndChannelAdminConditionalTransformations(
ts.DefaultTeamAdminRole,
ts.DefaultChannelAdminRole,
ts.DefaultChannelUserRole,
ts.DefaultChannelGuestRole,
)...)
}
// ensure team admins have create_post
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.TeamAdminRoleId),
Add: []string{PermissionCreatePost},
})
// ensure channel admins have create_post
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.ChannelAdminRoleId),
Add: []string{PermissionCreatePost},
})
// conditionally add all other moderated permissions to team and channel admins
transformations = append(transformations, teamAndChannelAdminConditionalTransformations(
model.TeamAdminRoleId,
model.ChannelAdminRoleId,
model.ChannelUserRoleId,
model.ChannelGuestRoleId,
)...)
// ensure system admin has all of the moderated permissions
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: append(moderatedPermissionsMinusCreatePost, PermissionCreatePost),
})
// add the new use_channel_mentions permission to everyone who has create_post
transformations = append(transformations, permissionTransformation{
On: permissionOr(permissionExists(PermissionCreatePost), permissionExists(PermissionCreatePost_PUBLIC)),
Add: []string{PermissionUseChannelMentions},
})
return transformations, nil
}
func (a *App) getAddUseGroupMentionsPermissionMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(
isNotExactRole(model.ChannelGuestRoleId),
isNotSchemeRole(sqlstore.SchemeRoleDisplayNameChannelGuest),
permissionOr(permissionExists(PermissionCreatePost), permissionExists(PermissionCreatePost_PUBLIC)),
),
Add: []string{PermissionUseGroupMentions},
},
}, nil
}
func (a *App) getAddSystemConsolePermissionsMigration() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsToAdd := []string{}
for _, permission := range append(model.SysconsoleReadPermissions, model.SysconsoleWritePermissions...) {
permissionsToAdd = append(permissionsToAdd, permission.Id)
}
// add the new permissions to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: permissionsToAdd,
})
// add read_jobs to all roles with manage_jobs
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManageJobs),
Add: []string{PermissionReadJobs},
})
// add read_other_users_teams to all roles with edit_other_users
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionEditOtherUsers),
Add: []string{PermissionReadOtherUsersTeams},
})
// add read_public_channel_groups to all roles with manage_public_channel_members
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManagePublicChannelMembers),
Add: []string{PermissionReadPublicChannelGroups},
})
// add read_private_channel_groups to all roles with manage_private_channel_members
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManagePrivateChannelMembers),
Add: []string{PermissionReadPrivateChannelGroups},
})
// add edit_brand to all roles with manage_system
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManageSystem),
Add: []string{PermissionEditBrand},
})
return transformations, nil
}
func (a *App) getAddConvertChannelPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageTeam),
Add: []string{PermissionConvertPublicChannelToPrivate, PermissionConvertPrivateChannelToPublic},
},
}, nil
}
func (a *App) getSystemRolesPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionSysconsoleReadUserManagementSystemRoles.Id, model.PermissionSysconsoleWriteUserManagementSystemRoles.Id},
},
}, nil
}
func (a *App) getAddManageSharedChannelsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionManageSharedChannels},
},
}, nil
}
func (a *App) getBillingPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionSysconsoleReadBilling.Id, model.PermissionSysconsoleWriteBilling.Id},
},
}, nil
}
func (a *App) getAddManageSecureConnectionsPermissionsMigration() (permissionsMap, error) {
transformations := []permissionTransformation{}
// add the new permission to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionManageSecureConnections},
})
// remote the deprecated permission from system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Remove: []string{PermissionManageRemoteClusters},
})
return transformations, nil
}
func (a *App) getAddDownloadComplianceExportResult() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsToAddComplianceRead := []string{model.PermissionDownloadComplianceExportResult.Id, model.PermissionReadDataRetentionJob.Id}
permissionsToAddComplianceWrite := []string{model.PermissionManageJobs.Id}
// add the new permissions to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionDownloadComplianceExportResult.Id},
})
// add Download Compliance Export Result and Read Jobs to all roles with sysconsole_read_compliance
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadCompliance.Id),
Add: permissionsToAddComplianceRead,
})
// add manage_jobs to all roles with sysconsole_write_compliance
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteCompliance.Id),
Add: permissionsToAddComplianceWrite,
})
return transformations, nil
}
func (a *App) getAddExperimentalSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsExperimentalRead := []string{model.PermissionSysconsoleReadExperimentalBleve.Id, model.PermissionSysconsoleReadExperimentalFeatures.Id, model.PermissionSysconsoleReadExperimentalFeatureFlags.Id}
permissionsExperimentalWrite := []string{model.PermissionSysconsoleWriteExperimentalBleve.Id, model.PermissionSysconsoleWriteExperimentalFeatures.Id, model.PermissionSysconsoleWriteExperimentalFeatureFlags.Id}
// Give the new subsection READ permissions to any user with READ_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadExperimental.Id),
Add: permissionsExperimentalRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteExperimental.Id),
Add: permissionsExperimentalWrite,
})
// Give the ancillary permissions MANAGE_JOBS and PURGE_BLEVE_INDEXES to anyone with WRITE_EXPERIMENTAL_BLEVE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteExperimentalBleve.Id),
Add: []string{model.PermissionCreatePostBleveIndexesJob.Id, model.PermissionPurgeBleveIndexes.Id},
})
return transformations, nil
}
func (a *App) getAddIntegrationsSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsIntegrationsRead := []string{model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.PermissionSysconsoleReadIntegrationsBotAccounts.Id, model.PermissionSysconsoleReadIntegrationsGif.Id, model.PermissionSysconsoleReadIntegrationsCors.Id}
permissionsIntegrationsWrite := []string{model.PermissionSysconsoleWriteIntegrationsIntegrationManagement.Id, model.PermissionSysconsoleWriteIntegrationsBotAccounts.Id, model.PermissionSysconsoleWriteIntegrationsGif.Id, model.PermissionSysconsoleWriteIntegrationsCors.Id}
// Give the new subsection READ permissions to any user with READ_INTEGRATIONS
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadIntegrations.Id),
Add: permissionsIntegrationsRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteIntegrations.Id),
Add: permissionsIntegrationsWrite,
})
return transformations, nil
}
func (a *App) getAddSiteSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsSiteRead := []string{model.PermissionSysconsoleReadSiteCustomization.Id, model.PermissionSysconsoleReadSiteLocalization.Id, model.PermissionSysconsoleReadSiteUsersAndTeams.Id, model.PermissionSysconsoleReadSiteNotifications.Id, model.PermissionSysconsoleReadSiteAnnouncementBanner.Id, model.PermissionSysconsoleReadSiteEmoji.Id, model.PermissionSysconsoleReadSitePosts.Id, model.PermissionSysconsoleReadSiteFileSharingAndDownloads.Id, model.PermissionSysconsoleReadSitePublicLinks.Id, model.PermissionSysconsoleReadSiteNotices.Id}
permissionsSiteWrite := []string{model.PermissionSysconsoleWriteSiteCustomization.Id, model.PermissionSysconsoleWriteSiteLocalization.Id, model.PermissionSysconsoleWriteSiteUsersAndTeams.Id, model.PermissionSysconsoleWriteSiteNotifications.Id, model.PermissionSysconsoleWriteSiteAnnouncementBanner.Id, model.PermissionSysconsoleWriteSiteEmoji.Id, model.PermissionSysconsoleWriteSitePosts.Id, model.PermissionSysconsoleWriteSiteFileSharingAndDownloads.Id, model.PermissionSysconsoleWriteSitePublicLinks.Id, model.PermissionSysconsoleWriteSiteNotices.Id}
// Give the new subsection READ permissions to any user with READ_SITE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadSite.Id),
Add: permissionsSiteRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_SITE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteSite.Id),
Add: permissionsSiteWrite,
})
// Give the ancillary permissions EDIT_BRAND to anyone with WRITE_SITE_CUSTOMIZATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteSiteCustomization.Id),
Add: []string{model.PermissionEditBrand.Id},
})
return transformations, nil
}
func (a *App) getAddComplianceSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsComplianceRead := []string{model.PermissionSysconsoleReadComplianceDataRetentionPolicy.Id, model.PermissionSysconsoleReadComplianceComplianceExport.Id, model.PermissionSysconsoleReadComplianceComplianceMonitoring.Id, model.PermissionSysconsoleReadComplianceCustomTermsOfService.Id}
permissionsComplianceWrite := []string{model.PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id, model.PermissionSysconsoleWriteComplianceComplianceExport.Id, model.PermissionSysconsoleWriteComplianceComplianceMonitoring.Id, model.PermissionSysconsoleWriteComplianceCustomTermsOfService.Id}
// Give the new subsection READ permissions to any user with READ_COMPLIANCE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadCompliance.Id),
Add: permissionsComplianceRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_COMPLIANCE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteCompliance.Id),
Add: permissionsComplianceWrite,
})
// Ancillary permissions
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id),
Add: []string{model.PermissionCreateDataRetentionJob.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceDataRetentionPolicy.Id),
Add: []string{model.PermissionReadDataRetentionJob.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteComplianceComplianceExport.Id),
Add: []string{model.PermissionCreateComplianceExportJob.Id, model.PermissionDownloadComplianceExportResult.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceComplianceExport.Id),
Add: []string{model.PermissionReadComplianceExportJob.Id, model.PermissionDownloadComplianceExportResult.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceCustomTermsOfService.Id),
Add: []string{model.PermissionReadAudits.Id},
})
return transformations, nil
}
func (a *App) getAddEnvironmentSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsEnvironmentRead := []string{
model.PermissionSysconsoleReadEnvironmentWebServer.Id,
model.PermissionSysconsoleReadEnvironmentDatabase.Id,
model.PermissionSysconsoleReadEnvironmentElasticsearch.Id,
model.PermissionSysconsoleReadEnvironmentFileStorage.Id,
model.PermissionSysconsoleReadEnvironmentImageProxy.Id,
model.PermissionSysconsoleReadEnvironmentSMTP.Id,
model.PermissionSysconsoleReadEnvironmentPushNotificationServer.Id,
model.PermissionSysconsoleReadEnvironmentHighAvailability.Id,
model.PermissionSysconsoleReadEnvironmentRateLimiting.Id,
model.PermissionSysconsoleReadEnvironmentLogging.Id,
model.PermissionSysconsoleReadEnvironmentSessionLengths.Id,
model.PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id,
model.PermissionSysconsoleReadEnvironmentDeveloper.Id,
}
permissionsEnvironmentWrite := []string{
model.PermissionSysconsoleWriteEnvironmentWebServer.Id,
model.PermissionSysconsoleWriteEnvironmentDatabase.Id,
model.PermissionSysconsoleWriteEnvironmentElasticsearch.Id,
model.PermissionSysconsoleWriteEnvironmentFileStorage.Id,
model.PermissionSysconsoleWriteEnvironmentImageProxy.Id,
model.PermissionSysconsoleWriteEnvironmentSMTP.Id,
model.PermissionSysconsoleWriteEnvironmentPushNotificationServer.Id,
model.PermissionSysconsoleWriteEnvironmentHighAvailability.Id,
model.PermissionSysconsoleWriteEnvironmentRateLimiting.Id,
model.PermissionSysconsoleWriteEnvironmentLogging.Id,
model.PermissionSysconsoleWriteEnvironmentSessionLengths.Id,
model.PermissionSysconsoleWriteEnvironmentPerformanceMonitoring.Id,
model.PermissionSysconsoleWriteEnvironmentDeveloper.Id,
}
// Give the new subsection READ permissions to any user with READ_ENVIRONMENT
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadEnvironment.Id),
Add: permissionsEnvironmentRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_ENVIRONMENT
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironment.Id),
Add: permissionsEnvironmentWrite,
})
// Give these ancillary permissions to anyone with READ_ENVIRONMENT_ELASTICSEARCH
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadEnvironmentElasticsearch.Id),
Add: []string{
model.PermissionReadElasticsearchPostIndexingJob.Id,
model.PermissionReadElasticsearchPostAggregationJob.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_WEB_SERVER
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentWebServer.Id),
Add: []string{
model.PermissionTestSiteURL.Id,
model.PermissionReloadConfig.Id,
model.PermissionInvalidateCaches.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_DATABASE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentDatabase.Id),
Add: []string{model.PermissionRecycleDatabaseConnections.Id},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_ELASTICSEARCH
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentElasticsearch.Id),
Add: []string{
model.PermissionTestElasticsearch.Id,
model.PermissionCreateElasticsearchPostIndexingJob.Id,
model.PermissionCreateElasticsearchPostAggregationJob.Id,
model.PermissionPurgeElasticsearchIndexes.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_FILE_STORAGE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentFileStorage.Id),
Add: []string{model.PermissionTestS3.Id},
})
return transformations, nil
}
func (a *App) getAddAboutSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsAboutRead := []string{model.PermissionSysconsoleReadAboutEditionAndLicense.Id}
permissionsAboutWrite := []string{model.PermissionSysconsoleWriteAboutEditionAndLicense.Id}
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAbout.Id),
Add: permissionsAboutRead,
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAbout.Id),
Add: permissionsAboutWrite,
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAboutEditionAndLicense.Id),
Add: []string{model.PermissionReadLicenseInformation.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id),
Add: []string{model.PermissionManageLicenseInformation.Id},
})
return transformations, nil
}
func (a *App) getAddReportingSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsReportingRead := []string{
model.PermissionSysconsoleReadReportingSiteStatistics.Id,
model.PermissionSysconsoleReadReportingTeamStatistics.Id,
model.PermissionSysconsoleReadReportingServerLogs.Id,
}
permissionsReportingWrite := []string{
model.PermissionSysconsoleWriteReportingSiteStatistics.Id,
model.PermissionSysconsoleWriteReportingTeamStatistics.Id,
model.PermissionSysconsoleWriteReportingServerLogs.Id,
}
// Give the new subsection READ permissions to any user with READ_REPORTING
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadReporting.Id),
Add: permissionsReportingRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_REPORTING
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteReporting.Id),
Add: permissionsReportingWrite,
})
// Give the ancillary permissions PERMISSION_GET_ANALYTICS to anyone with PERMISSION_SYSCONSOLE_READ_USERMANAGEMENT_USERS or PERMISSION_SYSCONSOLE_READ_REPORTING_SITE_STATISTICS
transformations = append(transformations, permissionTransformation{
On: permissionOr(permissionExists(model.PermissionSysconsoleReadUserManagementUsers.Id), permissionExists(model.PermissionSysconsoleReadReportingSiteStatistics.Id)),
Add: []string{model.PermissionGetAnalytics.Id},
})
// Give the ancillary permissions PERMISSION_GET_LOGS to anyone with PERMISSION_SYSCONSOLE_READ_REPORTING_SERVER_LOGS
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadReportingServerLogs.Id),
Add: []string{model.PermissionGetLogs.Id},
})
return transformations, nil
}
func (a *App) getAddAuthenticationSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsAuthenticationRead := []string{model.PermissionSysconsoleReadAuthenticationSignup.Id, model.PermissionSysconsoleReadAuthenticationEmail.Id, model.PermissionSysconsoleReadAuthenticationPassword.Id, model.PermissionSysconsoleReadAuthenticationMfa.Id, model.PermissionSysconsoleReadAuthenticationLdap.Id, model.PermissionSysconsoleReadAuthenticationSaml.Id, model.PermissionSysconsoleReadAuthenticationOpenid.Id, model.PermissionSysconsoleReadAuthenticationGuestAccess.Id}
permissionsAuthenticationWrite := []string{model.PermissionSysconsoleWriteAuthenticationSignup.Id, model.PermissionSysconsoleWriteAuthenticationEmail.Id, model.PermissionSysconsoleWriteAuthenticationPassword.Id, model.PermissionSysconsoleWriteAuthenticationMfa.Id, model.PermissionSysconsoleWriteAuthenticationLdap.Id, model.PermissionSysconsoleWriteAuthenticationSaml.Id, model.PermissionSysconsoleWriteAuthenticationOpenid.Id, model.PermissionSysconsoleWriteAuthenticationGuestAccess.Id}
// Give the new subsection READ permissions to any user with READ_AUTHENTICATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAuthentication.Id),
Add: permissionsAuthenticationRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_AUTHENTICATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthentication.Id),
Add: permissionsAuthenticationWrite,
})
// Give the ancillary permissions for LDAP to anyone with WRITE_AUTHENTICATION_LDAP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationLdap.Id),
Add: []string{model.PermissionCreateLdapSyncJob.Id, model.PermissionTestLdap.Id, model.PermissionAddLdapPublicCert.Id, model.PermissionAddLdapPrivateCert.Id, model.PermissionRemoveLdapPublicCert.Id, model.PermissionRemoveLdapPrivateCert.Id},
})
// Give the ancillary permissions PERMISSION_TEST_LDAP to anyone with READ_AUTHENTICATION_LDAP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAuthenticationLdap.Id),
Add: []string{model.PermissionReadLdapSyncJob.Id},
})
// Give the ancillary permissions PERMISSION_INVALIDATE_EMAIL_INVITE to anyone with WRITE_AUTHENTICATION_EMAIL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationEmail.Id),
Add: []string{model.PermissionInvalidateEmailInvite.Id},
})
// Give the ancillary permissions for SAML to anyone with WRITE_AUTHENTICATION_SAML
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationSaml.Id),
Add: []string{model.PermissionGetSamlMetadataFromIdp.Id, model.PermissionAddSamlPublicCert.Id, model.PermissionAddSamlPrivateCert.Id, model.PermissionAddSamlIdpCert.Id, model.PermissionRemoveSamlPublicCert.Id, model.PermissionRemoveSamlPrivateCert.Id, model.PermissionRemoveSamlIdpCert.Id, model.PermissionGetSamlCertStatus.Id},
})
return transformations, nil
}
// This migration fixes https://github.com/mattermost/mattermost-server/issues/17642 where this particular ancillary permission was forgotten during the initial migrations
func (a *App) getAddTestEmailAncillaryPermission() (permissionsMap, error) {
transformations := []permissionTransformation{}
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_SMTP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentSMTP.Id),
Add: []string{model.PermissionTestEmail.Id},
})
return transformations, nil
}
func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) {
t := []permissionTransformation{}
customGroupPermissions := []string{
model.PermissionCreateCustomGroup.Id,
model.PermissionManageCustomGroupMembers.Id,
model.PermissionEditCustomGroup.Id,
model.PermissionDeleteCustomGroup.Id,
}
t = append(t, permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: customGroupPermissions,
})
return t, nil
}
func (a *App) getAddCustomUserGroupsPermissionRestore() (permissionsMap, error) {
t := []permissionTransformation{}
customGroupPermissions := []string{
model.PermissionRestoreCustomGroup.Id,
}
t = append(t, permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemCustomGroupAdminRoleId),
Add: customGroupPermissions,
})
return t, nil
}
func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
transformations = append(transformations, permissionTransformation{
On: permissionOr(
permissionExists(model.PermissionCreatePublicChannel.Id),
permissionExists(model.PermissionCreatePrivateChannel.Id),
),
Add: []string{
model.PermissionPublicPlaybookCreate.Id,
model.PermissionPrivatePlaybookCreate.Id,
},
})
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{
model.PermissionPublicPlaybookManageProperties.Id,
model.PermissionPublicPlaybookManageMembers.Id,
model.PermissionPublicPlaybookView.Id,
model.PermissionPublicPlaybookMakePrivate.Id,
model.PermissionPrivatePlaybookManageProperties.Id,
model.PermissionPrivatePlaybookManageMembers.Id,
model.PermissionPrivatePlaybookView.Id,
model.PermissionPrivatePlaybookMakePublic.Id,
model.PermissionRunCreate.Id,
model.PermissionRunManageProperties.Id,
model.PermissionRunManageMembers.Id,
model.PermissionRunView.Id,
},
})
return transformations, nil
}
func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) {
transformations := []permissionTransformation{}
transformations = append(transformations, permissionTransformation{
On: permissionOr(
isExactRole(model.PlaybookAdminRoleId),
isExactRole(model.TeamAdminRoleId),
isExactRole(model.SystemAdminRoleId),
),
Add: []string{
model.PermissionPublicPlaybookManageRoles.Id,
model.PermissionPrivatePlaybookManageRoles.Id,
},
})
return transformations, nil
}
func (a *App) getProductsBoardsPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsProductsRead := []string{model.PermissionSysconsoleReadProductsBoards.Id}
permissionsProductsWrite := []string{model.PermissionSysconsoleWriteProductsBoards.Id}
// Give the new subsection READ permissions to any user with SYSTEM_MANAGER
transformations = append(transformations, permissionTransformation{
On: permissionOr(isExactRole(model.SystemManagerRoleId)),
Add: permissionsProductsRead,
})
// Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN
transformations = append(transformations, permissionTransformation{
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
Add: permissionsProductsWrite,
})
return transformations, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
}
func (s *Server) doPermissionsMigrations() error {
a := New(ServerConnector(s.Channels()))
PermissionsMigrations := []struct {
Key string
Migration func() (permissionsMap, error)
}{
{Key: model.MigrationKeyEmojiPermissionsSplit, Migration: a.getEmojisPermissionsSplitMigration},
{Key: model.MigrationKeyWebhookPermissionsSplit, Migration: a.getWebhooksPermissionsSplitMigration},
{Key: model.MigrationKeyListJoinPublicPrivateTeams, Migration: a.getListJoinPublicPrivateTeamsPermissionsMigration},
{Key: model.MigrationKeyRemovePermanentDeleteUser, Migration: a.removePermanentDeleteUserMigration},
{Key: model.MigrationKeyAddBotPermissions, Migration: a.getAddBotPermissionsMigration},
{Key: model.MigrationKeyApplyChannelManageDeleteToChannelUser, Migration: a.applyChannelManageDeleteToChannelUser},
{Key: model.MigrationKeyRemoveChannelManageDeleteFromTeamUser, Migration: a.removeChannelManageDeleteFromTeamUser},
{Key: model.MigrationKeyViewMembersNewPermission, Migration: a.getViewMembersPermissionMigration},
{Key: model.MigrationKeyAddManageGuestsPermissions, Migration: a.getAddManageGuestsPermissionsMigration},
{Key: model.MigrationKeyChannelModerationsPermissions, Migration: a.channelModerationPermissionsMigration},
{Key: model.MigrationKeyAddUseGroupMentionsPermission, Migration: a.getAddUseGroupMentionsPermissionMigration},
{Key: model.MigrationKeyAddSystemConsolePermissions, Migration: a.getAddSystemConsolePermissionsMigration},
{Key: model.MigrationKeyAddConvertChannelPermissions, Migration: a.getAddConvertChannelPermissionsMigration},
{Key: model.MigrationKeyAddManageSharedChannelPermissions, Migration: a.getAddManageSharedChannelsPermissionsMigration},
{Key: model.MigrationKeyAddManageSecureConnectionsPermissions, Migration: a.getAddManageSecureConnectionsPermissionsMigration},
{Key: model.MigrationKeyAddSystemRolesPermissions, Migration: a.getSystemRolesPermissionsMigration},
{Key: model.MigrationKeyAddBillingPermissions, Migration: a.getBillingPermissionsMigration},
{Key: model.MigrationKeyAddDownloadComplianceExportResults, Migration: a.getAddDownloadComplianceExportResult},
{Key: model.MigrationKeyAddExperimentalSubsectionPermissions, Migration: a.getAddExperimentalSubsectionPermissions},
{Key: model.MigrationKeyAddAuthenticationSubsectionPermissions, Migration: a.getAddAuthenticationSubsectionPermissions},
{Key: model.MigrationKeyAddIntegrationsSubsectionPermissions, Migration: a.getAddIntegrationsSubsectionPermissions},
{Key: model.MigrationKeyAddSiteSubsectionPermissions, Migration: a.getAddSiteSubsectionPermissions},
{Key: model.MigrationKeyAddComplianceSubsectionPermissions, Migration: a.getAddComplianceSubsectionPermissions},
{Key: model.MigrationKeyAddEnvironmentSubsectionPermissions, Migration: a.getAddEnvironmentSubsectionPermissions},
{Key: model.MigrationKeyAddAboutSubsectionPermissions, Migration: a.getAddAboutSubsectionPermissions},
{Key: model.MigrationKeyAddReportingSubsectionPermissions, Migration: a.getAddReportingSubsectionPermissions},
{Key: model.MigrationKeyAddTestEmailAncillaryPermission, Migration: a.getAddTestEmailAncillaryPermission},
{Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions},
{Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions},
{Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles},
{Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions},
{Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore},
}
roles, err := s.Store().Role().GetAll()
if err != nil {
return err
}
for _, migration := range PermissionsMigrations {
migMap, err := migration.Migration()
if err != nil {
return err
}
if err := s.doPermissionsMigration(migration.Key, migMap, roles); err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
const (
TimestampFormat = "Mon Jan 2 15:04:05 -0700 MST 2006"
)
// Busy represents the busy state of the server. A server marked busy
// will have non-critical services disabled. If a Cluster is provided
// any changes will be propagated to each node.
type Busy struct {
busy int32 // protected via atomic for fast IsBusy calls
mux sync.RWMutex
timer *time.Timer
expires time.Time
cluster einterfaces.ClusterInterface
}
// NewBusy creates a new Busy instance with optional cluster which will
// be notified of busy state changes.
func NewBusy(cluster einterfaces.ClusterInterface) *Busy {
return &Busy{cluster: cluster}
}
// IsBusy returns true if the server has been marked as busy.
func (b *Busy) IsBusy() bool {
if b == nil {
return false
}
return atomic.LoadInt32(&b.busy) != 0
}
// Set marks the server as busy for dur duration and notifies cluster nodes.
func (b *Busy) Set(dur time.Duration) {
b.mux.Lock()
defer b.mux.Unlock()
// minimum 1 second
if dur < (time.Second * 1) {
dur = time.Second * 1
}
b.setWithoutNotify(dur)
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: true, Expires: b.expires.Unix(), ExpiresTS: b.expires.UTC().Format(TimestampFormat)}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) setWithoutNotify(dur time.Duration) {
b.clearWithoutNotify()
atomic.StoreInt32(&b.busy, 1)
b.expires = time.Now().Add(dur)
b.timer = time.AfterFunc(dur, func() {
b.mux.Lock()
b.clearWithoutNotify()
b.mux.Unlock()
})
}
// ClearBusy marks the server as not busy and notifies cluster nodes.
func (b *Busy) Clear() {
b.mux.Lock()
defer b.mux.Unlock()
b.clearWithoutNotify()
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: false, Expires: time.Time{}.Unix(), ExpiresTS: ""}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) clearWithoutNotify() {
if b.timer != nil {
b.timer.Stop() // don't drain timer.C channel for AfterFunc timers.
}
b.timer = nil
b.expires = time.Time{}
atomic.StoreInt32(&b.busy, 0)
}
// Expires returns the expected time that the server
// will be marked not busy. This expiry can be extended
// via additional calls to SetBusy.
func (b *Busy) Expires() time.Time {
b.mux.RLock()
defer b.mux.RUnlock()
return b.expires
}
// notifyServerBusyChange informs all cluster members of a server busy state change.
func (b *Busy) notifyServerBusyChange(sbs *model.ServerBusyState) {
if b.cluster == nil {
return
}
buf, _ := json.Marshal(sbs)
msg := &model.ClusterMessage{
Event: model.ClusterEventBusyStateChanged,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
b.cluster.SendClusterMessage(msg)
}
// ClusterEventChanged is called when a CLUSTER_EVENT_BUSY_STATE_CHANGED is received.
func (b *Busy) ClusterEventChanged(sbs *model.ServerBusyState) {
b.mux.Lock()
defer b.mux.Unlock()
if sbs.Busy {
expires := time.Unix(sbs.Expires, 0)
dur := time.Until(expires)
if dur > 0 {
b.setWithoutNotify(dur)
}
} else {
b.clearWithoutNotify()
}
}
func (b *Busy) ToJSON() ([]byte, error) {
b.mux.RLock()
defer b.mux.RUnlock()
sbs := &model.ServerBusyState{
Busy: atomic.LoadInt32(&b.busy) != 0,
Expires: b.expires.Unix(),
ExpiresTS: b.expires.UTC().Format(TimestampFormat),
}
sbsJSON, jsonErr := json.Marshal(sbs)
if jsonErr != nil {
return []byte{}, fmt.Errorf("failed to encode server busy state to JSON: %w", jsonErr)
}
return sbsJSON, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ensure cluster service wrapper implements `product.ClusterService`
var _ product.ClusterService = (*PlatformService)(nil)
// Ensure KV store wrapper implements `product.KVStoreService`
var _ product.KVStoreService = (*PlatformService)(nil)
func (ps *PlatformService) Cluster() einterfaces.ClusterInterface {
return ps.clusterIFace
}
func (ps *PlatformService) NewClusterDiscoveryService() *ClusterDiscoveryService {
ds := &ClusterDiscoveryService{
ClusterDiscovery: model.ClusterDiscovery{},
platform: ps,
stop: make(chan bool),
}
return ds
}
func (ps *PlatformService) IsLeader() bool {
if ps.License() != nil && *ps.Config().ClusterSettings.Enable && ps.clusterIFace != nil {
return ps.clusterIFace.IsLeader()
}
return true
}
func (ps *PlatformService) SetCluster(impl einterfaces.ClusterInterface) { //nolint:unused
ps.clusterIFace = impl
}
func (ps *PlatformService) PublishPluginClusterEvent(productID string, ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error {
if ps.clusterIFace == nil {
return nil
}
msg := &model.ClusterMessage{
Event: model.ClusterEventPluginEvent,
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"ProductID": productID,
"EventID": ev.Id,
},
Data: ev.Data,
}
// If TargetId is empty we broadcast to all other cluster nodes.
if opts.TargetId == "" {
ps.clusterIFace.SendClusterMessage(msg)
} else {
if err := ps.clusterIFace.SendClusterMessageToNode(opts.TargetId, msg); err != nil {
return fmt.Errorf("failed to send message to cluster node %q: %w", opts.TargetId, err)
}
}
return nil
}
func (ps *PlatformService) PublishWebSocketEvent(productID string, event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
ev := model.NewWebSocketEvent(fmt.Sprintf("custom_%v_%v", productID, event), "", "", "", nil, "")
ev = ev.SetBroadcast(broadcast).SetData(payload)
ps.Publish(ev)
}
func (ps *PlatformService) SetPluginKeyWithOptions(productID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
if err := options.IsValid(); err != nil {
mlog.Debug("Failed to set plugin key value with options", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return false, err
}
updated, err := ps.Store.Plugin().SetWithOptions(productID, key, value, options)
if err != nil {
mlog.Error("Failed to set plugin key value with options", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return false, appErr
default:
return false, model.NewAppError("SetPluginKeyWithOptions", "app.plugin_store.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Clean up a previous entry using the hashed key, if it exists.
if err := ps.Store.Plugin().Delete(productID, getKeyHash(key)); err != nil {
mlog.Warn("Failed to clean up previously hashed plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
}
return updated, nil
}
func (ps *PlatformService) KVGet(productID, key string) ([]byte, *model.AppError) {
if kv, err := ps.Store.Plugin().Get(productID, key); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Lookup using the hashed version of the key for keys written prior to v5.6.
if kv, err := ps.Store.Plugin().Get(productID, getKeyHash(key)); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value using hashed key", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, nil
}
func (ps *PlatformService) KVDelete(productID, key string) *model.AppError {
if err := ps.Store.Plugin().Delete(productID, getKeyHash(key)); err != nil {
ps.logger.Error("Failed to delete plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Also delete the key without hashing
if err := ps.Store.Plugin().Delete(productID, key); err != nil {
ps.logger.Error("Failed to delete plugin key value using hashed key", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (ps *PlatformService) KVList(productID string, page, perPage int) ([]string, *model.AppError) {
data, err := ps.Store.Plugin().List(productID, page*perPage, perPage)
if err != nil {
ps.logger.Error("Failed to list plugin key values", mlog.Int("page", page), mlog.Int("perPage", perPage), mlog.Err(err))
return nil, model.NewAppError("ListPluginKeys", "app.plugin_store.list.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
// Registers a given function to be called when the cluster leader may have changed. Returns a unique ID for the
// listener which can later be used to remove it. If clustering is not enabled in this build, the callback will never
// be called.
func (ps *PlatformService) AddClusterLeaderChangedListener(listener func()) string {
id := model.NewId()
ps.clusterLeaderListeners.Store(id, listener)
return id
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (ps *PlatformService) RemoveClusterLeaderChangedListener(id string) {
ps.clusterLeaderListeners.Delete(id)
}
func (ps *PlatformService) InvokeClusterLeaderChangedListeners() {
ps.logger.Info("Cluster leader changed. Invoking ClusterLeaderChanged listeners.")
// This needs to be run in a separate goroutine otherwise a recursive lock happens
// because the listener function eventually ends up calling .IsLeader().
// Fixing this would require the changed event to pass the leader directly, but that
// requires a lot of work.
ps.Go(func() {
ps.clusterLeaderListeners.Range(func(_, listener any) bool {
listener.(func())()
return true
})
})
}
func (ps *PlatformService) Publish(message *model.WebSocketEvent) {
if ps.metricsIFace != nil {
ps.metricsIFace.IncrementWebsocketEvent(message.EventType())
}
ps.PublishSkipClusterSend(message)
if ps.clusterIFace != nil {
data, err := message.ToJSON()
if err != nil {
mlog.Warn("Failed to encode message to JSON", mlog.Err(err))
}
cm := &model.ClusterMessage{
Event: model.ClusterEventPublish,
SendType: model.ClusterSendBestEffort,
Data: data,
}
if message.EventType() == model.WebsocketEventPosted ||
message.EventType() == model.WebsocketEventPostEdited ||
message.EventType() == model.WebsocketEventDirectAdded ||
message.EventType() == model.WebsocketEventGroupAdded ||
message.EventType() == model.WebsocketEventAddedToTeam ||
message.GetBroadcast().ReliableClusterSend {
cm.SendType = model.ClusterSendReliable
}
ps.clusterIFace.SendClusterMessage(cm)
}
}
func (ps *PlatformService) PublishSkipClusterSend(event *model.WebSocketEvent) {
if event.GetBroadcast().UserId != "" {
hub := ps.GetHubForUserId(event.GetBroadcast().UserId)
if hub != nil {
hub.Broadcast(event)
}
} else {
for _, hub := range ps.hubs {
hub.Broadcast(event)
}
}
// Notify shared channel sync service
ps.SharedChannelSyncHandler(event)
}
func (ps *PlatformService) ListPluginKeys(pluginID string, page, perPage int) ([]string, *model.AppError) {
data, err := ps.Store.Plugin().List(pluginID, page*perPage, perPage)
if err != nil {
mlog.Error("Failed to list plugin key values", mlog.Int("page", page), mlog.Int("perPage", perPage), mlog.Err(err))
return nil, model.NewAppError("ListPluginKeys", "app.plugin_store.list.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
func (ps *PlatformService) DeletePluginKey(pluginID string, key string) *model.AppError {
if err := ps.Store.Plugin().Delete(pluginID, getKeyHash(key)); err != nil {
mlog.Error("Failed to delete plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Also delete the key without hashing
if err := ps.Store.Plugin().Delete(pluginID, key); err != nil {
mlog.Error("Failed to delete plugin key value using hashed key", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DiscoveryServiceWritePing = 60 * time.Second
)
type ClusterDiscoveryService struct {
model.ClusterDiscovery
platform *PlatformService
stop chan bool
}
func (cds *ClusterDiscoveryService) Start() {
err := cds.platform.Store.ClusterDiscovery().Cleanup()
if err != nil {
mlog.Warn("ClusterDiscoveryService failed to cleanup the outdated cluster discovery information", mlog.Err(err))
}
exists, err := cds.platform.Store.ClusterDiscovery().Exists(&cds.ClusterDiscovery)
if err != nil {
mlog.Warn("ClusterDiscoveryService failed to check if row exists", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
} else if exists {
if _, err := cds.platform.Store.ClusterDiscovery().Delete(&cds.ClusterDiscovery); err != nil {
mlog.Warn("ClusterDiscoveryService failed to start clean", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
}
if err := cds.platform.Store.ClusterDiscovery().Save(&cds.ClusterDiscovery); err != nil {
mlog.Error("ClusterDiscoveryService failed to save", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
return
}
go func() {
mlog.Debug("ClusterDiscoveryService ping writer started", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id))
ticker := time.NewTicker(DiscoveryServiceWritePing)
defer func() {
ticker.Stop()
if _, err := cds.platform.Store.ClusterDiscovery().Delete(&cds.ClusterDiscovery); err != nil {
mlog.Warn("ClusterDiscoveryService failed to cleanup", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
mlog.Debug("ClusterDiscoveryService ping writer stopped", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id))
}()
for {
select {
case <-ticker.C:
if err := cds.platform.Store.ClusterDiscovery().SetLastPingAt(&cds.ClusterDiscovery); err != nil {
mlog.Error("ClusterDiscoveryService failed to write ping", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
case <-cds.stop:
return
}
}
}()
}
func (cds *ClusterDiscoveryService) Stop() {
cds.stop <- true
}
func (ps *PlatformService) GetClusterId() string {
if ps.Cluster() == nil {
return ""
}
return ps.Cluster().GetClusterId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"fmt"
"runtime/debug"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) RegisterClusterHandlers() {
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventPublish, ps.ClusterPublishHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventUpdateStatus, ps.ClusterUpdateStatusHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateAllCaches, ps.ClusterInvalidateAllCachesHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMembersNotifyProps, ps.clusterInvalidateCacheForChannelMembersNotifyPropHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelByName, ps.clusterInvalidateCacheForChannelByNameHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUser, ps.clusterInvalidateCacheForUserHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUserTeams, ps.clusterInvalidateCacheForUserTeamsHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventBusyStateChanged, ps.clusterBusyStateChgHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventClearSessionCacheForUser, ps.clusterClearSessionCacheForUserHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventClearSessionCacheForAllUsers, ps.clusterClearSessionCacheForAllUsersHandler)
for e, h := range ps.additionalClusterHandlers {
ps.clusterIFace.RegisterClusterMessageHandler(e, h)
}
}
func (ps *PlatformService) RegisterClusterMessageHandler(ev model.ClusterEvent, h einterfaces.ClusterMessageHandler) {
ps.additionalClusterHandlers[ev] = h
}
// ClusterHandlersPreCheck checks whether the platform service is ready to handle cluster messages.
func (ps *PlatformService) ClusterHandlersPreCheck() error {
if ps.Store == nil {
return fmt.Errorf("could not find store")
}
if ps.statusCache == nil {
return fmt.Errorf("could not find status cache")
}
return nil
}
func (ps *PlatformService) ClusterPublishHandler(msg *model.ClusterMessage) {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
if err != nil {
ps.logger.Warn("Failed to decode event from JSON", mlog.Err(err))
return
}
ps.PublishSkipClusterSend(event)
}
func (ps *PlatformService) ClusterUpdateStatusHandler(msg *model.ClusterMessage) {
var status model.Status
if jsonErr := json.Unmarshal(msg.Data, &status); jsonErr != nil {
ps.logger.Warn("Failed to decode status from JSON")
}
ps.statusCache.Set(status.UserId, status)
}
func (ps *PlatformService) ClusterInvalidateAllCachesHandler(msg *model.ClusterMessage) {
ps.InvalidateAllCachesSkipSend()
}
func (ps *PlatformService) clusterInvalidateCacheForChannelMembersNotifyPropHandler(msg *model.ClusterMessage) {
ps.invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterInvalidateCacheForChannelByNameHandler(msg *model.ClusterMessage) {
ps.invalidateCacheForChannelByNameSkipClusterSend(msg.Props["id"], msg.Props["name"])
}
func (ps *PlatformService) clusterInvalidateCacheForUserHandler(msg *model.ClusterMessage) {
ps.InvalidateCacheForUserSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterInvalidateCacheForUserTeamsHandler(msg *model.ClusterMessage) {
ps.invalidateWebConnSessionCacheForUser(string(msg.Data))
}
func (ps *PlatformService) ClearSessionCacheForUserSkipClusterSend(userID string) {
ps.ClearUserSessionCacheLocal(userID)
ps.invalidateWebConnSessionCacheForUser(userID)
}
func (ps *PlatformService) ClearSessionCacheForAllUsersSkipClusterSend() {
ps.logger.Info("Purging sessions cache")
ps.ClearAllUsersSessionCacheLocal()
}
func (ps *PlatformService) clusterClearSessionCacheForUserHandler(msg *model.ClusterMessage) {
ps.ClearSessionCacheForUserSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterClearSessionCacheForAllUsersHandler(msg *model.ClusterMessage) {
ps.ClearSessionCacheForAllUsersSkipClusterSend()
}
func (ps *PlatformService) clusterBusyStateChgHandler(msg *model.ClusterMessage) {
var sbs model.ServerBusyState
if jsonErr := json.Unmarshal(msg.Data, &sbs); jsonErr != nil {
mlog.Warn("Failed to decode server busy state from JSON", mlog.Err(jsonErr))
}
ps.Busy.ClusterEventChanged(&sbs)
if sbs.Busy {
ps.logger.Warn("server busy state activated via cluster event - non-critical services disabled", mlog.Int64("expires_sec", sbs.Expires))
} else {
ps.logger.Info("server busy state cleared via cluster event - non-critical services enabled")
}
}
func (ps *PlatformService) invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelID string) {
ps.Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (ps *PlatformService) invalidateCacheForChannelByNameSkipClusterSend(teamID, name string) {
if teamID == "" {
teamID = "dm"
}
ps.Store.Channel().InvalidateChannelByName(teamID, name)
}
func (ps *PlatformService) InvalidateCacheForUserSkipClusterSend(userID string) {
ps.Store.Channel().InvalidateAllChannelMembersForUser(userID)
ps.invalidateWebConnSessionCacheForUser(userID)
}
func (ps *PlatformService) invalidateWebConnSessionCacheForUser(userID string) {
hub := ps.GetHubForUserId(userID)
if hub != nil {
hub.InvalidateUser(userID)
}
}
func (ps *PlatformService) InvalidateAllCachesSkipSend() {
ps.logger.Info("Purging all caches")
ps.ClearAllUsersSessionCacheLocal()
ps.statusCache.Purge()
ps.Store.Team().ClearCaches()
ps.Store.Channel().ClearCaches()
ps.Store.User().ClearCaches()
ps.Store.Post().ClearCaches()
ps.Store.FileInfo().ClearCaches()
ps.Store.Webhook().ClearCaches()
linkCache.Purge()
ps.LoadLicense()
}
func (ps *PlatformService) InvalidateAllCaches() *model.AppError {
debug.FreeOSMemory()
ps.InvalidateAllCachesSkipSend()
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateAllCaches,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
}
ps.clusterIFace.SendClusterMessage(msg)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ServiceConfig is used to initialize the PlatformService.
// The mandatory fields will be checked during the initialization of the service.
type ServiceConfig struct {
// Mandatory fields
ConfigStore *config.Store
Store store.Store
// Optional fields
Cluster einterfaces.ClusterInterface
}
// ensure the config wrapper implements `product.ConfigService`
var _ product.ConfigService = (*PlatformService)(nil)
func (ps *PlatformService) Config() *model.Config {
return ps.configStore.Get()
}
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
// for the listener that can later be used to remove it.
func (ps *PlatformService) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return ps.configStore.AddListener(listener)
}
func (ps *PlatformService) RemoveConfigListener(id string) {
ps.configStore.RemoveListener(id)
}
func (ps *PlatformService) UpdateConfig(f func(*model.Config)) {
if ps.configStore.IsReadOnly() {
return
}
old := ps.Config()
updated := old.Clone()
f(updated)
if _, _, err := ps.configStore.Set(updated); err != nil {
ps.logger.Error("Failed to update config", mlog.Err(err))
}
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
// It returns both the previous and current configs.
func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
oldCfg, newCfg, err := ps.configStore.Set(newCfg)
if errors.Is(err, config.ErrReadOnlyConfiguration) {
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err)
} else if err != nil {
return nil, nil, model.NewAppError("saveConfig", "app.save_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if ps.clusterIFace != nil {
err := ps.clusterIFace.ConfigChanged(ps.configStore.RemoveEnvironmentOverrides(oldCfg),
ps.configStore.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage)
if err != nil {
return nil, nil, err
}
}
return oldCfg, newCfg, nil
}
func (ps *PlatformService) ReloadConfig() error {
if err := ps.configStore.Load(); err != nil {
return err
}
return nil
}
func (ps *PlatformService) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]interface{} {
return ps.configStore.GetEnvironmentOverridesWithFilter(filter)
}
func (ps *PlatformService) GetEnvironmentOverrides() map[string]interface{} {
return ps.configStore.GetEnvironmentOverrides()
}
func (ps *PlatformService) DescribeConfig() string {
return ps.configStore.String()
}
func (ps *PlatformService) CleanUpConfig() error {
return ps.configStore.CleanUp()
}
// ConfigureLogger applies the specified configuration to a logger.
func (ps *PlatformService) ConfigureLogger(name string, logger *mlog.Logger, logSettings *model.LogSettings, getPath func(string) string) error {
// Advanced logging is E20 only, however logging must be initialized before the license
// file is loaded. If no valid E20 license exists then advanced logging will be
// shutdown once license is loaded/checked.
var err error
dsn := *logSettings.AdvancedLoggingConfig
var logConfigSrc config.LogConfigSrc
if dsn != "" {
logConfigSrc, err = config.NewLogConfigSrc(dsn, ps.configStore)
if err != nil {
return fmt.Errorf("invalid config source for %s, %w", name, err)
}
ps.logger.Info("Loaded configuration for "+name, mlog.String("source", dsn))
}
cfg, err := config.MloggerConfigFromLoggerConfig(logSettings, logConfigSrc, getPath)
if err != nil {
return fmt.Errorf("invalid config source for %s, %w", name, err)
}
if err := logger.ConfigureTargets(cfg, nil); err != nil {
return fmt.Errorf("invalid config for %s, %w", name, err)
}
return nil
}
func (ps *PlatformService) GetConfigStore() *config.Store {
return ps.configStore
}
func (ps *PlatformService) GetConfigFile(name string) ([]byte, error) {
return ps.configStore.GetFile(name)
}
func (ps *PlatformService) SetConfigFile(name string, data []byte) error {
return ps.configStore.SetFile(name, data)
}
func (ps *PlatformService) RemoveConfigFile(name string) error {
return ps.configStore.RemoveFile(name)
}
func (ps *PlatformService) HasConfigFile(name string) (bool, error) {
return ps.configStore.HasFile(name)
}
func (ps *PlatformService) SetConfigReadOnlyFF(readOnly bool) {
ps.configStore.SetReadOnlyFF(readOnly)
}
func (ps *PlatformService) ClientConfigHash() string {
return ps.clientConfigHash.Load().(string)
}
func (ps *PlatformService) regenerateClientConfig() {
clientConfig := config.GenerateClientConfig(ps.Config(), ps.telemetryId, ps.License())
limitedClientConfig := config.GenerateLimitedClientConfig(ps.Config(), ps.telemetryId, ps.License())
if clientConfig["EnableCustomTermsOfService"] == "true" {
termsOfService, err := ps.Store.TermsOfService().GetLatest(true)
if err != nil {
mlog.Err(err)
} else {
clientConfig["CustomTermsOfServiceId"] = termsOfService.Id
limitedClientConfig["CustomTermsOfServiceId"] = termsOfService.Id
}
}
if key := ps.AsymmetricSigningKey(); key != nil {
der, _ := x509.MarshalPKIXPublicKey(&key.PublicKey)
clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
limitedClientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
}
clientConfigJSON, _ := json.Marshal(clientConfig)
ps.clientConfig.Store(clientConfig)
ps.limitedClientConfig.Store(limitedClientConfig)
ps.clientConfigHash.Store(fmt.Sprintf("%x", md5.Sum(clientConfigJSON)))
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (ps *PlatformService) AsymmetricSigningKey() *ecdsa.PrivateKey {
if key := ps.asymmetricSigningKey.Load(); key != nil {
return key.(*ecdsa.PrivateKey)
}
return nil
}
// EnsureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to
// AsymmetricSigningKey will always return a valid signing key.
func (ps *PlatformService) EnsureAsymmetricSigningKey() error {
if ps.AsymmetricSigningKey() != nil {
return nil
}
var key *model.SystemAsymmetricSigningKey
value, err := ps.Store.System().GetByName(model.SystemAsymmetricSigningKeyKey)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if key == nil {
newECDSAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
newKey := &model.SystemAsymmetricSigningKey{
ECDSAKey: &model.SystemECDSAKey{
Curve: "P-256",
X: newECDSAKey.X,
Y: newECDSAKey.Y,
D: newECDSAKey.D,
},
}
system := &model.System{
Name: model.SystemAsymmetricSigningKeyKey,
}
v, err := json.Marshal(newKey)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = ps.Store.System().Save(system); err != nil {
mlog.Warn("Failed to save AsymmetricSigningKey", mlog.Err(err))
} else {
key = newKey
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if key == nil {
value, err := ps.Store.System().GetByName(model.SystemAsymmetricSigningKeyKey)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
var curve elliptic.Curve
switch key.ECDSAKey.Curve {
case "P-256":
curve = elliptic.P256()
default:
return fmt.Errorf("unknown curve: " + key.ECDSAKey.Curve)
}
ps.asymmetricSigningKey.Store(&ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: key.ECDSAKey.X,
Y: key.ECDSAKey.Y,
},
D: key.ECDSAKey.D,
})
ps.regenerateClientConfig()
return nil
}
// LimitedClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (ps *PlatformService) LimitedClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range ps.LimitedClientConfig() {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(ps.IsFirstUserAccount())
return respCfg
}
// ClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (ps *PlatformService) ClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range ps.clientConfig.Load().(map[string]string) {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(ps.IsFirstUserAccount())
respCfg["MaxPostSize"] = strconv.Itoa(ps.MaxPostSize())
respCfg["UpgradedFromTE"] = strconv.FormatBool(ps.isUpgradedFromTE())
respCfg["InstallationDate"] = ""
if installationDate, err := ps.GetSystemInstallDate(); err == nil {
respCfg["InstallationDate"] = strconv.FormatInt(installationDate, 10)
}
if ver, err := ps.Store.GetDBSchemaVersion(); err != nil {
mlog.Error("Could not get the schema version", mlog.Err(err))
} else {
respCfg["SchemaVersion"] = strconv.Itoa(ver)
}
return respCfg
}
func (ps *PlatformService) LimitedClientConfig() map[string]string {
return ps.limitedClientConfig.Load().(map[string]string)
}
func (ps *PlatformService) IsFirstUserAccount() bool {
count, err := ps.Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
return false
}
return count <= 0
}
func (ps *PlatformService) MaxPostSize() int {
maxPostSize := ps.Store.Post().GetMaxPostSize()
if maxPostSize == 0 {
return model.PostMessageMaxRunesV1
}
return maxPostSize
}
func (ps *PlatformService) isUpgradedFromTE() bool {
val, err := ps.Store.System().GetByName(model.SystemUpgradedFromTeId)
if err != nil {
return false
}
return val.Value == "true"
}
func (ps *PlatformService) GetSystemInstallDate() (int64, *model.AppError) {
systemData, err := ps.Store.System().GetByName(model.SystemInstallationDateKey)
if err != nil {
return 0, model.NewAppError("getSystemInstallDate", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
value, err := strconv.ParseInt(systemData.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("getSystemInstallDate", "app.system_install_date.parse_int.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return value, nil
}
func (ps *PlatformService) ClientConfig() map[string]string {
return ps.clientConfig.Load().(map[string]string)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
var clusterInterface func(*PlatformService) einterfaces.ClusterInterface
func RegisterClusterInterface(f func(*PlatformService) einterfaces.ClusterInterface) {
clusterInterface = f
}
var elasticsearchInterface func(*PlatformService) searchengine.SearchEngineInterface
func RegisterElasticsearchInterface(f func(*PlatformService) searchengine.SearchEngineInterface) {
elasticsearchInterface = f
}
var licenseInterface func(*PlatformService) einterfaces.LicenseInterface
func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterface) {
licenseInterface = f
}
var metricsInterfaceFn func(*PlatformService, string, string) einterfaces.MetricsInterface
func RegisterMetricsInterface(f func(*PlatformService, string, string) einterfaces.MetricsInterface) {
metricsInterfaceFn = f
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"os"
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/app/featureflag"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// SetupFeatureFlags called on startup and when the cluster leader changes.
// Starts or stops the synchronization of feature flags from upstream management.
func (ps *PlatformService) SetupFeatureFlags() {
ps.featureFlagSynchronizerMutex.Lock()
defer ps.featureFlagSynchronizerMutex.Unlock()
splitKey := *ps.Config().ServiceSettings.SplitKey
splitConfigured := splitKey != ""
syncFeatureFlags := splitConfigured && ps.IsLeader()
ps.configStore.SetReadOnlyFF(!splitConfigured)
if syncFeatureFlags {
if err := ps.startFeatureFlagUpdateJob(); err != nil {
ps.logger.Warn("Unable to setup synchronization with feature flag management. Will fallback to cache.", mlog.Err(err))
}
} else {
ps.StopFeatureFlagUpdateJob()
}
if err := ps.configStore.Load(); err != nil {
ps.logger.Warn("Unable to load config store after feature flag setup.", mlog.Err(err))
}
}
func (ps *PlatformService) updateFeatureFlagValuesFromManagement() {
newCfg := ps.configStore.GetNoEnv().Clone()
oldFlags := *newCfg.FeatureFlags
newFlags := ps.featureFlagSynchronizer.UpdateFeatureFlagValues(oldFlags)
oldFlagsBytes, _ := json.Marshal(oldFlags)
newFlagsBytes, _ := json.Marshal(newFlags)
ps.logger.Debug("Checking feature flags from management service", mlog.String("old_flags", string(oldFlagsBytes)), mlog.String("new_flags", string(newFlagsBytes)))
if oldFlags != newFlags {
ps.logger.Debug("Feature flag change detected, updating config")
*newCfg.FeatureFlags = newFlags
ps.SaveConfig(newCfg, true)
}
}
func (ps *PlatformService) startFeatureFlagUpdateJob() error {
// Can be run multiple times
if ps.featureFlagSynchronizer != nil {
return nil
}
var log *mlog.Logger
if *ps.Config().ServiceSettings.DebugSplit {
log = ps.logger
}
attributes := map[string]any{}
// if we are part of a cloud installation, add its installation and group id
if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" {
attributes["installation_id"] = installationId
}
if groupId := os.Getenv("MM_CLOUD_GROUP_ID"); groupId != "" {
attributes["group_id"] = groupId
}
synchronizer, err := featureflag.NewSynchronizer(featureflag.SyncParams{
ServerID: ps.telemetryId,
SplitKey: *ps.Config().ServiceSettings.SplitKey,
Log: log,
Attributes: attributes,
})
if err != nil {
return err
}
ps.featureFlagStop = make(chan struct{})
ps.featureFlagStopped = make(chan struct{})
ps.featureFlagSynchronizer = synchronizer
syncInterval := *ps.Config().ServiceSettings.FeatureFlagSyncIntervalSeconds
go func() {
ticker := time.NewTicker(time.Duration(syncInterval) * time.Second)
defer ticker.Stop()
defer close(ps.featureFlagStopped)
if err := synchronizer.EnsureReady(); err != nil {
ps.logger.Warn("Problem connecting to feature flag management. Will fallback to cloud cache.", mlog.Err(err))
return
}
ps.updateFeatureFlagValuesFromManagement()
for {
select {
case <-ps.featureFlagStop:
return
case <-ticker.C:
ps.updateFeatureFlagValuesFromManagement()
}
}
}()
return nil
}
func (ps *PlatformService) StopFeatureFlagUpdateJob() {
if ps.featureFlagSynchronizer != nil {
close(ps.featureFlagStop)
<-ps.featureFlagStopped
ps.featureFlagSynchronizer.Close()
ps.featureFlagSynchronizer = nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import "sync/atomic"
// Go creates a goroutine, but maintains a record of it to ensure that execution completes before
// the server is shutdown.
func (ps *PlatformService) Go(f func()) {
atomic.AddInt32(&ps.goroutineCount, 1)
go func() {
f()
atomic.AddInt32(&ps.goroutineCount, -1)
select {
case ps.goroutineExitSignal <- struct{}{}:
default:
}
}()
}
// waitForGoroutines blocks until all goroutines created by PlatformService.Go() exit.
func (ps *PlatformService) waitForGoroutines() {
for atomic.LoadInt32(&ps.goroutineCount) != 0 {
<-ps.goroutineExitSignal
}
}
func (ps *PlatformService) GoBuffered(f func()) {
ps.goroutineBuffered <- struct{}{}
atomic.AddInt32(&ps.goroutineCount, 1)
go func() {
f()
atomic.AddInt32(&ps.goroutineCount, -1)
select {
case ps.goroutineExitSignal <- struct{}{}:
default:
}
<-ps.goroutineBuffered
}()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
LicenseEnv = "MM_LICENSE"
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
)
// JWTClaims custom JWT claims with the needed information for the
// renewal process
type JWTClaims struct {
LicenseID string `json:"license_id"`
ActiveUsers int64 `json:"active_users"`
jwt.StandardClaims
}
func (ps *PlatformService) LicenseManager() einterfaces.LicenseInterface {
return ps.licenseManager
}
func (ps *PlatformService) SetLicenseManager(impl einterfaces.LicenseInterface) {
ps.licenseManager = impl
}
func (ps *PlatformService) License() *model.License {
license, _ := ps.licenseValue.Load().(*model.License)
return license
}
func (ps *PlatformService) LoadLicense() {
// ENV var overrides all other sources of license.
licenseStr := os.Getenv(LicenseEnv)
if licenseStr != "" {
license, err := utils.LicenseValidator.LicenseFromBytes([]byte(licenseStr))
if err != nil {
ps.logger.Error("Failed to read license set in environment.", mlog.Err(err))
return
}
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
canStartTrialLicense, err := ps.licenseManager.CanStartTrial()
if err != nil {
ps.logger.Error("Failed to validate trial eligibility.", mlog.Err(err))
return
}
if !canStartTrialLicense {
ps.logger.Info("Cannot start trial multiple times.")
return
}
}
if ps.ValidateAndSetLicenseBytes([]byte(licenseStr)) {
ps.logger.Info("License key from ENV is valid, unlocking enterprise features.")
}
return
}
licenseId := ""
props, nErr := ps.Store.System().Get()
if nErr == nil {
licenseId = props[model.SystemActiveLicenseId]
}
if !model.IsValidId(licenseId) {
// Lets attempt to load the file from disk since it was missing from the DB
license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk(*ps.Config().ServiceSettings.LicenseFileLocation)
if license != nil {
if _, err := ps.SaveLicense(licenseBytes); err != nil {
ps.logger.Error("Failed to save license key loaded from disk.", mlog.Err(err))
} else {
licenseId = license.Id
}
}
}
record, nErr := ps.Store.License().Get(licenseId)
if nErr != nil {
ps.logger.Error("License key from https://mattermost.com required to unlock enterprise features.", mlog.Err(nErr))
ps.SetLicense(nil)
return
}
ps.ValidateAndSetLicenseBytes([]byte(record.Bytes))
ps.logger.Info("License key valid unlocking enterprise features.")
}
func (ps *PlatformService) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
success, licenseStr := utils.LicenseValidator.ValidateLicense(licenseBytes)
if !success {
return nil, model.NewAppError("addLicense", model.InvalidLicenseError, nil, "", http.StatusBadRequest)
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, model.NewAppError("addLicense", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
uniqueUserCount, err := ps.Store.User().Count(model.UserCountOptions{})
if err != nil {
return nil, model.NewAppError("addLicense", "api.license.add_license.invalid_count.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if uniqueUserCount > int64(*license.Features.Users) {
return nil, model.NewAppError("addLicense", "api.license.add_license.unique_users.app_error", map[string]any{"Users": *license.Features.Users, "Count": uniqueUserCount}, "", http.StatusBadRequest)
}
if license.IsExpired() {
return nil, model.NewAppError("addLicense", model.ExpiredLicenseError, nil, "", http.StatusBadRequest)
}
if *ps.Config().JobSettings.RunJobs && ps.Jobs != nil {
if err := ps.Jobs.StopWorkers(); err != nil && !errors.Is(err, jobs.ErrWorkersNotRunning) {
ps.logger.Warn("Stopping job server workers failed", mlog.Err(err))
}
}
if *ps.Config().JobSettings.RunScheduler && ps.Jobs != nil {
if err := ps.Jobs.StopSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersNotRunning) {
ps.logger.Error("Stopping job server schedulers failed", mlog.Err(err))
}
}
defer func() {
// restart job server workers - this handles the edge case where a license file is uploaded, but the job server
// doesn't start until the server is restarted, which prevents the 'run job now' buttons in system console from
// functioning as expected
if *ps.Config().JobSettings.RunJobs && ps.Jobs != nil {
if err := ps.Jobs.StartWorkers(); err != nil {
ps.logger.Error("Starting job server workers failed", mlog.Err(err))
}
}
if *ps.Config().JobSettings.RunScheduler && ps.Jobs != nil {
if err := ps.Jobs.StartSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersRunning) {
ps.logger.Error("Starting job server schedulers failed", mlog.Err(err))
}
}
}()
if ok := ps.SetLicense(&license); !ok {
return nil, model.NewAppError("addLicense", model.ExpiredLicenseError, nil, "", http.StatusBadRequest)
}
record := &model.LicenseRecord{}
record.Id = license.Id
record.Bytes = string(licenseBytes)
_, nErr := ps.Store.License().Save(record)
if nErr != nil {
ps.RemoveLicense()
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("addLicense", "api.license.add_license.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
sysVar := &model.System{}
sysVar.Name = model.SystemActiveLicenseId
sysVar.Value = license.Id
if err := ps.Store.System().SaveOrUpdate(sysVar); err != nil {
ps.RemoveLicense()
return nil, model.NewAppError("addLicense", "api.license.add_license.save_active.app_error", nil, "", http.StatusInternalServerError)
}
// only on prem licenses set this in the first place
if !license.IsCloud() {
_, err := ps.Store.System().PermanentDeleteByName(model.SystemHostedPurchaseNeedsScreening)
if err != nil {
ps.logger.Warn(fmt.Sprintf("Failed to remove %s system store key", model.SystemHostedPurchaseNeedsScreening))
}
}
ps.ReloadConfig()
ps.InvalidateAllCaches()
return &license, nil
}
func (ps *PlatformService) SetLicense(license *model.License) bool {
oldLicense := ps.licenseValue.Load()
defer func() {
for _, listener := range ps.licenseListeners {
if oldLicense == nil {
listener(nil, license)
} else {
listener(oldLicense.(*model.License), license)
}
}
}()
if license != nil {
license.Features.SetDefaults()
ps.licenseValue.Store(license)
ps.clientLicenseValue.Store(utils.GetClientLicense(license))
return true
}
ps.licenseValue.Store((*model.License)(nil))
ps.clientLicenseValue.Store(map[string]string(nil))
return false
}
func (ps *PlatformService) ValidateAndSetLicenseBytes(b []byte) bool {
if success, licenseStr := utils.LicenseValidator.ValidateLicense(b); success {
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
ps.logger.Warn("Failed to decode license from JSON", mlog.Err(jsonErr))
return false
}
ps.SetLicense(&license)
return true
}
ps.logger.Warn("No valid enterprise license found")
return false
}
func (ps *PlatformService) SetClientLicense(m map[string]string) {
ps.clientLicenseValue.Store(m)
}
func (ps *PlatformService) ClientLicense() map[string]string {
if clientLicense, _ := ps.clientLicenseValue.Load().(map[string]string); clientLicense != nil {
return clientLicense
}
return map[string]string{"IsLicensed": "false"}
}
func (ps *PlatformService) RemoveLicense() *model.AppError {
if license, _ := ps.licenseValue.Load().(*model.License); license == nil {
return nil
}
ps.logger.Info("Remove license.", mlog.String("id", model.SystemActiveLicenseId))
sysVar := &model.System{}
sysVar.Name = model.SystemActiveLicenseId
sysVar.Value = ""
if err := ps.Store.System().SaveOrUpdate(sysVar); err != nil {
return model.NewAppError("RemoveLicense", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
ps.SetLicense(nil)
ps.ReloadConfig()
ps.InvalidateAllCaches()
return nil
}
func (ps *PlatformService) AddLicenseListener(listener func(oldLicense, newLicense *model.License)) string {
id := model.NewId()
ps.licenseListeners[id] = listener
return id
}
func (ps *PlatformService) RemoveLicenseListener(id string) {
delete(ps.licenseListeners, id)
}
func (ps *PlatformService) GetSanitizedClientLicense() map[string]string {
return utils.GetSanitizedClientLicense(ps.ClientLicense())
}
// RequestTrialLicense request a trial license from the mattermost official license server
func (ps *PlatformService) RequestTrialLicense(trialRequest *model.TrialLicenseRequest) *model.AppError {
trialRequestJSON, err := json.Marshal(trialRequest)
if err != nil {
return model.NewAppError("RequestTrialLicense", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp, err := http.Post(ps.getRequestTrialURL(), "application/json", bytes.NewBuffer(trialRequestJSON))
if err != nil {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer resp.Body.Close()
// CloudFlare sitting in front of the Customer Portal will block this request with a 451 response code in the event that the request originates from a country sanctioned by the U.S. Government.
if resp.StatusCode == http.StatusUnavailableForLegalReasons {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.embargoed", nil, "Request for trial license came from an embargoed country", http.StatusUnavailableForLegalReasons)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil,
fmt.Sprintf("Unexpected HTTP status code %q returned by server", resp.Status), http.StatusInternalServerError)
}
var licenseResponse map[string]string
err = json.NewDecoder(resp.Body).Decode(&licenseResponse)
if err != nil {
ps.logger.Warn("Error decoding license response", mlog.Err(err))
}
if _, ok := licenseResponse["license"]; !ok {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil, licenseResponse["message"], http.StatusBadRequest)
}
if _, err := ps.SaveLicense([]byte(licenseResponse["license"])); err != nil {
return err
}
ps.ReloadConfig()
ps.InvalidateAllCaches()
return nil
}
// GenerateRenewalToken returns a renewal token that expires after duration expiration
func (ps *PlatformService) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) {
license := ps.License()
if license == nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.no_license", nil, "", http.StatusBadRequest)
}
if license.IsCloud() {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.bad_license", nil, "", http.StatusBadRequest)
}
activeUsers, err := ps.Store.User().Count(model.UserCountOptions{})
if err != nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
expirationTime := time.Now().UTC().Add(expiration)
claims := &JWTClaims{
LicenseID: license.Id,
ActiveUsers: activeUsers,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(license.Customer.Email))
if err != nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tokenString, nil
}
// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license
func (ps *PlatformService) GenerateLicenseRenewalLink() (string, string, *model.AppError) {
renewalToken, err := ps.GenerateRenewalToken(JWTDefaultTokenExpiration)
if err != nil {
return "", "", err
}
return fmt.Sprintf("%s?token=%s", ps.getLicenseRenewalURL(), renewalToken), renewalToken, nil
}
func (ps *PlatformService) getLicenseRenewalURL() string {
return fmt.Sprintf("%s/subscribe/renew", *ps.Config().CloudSettings.CWSURL)
}
func (ps *PlatformService) getRequestTrialURL() string {
return fmt.Sprintf("%s/api/v1/trials", *ps.Config().CloudSettings.CWSURL)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
)
const LinkCacheSize = 10000
const LinkCacheDuration = 1 * time.Hour
var linkCache = cache.NewLRU(cache.LRUOptions{
Size: LinkCacheSize,
})
func PurgeLinkCache() {
linkCache.Purge()
}
func LinkCache() cache.Cache {
return linkCache
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) Log() mlog.LoggerIFace {
return ps.logger
}
func (ps *PlatformService) ReconfigureLogger() error {
return ps.initLogging()
}
// initLogging initializes and configures the logger(s). This may be called more than once.
func (ps *PlatformService) initLogging() error {
// create the app logger if needed
if ps.logger == nil {
var err error
ps.logger, err = mlog.NewLogger()
if err != nil {
return err
}
logCfg, err := config.MloggerConfigFromLoggerConfig(&ps.Config().LogSettings, nil, config.GetLogFileLocation)
if err != nil {
return err
}
if errCfg := ps.logger.ConfigureTargets(logCfg, nil); errCfg != nil {
return fmt.Errorf("failed to configure test logger: %w", errCfg)
}
}
// create notification logger if needed
if ps.notificationsLogger == nil {
l, err := mlog.NewLogger()
if err != nil {
return err
}
ps.notificationsLogger = l.With(mlog.String("logSource", "notifications"))
}
if err := ps.ConfigureLogger("logging", ps.logger, &ps.Config().LogSettings, config.GetLogFileLocation); err != nil {
// if the config is locked then a unit test has already configured and locked the logger; not an error.
if !errors.Is(err, mlog.ErrConfigurationLock) {
// revert to default logger if the config is invalid
mlog.InitGlobalLogger(nil)
return err
}
}
// Redirect default Go logger to app logger.
ps.logger.RedirectStdLog(mlog.LvlStdLog)
// Use the app logger as the global logger (eventually remove all instances of global logging).
mlog.InitGlobalLogger(ps.logger)
notificationLogSettings := config.GetLogSettingsFromNotificationsLogSettings(&ps.Config().NotificationLogSettings)
if err := ps.ConfigureLogger("notification logging", ps.notificationsLogger, notificationLogSettings, config.GetNotificationsLogFileLocation); err != nil {
if !errors.Is(err, mlog.ErrConfigurationLock) {
mlog.Error("Error configuring notification logger", mlog.Err(err))
return err
}
}
return nil
}
func (ps *PlatformService) Logger() *mlog.Logger {
return ps.logger
}
func (ps *PlatformService) NotificationsLogger() *mlog.Logger {
return ps.notificationsLogger
}
func (ps *PlatformService) EnableLoggingMetrics() {
if ps.metrics == nil || ps.metricsIFace == nil {
return
}
ps.logger.SetMetricsCollector(ps.metricsIFace.GetLoggerMetricsCollector(), mlog.DefaultMetricsUpdateFreqMillis)
// logging config needs to be reloaded when metrics collector is added or changed.
if err := ps.initLogging(); err != nil {
mlog.Error("Error re-configuring logging for metrics")
return
}
mlog.Debug("Logging metrics enabled")
}
// RemoveUnlicensedLogTargets removes any unlicensed log target types.
func (ps *PlatformService) RemoveUnlicensedLogTargets(license *model.License) {
if license != nil && *license.Features.AdvancedLogging {
// advanced logging enabled via license; no need to remove any targets
return
}
timeoutCtx, cancelCtx := context.WithTimeout(context.Background(), time.Second*10)
defer cancelCtx()
ps.logger.RemoveTargets(timeoutCtx, func(ti mlog.TargetInfo) bool {
return ti.Type != "*targets.Writer" && ti.Type != "*targets.File"
})
ps.notificationsLogger.RemoveTargets(timeoutCtx, func(ti mlog.TargetInfo) bool {
return ti.Type != "*targets.Writer" && ti.Type != "*targets.File"
})
}
func (ps *PlatformService) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
var lines []string
if *ps.Config().LogSettings.EnableFile {
ps.Log().Flush()
logFile := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation)
file, err := os.Open(logFile)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
var newLine = []byte{'\n'}
var lineCount int
const searchPos = -1
b := make([]byte, 1)
var endOffset int64 = 0
// if the file exists and it's last byte is '\n' - skip it
var stat os.FileInfo
if stat, err = os.Stat(logFile); err == nil {
if _, err = file.ReadAt(b, stat.Size()-1); err == nil && b[0] == newLine[0] {
endOffset = -1
}
}
lineEndPos, err := file.Seek(endOffset, io.SeekEnd)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for {
pos, err := file.Seek(searchPos, io.SeekCurrent)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
_, err = file.ReadAt(b, pos)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if b[0] == newLine[0] || pos == 0 {
lineCount++
if lineCount > page*perPage {
line := make([]byte, lineEndPos-pos)
_, err := file.ReadAt(line, pos)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filtered := false
var entry *model.LogEntry
err = json.Unmarshal(line, &entry)
if err != nil {
mlog.Debug("Failed to parse line, skipping")
} else {
filtered = isLogFilteredByLevel(logFilter, entry) || filtered
filtered = isLogFilteredByDate(logFilter, entry) || filtered
}
if filtered {
lineCount--
} else {
lines = append(lines, string(line))
}
}
if pos == 0 {
break
}
lineEndPos = pos
}
if len(lines) == perPage {
break
}
}
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
lines[i], lines[j] = lines[j], lines[i]
}
} else {
lines = append(lines, "")
}
return lines, nil
}
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
logLevels := logFilter.LogLevels
if len(logLevels) == 0 {
return false
}
for _, level := range logLevels {
if entry.Level == level {
return false
}
}
return true
}
func isLogFilteredByDate(logFilter *model.LogFilter, entry *model.LogEntry) bool {
if logFilter.DateFrom == "" && logFilter.DateTo == "" {
return false
}
dateFrom, err := time.Parse("2006-01-02 15:04:05.999 -07:00", logFilter.DateFrom)
if err != nil {
dateFrom = time.Time{}
}
dateTo, err := time.Parse("2006-01-02 15:04:05.999 -07:00", logFilter.DateTo)
if err != nil {
dateTo = time.Now()
}
timestamp, err := time.Parse("2006-01-02 15:04:05.999 -07:00", entry.Timestamp)
if err != nil {
mlog.Debug("Cannot parse timestamp, skipping")
return false
}
if timestamp.Equal(dateFrom) || timestamp.Equal(dateTo) {
return false
}
if timestamp.After(dateFrom) && timestamp.Before(dateTo) {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"net"
"net/http"
"net/http/pprof"
"runtime"
"sync"
"text/template"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
type platformMetrics struct {
server *http.Server
router *mux.Router
lock sync.Mutex
logger *mlog.Logger
metricsImpl einterfaces.MetricsInterface
cfgFn func() *model.Config
listenAddr string
}
// resetMetrics resets the metrics server. Clears the metrics if the metrics are disabled by the config.
func (ps *PlatformService) resetMetrics() error {
if !*ps.Config().MetricsSettings.Enable {
if ps.metrics != nil {
return ps.metrics.stopMetricsServer()
}
return nil
}
if ps.metrics != nil {
if err := ps.metrics.stopMetricsServer(); err != nil {
return err
}
}
ps.metrics = &platformMetrics{
cfgFn: ps.Config,
metricsImpl: ps.metricsIFace,
logger: ps.logger,
}
if err := ps.metrics.initMetricsRouter(); err != nil {
return err
}
if ps.metricsIFace != nil {
ps.metricsIFace.Register()
}
return ps.metrics.startMetricsServer()
}
func (pm *platformMetrics) stopMetricsServer() error {
pm.lock.Lock()
defer pm.lock.Unlock()
if pm.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), TimeToWaitForConnectionsToCloseOnServerShutdown)
defer cancel()
if err := pm.server.Shutdown(ctx); err != nil {
return fmt.Errorf("could not shutdown metrics server: %v", err)
}
pm.logger.Info("Metrics and profiling server is stopped")
}
return nil
}
func (pm *platformMetrics) startMetricsServer() error {
var notify chan struct{}
pm.lock.Lock()
defer func() {
if notify != nil {
<-notify
}
pm.lock.Unlock()
}()
l, err := net.Listen("tcp", *pm.cfgFn().MetricsSettings.ListenAddress)
if err != nil {
return err
}
notify = make(chan struct{})
pm.server = &http.Server{
Handler: handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(pm.router),
ReadTimeout: time.Duration(*pm.cfgFn().ServiceSettings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(*pm.cfgFn().ServiceSettings.WriteTimeout) * time.Second,
}
go func() {
close(notify)
if err := pm.server.Serve(l); err != nil && err != http.ErrServerClosed {
pm.logger.Fatal(err.Error())
}
}()
pm.listenAddr = l.Addr().String()
pm.logger.Info("Metrics and profiling server is started", mlog.String("address", pm.listenAddr))
return nil
}
func (pm *platformMetrics) initMetricsRouter() error {
pm.router = mux.NewRouter()
runtime.SetBlockProfileRate(*pm.cfgFn().MetricsSettings.BlockProfileRate)
metricsPage := `
<html>
<body>{{if .}}
<div><a href="/metrics">Metrics</a></div>{{end}}
<div><a href="/debug/pprof/">Profiling Root</a></div>
<div><a href="/debug/pprof/cmdline">Profiling Command Line</a></div>
<div><a href="/debug/pprof/symbol">Profiling Symbols</a></div>
<div><a href="/debug/pprof/goroutine">Profiling Goroutines</a></div>
<div><a href="/debug/pprof/heap">Profiling Heap</a></div>
<div><a href="/debug/pprof/threadcreate">Profiling Threads</a></div>
<div><a href="/debug/pprof/block">Profiling Blocking</a></div>
<div><a href="/debug/pprof/trace">Profiling Execution Trace</a></div>
<div><a href="/debug/pprof/profile">Profiling CPU</a></div>
</body>
</html>
`
metricsPageTmpl, err := template.New("page").Parse(metricsPage)
if err != nil {
return errors.Wrap(err, "failed to create template")
}
rootHandler := func(w http.ResponseWriter, r *http.Request) {
metricsPageTmpl.Execute(w, pm.metricsImpl != nil)
}
pm.router.HandleFunc("/", rootHandler)
pm.router.StrictSlash(true)
pm.router.Handle("/debug", http.RedirectHandler("/", http.StatusMovedPermanently))
pm.router.HandleFunc("/debug/pprof/", pprof.Index)
pm.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
pm.router.HandleFunc("/debug/pprof/profile", pprof.Profile)
pm.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
pm.router.HandleFunc("/debug/pprof/trace", pprof.Trace)
// Manually add support for paths linked to by index page at /debug/pprof/
pm.router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
pm.router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
pm.router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
pm.router.Handle("/debug/pprof/block", pprof.Handler("block"))
return nil
}
func (ps *PlatformService) HandleMetrics(route string, h http.Handler) {
if ps.metrics != nil {
ps.metrics.router.Handle(route, h)
}
}
func (ps *PlatformService) RestartMetrics() error {
return ps.resetMetrics()
}
func (ps *PlatformService) Metrics() einterfaces.MetricsInterface {
if ps.metrics == nil {
return nil
}
return ps.metricsIFace
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/localcachelayer"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Option func(ps *PlatformService) error
// By default, the app will use the store specified by the configuration. This allows you to
// construct an app with a different store.
//
// The override parameter must be either a store.Store or func(App) store.Store().
func StoreOverride(override any) Option {
return func(ps *PlatformService) error {
switch o := override.(type) {
case store.Store:
ps.newStore = func() (store.Store, error) {
return o, nil
}
return nil
case func(*PlatformService) store.Store:
ps.newStore = func() (store.Store, error) {
return o(ps), nil
}
return nil
default:
return errors.New("invalid StoreOverride")
}
}
}
func StoreOverrideWithCache(override store.Store) Option {
return func(ps *PlatformService) error {
ps.newStore = func() (store.Store, error) {
lcl, err := localcachelayer.NewLocalCacheLayer(override, ps.metricsIFace, ps.clusterIFace, ps.cacheProvider)
if err != nil {
return nil, err
}
return lcl, nil
}
return nil
}
}
// Config applies the given config dsn, whether a path to config.json
// or a database connection string. It receives as well a set of
// custom defaults that will be applied for any unset property of the
// config loaded from the dsn on top of the normal defaults
func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
return func(ps *PlatformService) error {
configStore, err := config.NewStoreFromDSN(dsn, readOnly, configDefaults, true)
if err != nil {
return fmt.Errorf("failed to apply Config option: %w", err)
}
ps.configStore = configStore
return nil
}
}
func SetFileStore(filestore filestore.FileBackend) Option {
return func(ps *PlatformService) error {
ps.filestore = filestore
return nil
}
}
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
func ConfigStore(configStore *config.Store) Option {
return func(ps *PlatformService) error {
ps.configStore = configStore
return nil
}
}
func StartMetrics() Option {
return func(ps *PlatformService) error {
ps.startMetrics = true
return nil
}
}
func SetLogger(logger *mlog.Logger) Option {
return func(ps *PlatformService) error {
ps.SetLogger(logger)
return nil
}
}
func SetCluster(cluster einterfaces.ClusterInterface) Option {
return func(ps *PlatformService) error {
ps.clusterIFace = cluster
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) StartSearchEngine() (string, string) {
if ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
ps.Log().Error(err.Error())
}
})
}
configListenerId := ps.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) {
if ps.SearchEngine == nil {
return
}
ps.SearchEngine.UpdateConfig(newConfig)
if ps.SearchEngine.ElasticsearchEngine != nil && !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
})
} else if ps.SearchEngine.ElasticsearchEngine != nil && *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
})
} else if ps.SearchEngine.ElasticsearchEngine != nil && *oldConfig.ElasticsearchSettings.Password != *newConfig.ElasticsearchSettings.Password || *oldConfig.ElasticsearchSettings.Username != *newConfig.ElasticsearchSettings.Username || *oldConfig.ElasticsearchSettings.ConnectionURL != *newConfig.ElasticsearchSettings.ConnectionURL || *oldConfig.ElasticsearchSettings.Sniff != *newConfig.ElasticsearchSettings.Sniff {
ps.Go(func() {
if *oldConfig.ElasticsearchSettings.EnableIndexing {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
}
})
}
})
licenseListenerId := ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
if ps.SearchEngine == nil {
return
}
if oldLicense == nil && newLicense != nil {
if ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
})
}
} else if oldLicense != nil && newLicense == nil {
if ps.SearchEngine.ElasticsearchEngine != nil {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
})
}
}
})
return configListenerId, licenseListenerId
}
func (ps *PlatformService) StopSearchEngine() {
ps.RemoveConfigListener(ps.searchConfigListenerId)
ps.RemoveLicenseListener(ps.searchLicenseListenerId)
if ps.SearchEngine != nil && ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.SearchEngine.ElasticsearchEngine.Stop()
}
if ps.SearchEngine != nil && ps.SearchEngine.BleveEngine != nil && ps.SearchEngine.BleveEngine.IsActive() {
ps.SearchEngine.BleveEngine.Stop()
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"fmt"
"hash/maphash"
"net/http"
"runtime"
"sync"
"sync/atomic"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/featureflag"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/localcachelayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/retrylayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/store/timerlayer"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// PlatformService is the service for the platform related tasks. It is
// responsible for non-entity related functionalities that are required
// by a product such as database access, configuration access, licensing etc.
type PlatformService struct {
sqlStore *sqlstore.SqlStore
Store store.Store
newStore func() (store.Store, error)
WebSocketRouter *WebSocketRouter
configStore *config.Store
filestore filestore.FileBackend
cacheProvider cache.Provider
statusCache cache.Cache
sessionCache cache.Cache
sessionPool sync.Pool
asymmetricSigningKey atomic.Value
clientConfig atomic.Value
clientConfigHash atomic.Value
limitedClientConfig atomic.Value
logger *mlog.Logger
notificationsLogger *mlog.Logger
startMetrics bool
metrics *platformMetrics
metricsIFace einterfaces.MetricsInterface
featureFlagSynchronizerMutex sync.Mutex
featureFlagSynchronizer *featureflag.Synchronizer
featureFlagStop chan struct{}
featureFlagStopped chan struct{}
licenseValue atomic.Value
clientLicenseValue atomic.Value
licenseListeners map[string]func(*model.License, *model.License)
licenseManager einterfaces.LicenseInterface
telemetryId string
configListenerId string
licenseListenerId string
clusterLeaderListeners sync.Map
clusterIFace einterfaces.ClusterInterface
Busy *Busy
SearchEngine *searchengine.Broker
searchConfigListenerId string
searchLicenseListenerId string
Jobs *jobs.JobServer
hubs []*Hub
hashSeed maphash.Seed
goroutineCount int32
goroutineExitSignal chan struct{}
goroutineBuffered chan struct{}
additionalClusterHandlers map[model.ClusterEvent]einterfaces.ClusterMessageHandler
sharedChannelService SharedChannelServiceIFace
pluginEnv HookRunner
}
type HookRunner interface {
RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int)
GetPluginsEnvironment() *plugin.Environment
}
// New creates a new PlatformService.
func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
// Step 0: Create the PlatformService.
// ConfigStore is and should be handled on a upper level.
ps := &PlatformService{
Store: sc.Store,
configStore: sc.ConfigStore,
clusterIFace: sc.Cluster,
hashSeed: maphash.MakeSeed(),
goroutineExitSignal: make(chan struct{}, 1),
goroutineBuffered: make(chan struct{}, runtime.NumCPU()),
WebSocketRouter: &WebSocketRouter{
handlers: make(map[string]webSocketHandler),
},
sessionPool: sync.Pool{
New: func() any {
return &model.Session{}
},
},
licenseListeners: map[string]func(*model.License, *model.License){},
additionalClusterHandlers: map[model.ClusterEvent]einterfaces.ClusterMessageHandler{},
}
// Step 1: Cache provider.
// At the moment we only have this implementation
// in the future the cache provider will be built based on the loaded config
ps.cacheProvider = cache.NewProvider()
if err2 := ps.cacheProvider.Connect(); err2 != nil {
return nil, fmt.Errorf("unable to connect to cache provider: %w", err2)
}
// Apply options, some of the options overrides the default config actually.
for _, option := range options {
if err := option(ps); err != nil {
return nil, fmt.Errorf("failed to apply option: %w", err)
}
}
// the config store is not set, we need to create a new one
if ps.configStore == nil {
innerStore, err := config.NewFileStore("config.json", true)
if err != nil {
return nil, fmt.Errorf("failed to load config from file: %w", err)
}
configStore, err := config.NewStoreFromBacking(innerStore, nil, false)
if err != nil {
return nil, fmt.Errorf("failed to load config from file: %w", err)
}
ps.configStore = configStore
}
// Step 2: Start logging.
if err := ps.initLogging(); err != nil {
return nil, fmt.Errorf("failed to initialize logging: %w", err)
}
// This is called after initLogging() to avoid a race condition.
mlog.Info("Server is initializing...", mlog.String("go_version", runtime.Version()))
// Step 3: Search Engine
searchEngine := searchengine.NewBroker(ps.Config())
bleveEngine := bleveengine.NewBleveEngine(ps.Config())
if err := bleveEngine.Start(); err != nil {
return nil, err
}
searchEngine.RegisterBleveEngine(bleveEngine)
ps.SearchEngine = searchEngine
// Step 4: Init Enterprise
// Depends on step 3 (s.SearchEngine must be non-nil)
ps.initEnterprise()
// Step 5: Init Metrics
if metricsInterfaceFn != nil && ps.metricsIFace == nil { // if the metrics interface is set by options, do not override it
ps.metricsIFace = metricsInterfaceFn(ps, *ps.configStore.Get().SqlSettings.DriverName, *ps.configStore.Get().SqlSettings.DataSource)
}
// Step 6: Store.
// Depends on Step 0 (config), 1 (cacheProvider), 3 (search engine), 5 (metrics) and cluster.
if ps.newStore == nil {
ps.newStore = func() (store.Store, error) {
ps.sqlStore = sqlstore.New(ps.Config().SqlSettings, ps.metricsIFace)
lcl, err2 := localcachelayer.NewLocalCacheLayer(
retrylayer.New(ps.sqlStore),
ps.metricsIFace,
ps.clusterIFace,
ps.cacheProvider,
)
if err2 != nil {
return nil, fmt.Errorf("cannot create local cache layer: %w", err2)
}
searchStore := searchlayer.NewSearchLayer(
lcl,
ps.SearchEngine,
ps.Config(),
)
ps.AddConfigListener(func(prevCfg, cfg *model.Config) {
searchStore.UpdateConfig(cfg)
})
license := ps.License()
ps.sqlStore.UpdateLicense(license)
ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
ps.sqlStore.UpdateLicense(newLicense)
})
return timerlayer.New(
searchStore,
ps.metricsIFace,
), nil
}
}
license := ps.License()
// Step 3: Initialize filestore
if ps.filestore == nil {
insecure := ps.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err2 := filestore.NewFileBackend(ps.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err2 != nil {
return nil, fmt.Errorf("failed to initialize filebackend: %w", err2)
}
ps.filestore = backend
}
var err error
ps.Store, err = ps.newStore()
if err != nil {
return nil, fmt.Errorf("cannot create store: %w", err)
}
// Needed before loading license
ps.statusCache, err = ps.cacheProvider.NewCache(&cache.CacheOptions{
Size: model.StatusCacheSize,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
})
if err != nil {
return nil, fmt.Errorf("unable to create status cache: %w", err)
}
ps.sessionCache, err = ps.cacheProvider.NewCache(&cache.CacheOptions{
Size: model.SessionCacheSize,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
})
if err != nil {
return nil, fmt.Errorf("could not create session cache: %w", err)
}
// Step 7: Init License
if model.BuildEnterpriseReady == "true" {
ps.LoadLicense()
}
// Step 8: Init Metrics Server depends on step 6 (store) and 7 (license)
if ps.startMetrics {
if mErr := ps.resetMetrics(); mErr != nil {
return nil, mErr
}
ps.configStore.AddListener(func(oldCfg, newCfg *model.Config) {
if *oldCfg.MetricsSettings.Enable != *newCfg.MetricsSettings.Enable || *oldCfg.MetricsSettings.ListenAddress != *newCfg.MetricsSettings.ListenAddress {
if mErr := ps.resetMetrics(); mErr != nil {
mlog.Warn("Failed to reset metrics", mlog.Err(mErr))
}
}
})
}
// Step 9: Init AsymmetricSigningKey depends on step 6 (store)
if err = ps.EnsureAsymmetricSigningKey(); err != nil {
return nil, fmt.Errorf("unable to ensure asymmetric signing key: %w", err)
}
ps.Busy = NewBusy(ps.clusterIFace)
// Enable developer settings if this is a "dev" build
if model.BuildNumber == "dev" {
ps.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true })
}
ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
if (oldLicense == nil && newLicense == nil) || !ps.startMetrics {
return
}
if oldLicense != nil && newLicense != nil && *oldLicense.Features.Metrics == *newLicense.Features.Metrics {
return
}
if err := ps.RestartMetrics(); err != nil {
ps.logger.Error("Failed to reset metrics server", mlog.Err(err))
}
})
ps.SearchEngine.UpdateConfig(ps.Config())
searchConfigListenerId, searchLicenseListenerId := ps.StartSearchEngine()
ps.searchConfigListenerId = searchConfigListenerId
ps.searchLicenseListenerId = searchLicenseListenerId
return ps, nil
}
func (ps *PlatformService) Start() error {
ps.hubStart()
ps.configListenerId = ps.AddConfigListener(func(_, _ *model.Config) {
ps.regenerateClientConfig()
message := model.NewWebSocketEvent(model.WebsocketEventConfigChanged, "", "", "", nil, "")
message.Add("config", ps.ClientConfigWithComputed())
ps.Go(func() {
ps.Publish(message)
})
if err := ps.ReconfigureLogger(); err != nil {
mlog.Error("Error re-configuring logging after config change", mlog.Err(err))
return
}
})
ps.licenseListenerId = ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
ps.regenerateClientConfig()
message := model.NewWebSocketEvent(model.WebsocketEventLicenseChanged, "", "", "", nil, "")
message.Add("license", ps.GetSanitizedClientLicense())
ps.Go(func() {
ps.Publish(message)
})
})
return nil
}
func (ps *PlatformService) ShutdownMetrics() error {
if ps.metrics != nil {
return ps.metrics.stopMetricsServer()
}
return nil
}
func (ps *PlatformService) ShutdownConfig() error {
ps.RemoveConfigListener(ps.configListenerId)
if ps.configStore != nil {
err := ps.configStore.Close()
if err != nil {
return fmt.Errorf("failed to close config store: %w", err)
}
}
return nil
}
func (ps *PlatformService) SetTelemetryId(id string) {
ps.telemetryId = id
}
func (ps *PlatformService) SetLogger(logger *mlog.Logger) {
ps.logger = logger
}
func (ps *PlatformService) initEnterprise() {
if clusterInterface != nil && ps.clusterIFace == nil {
ps.clusterIFace = clusterInterface(ps)
}
if elasticsearchInterface != nil {
ps.SearchEngine.RegisterElasticsearchEngine(elasticsearchInterface(ps))
}
if licenseInterface != nil {
ps.licenseManager = licenseInterface(ps)
}
}
func (ps *PlatformService) TotalWebsocketConnections() int {
// This method is only called after the hub is initialized.
// Therefore, no mutex is needed to protect s.hubs.
count := int64(0)
for _, hub := range ps.hubs {
count = count + atomic.LoadInt64(&hub.connectionCount)
}
return int(count)
}
func (ps *PlatformService) Shutdown() error {
ps.HubStop()
ps.RemoveLicenseListener(ps.licenseListenerId)
// we need to wait the goroutines to finish before closing the store
// and this needs to be called after hub stop because hub generates goroutines
// when it is active. If we wait first we have no mechanism to prevent adding
// more go routines hence they still going to be invoked.
ps.waitForGoroutines()
if ps.Store != nil {
ps.Store.Close()
}
if ps.cacheProvider != nil {
if err := ps.cacheProvider.Close(); err != nil {
return fmt.Errorf("unable to cleanly shutdown cache: %w", err)
}
}
return nil
}
func (ps *PlatformService) CacheProvider() cache.Provider {
return ps.cacheProvider
}
func (ps *PlatformService) StatusCache() cache.Cache {
return ps.statusCache
}
// SetSqlStore is used for plugin testing
func (ps *PlatformService) SetSqlStore(s *sqlstore.SqlStore) {
ps.sqlStore = s
}
func (ps *PlatformService) SetSharedChannelService(s SharedChannelServiceIFace) {
ps.sharedChannelService = s
}
func (ps *PlatformService) SetPluginsEnvironment(runner HookRunner) {
ps.pluginEnv = runner
}
// GetPluginStatuses meant to be used by cluster implementation
func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
if ps.pluginEnv == nil || ps.pluginEnv.GetPluginsEnvironment() == nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := ps.pluginEnv.GetPluginsEnvironment().Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Add our cluster ID
for _, status := range pluginStatuses {
if ps.Cluster() != nil {
status.ClusterId = ps.Cluster().GetClusterId()
} else {
status.ClusterId = ""
}
}
return pluginStatuses, nil
}
func (ps *PlatformService) FileBackend() filestore.FileBackend {
return ps.filestore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) ReturnSessionToPool(session *model.Session) {
if session != nil {
session.Id = ""
ps.sessionPool.Put(session)
}
}
func (ps *PlatformService) CreateSession(session *model.Session) (*model.Session, error) {
session.Token = ""
session, err := ps.Store.Session().Save(session)
if err != nil {
return nil, err
}
ps.AddSessionToCache(session)
return session, nil
}
func (ps *PlatformService) GetSessionContext(ctx context.Context, token string) (*model.Session, error) {
return ps.Store.Session().Get(ctx, token)
}
func (ps *PlatformService) GetSessions(userID string) ([]*model.Session, error) {
return ps.Store.Session().GetSessions(userID)
}
func (ps *PlatformService) AddSessionToCache(session *model.Session) {
ps.sessionCache.SetWithExpiry(session.Token, session, time.Duration(int64(*ps.Config().ServiceSettings.SessionCacheInMinutes))*time.Minute)
}
func (ps *PlatformService) SessionCacheLength() int {
if l, err := ps.sessionCache.Len(); err == nil {
return l
}
return 0
}
func (ps *PlatformService) ClearUserSessionCacheLocal(userID string) {
if keys, err := ps.sessionCache.Keys(); err == nil {
var session *model.Session
for _, key := range keys {
if err := ps.sessionCache.Get(key, &session); err == nil {
if session.UserId == userID {
ps.sessionCache.Remove(key)
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheInvalidationCounterSession()
}
}
}
}
}
}
func (ps *PlatformService) ClearAllUsersSessionCacheLocal() {
ps.sessionCache.Purge()
}
func (ps *PlatformService) ClearUserSessionCache(userID string) {
ps.ClearUserSessionCacheLocal(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventClearSessionCacheForUser,
SendType: model.ClusterSendReliable,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) ClearAllUsersSessionCache() {
ps.ClearAllUsersSessionCacheLocal()
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventClearSessionCacheForAllUsers,
SendType: model.ClusterSendReliable,
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) GetSession(token string) (*model.Session, error) {
var session = ps.sessionPool.Get().(*model.Session)
if err := ps.sessionCache.Get(token, session); err == nil {
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheHitCounterSession()
}
} else {
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheMissCounterSession()
}
}
if session.Id != "" {
return session, nil
}
return ps.GetSessionContext(sqlstore.WithMaster(context.Background()), token)
}
func (ps *PlatformService) GetSessionByID(sessionID string) (*model.Session, error) {
return ps.Store.Session().Get(context.Background(), sessionID)
}
func (ps *PlatformService) RevokeSessionsFromAllUsers() error {
// revoke tokens before sessions so they can't be used to relogin
nErr := ps.Store.OAuth().RemoveAllAccessData()
if nErr != nil {
return fmt.Errorf("%s: %w", nErr.Error(), DeleteAllAccessDataError)
}
err := ps.Store.Session().RemoveAllSessions()
if err != nil {
return err
}
ps.ClearAllUsersSessionCache()
return nil
}
func (ps *PlatformService) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) error {
sessions, err := ps.Store.Session().GetSessions(userID)
if err != nil {
return err
}
for _, session := range sessions {
if session.DeviceId == deviceID && session.Id != currentSessionId {
mlog.Debug("Revoking sessionId for userId. Re-login with the same device Id", mlog.String("session_id", session.Id), mlog.String("user_id", userID))
if err := ps.RevokeSession(session); err != nil {
mlog.Warn("Could not revoke session for device", mlog.String("device_id", deviceID), mlog.Err(err))
}
}
}
return nil
}
func (ps *PlatformService) RevokeSession(session *model.Session) error {
if session.IsOAuth {
if err := ps.RevokeAccessToken(session.Token); err != nil {
return err
}
} else {
if err := ps.Store.Session().Remove(session.Id); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
}
ps.ClearUserSessionCache(session.UserId)
return nil
}
func (ps *PlatformService) RevokeAccessToken(token string) error {
session, _ := ps.GetSession(token)
defer ps.ReturnSessionToPool(session)
schan := make(chan error, 1)
go func() {
schan <- ps.Store.Session().Remove(token)
close(schan)
}()
if _, err := ps.Store.OAuth().GetAccessData(token); err != nil {
return fmt.Errorf("%s: %w", err.Error(), GetTokenError)
}
if err := ps.Store.OAuth().RemoveAccessData(token); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteTokenError)
}
if err := <-schan; err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
if session != nil {
ps.ClearUserSessionCache(session.UserId)
}
return nil
}
// SetSessionExpireInHours sets the session's expiry the specified number of hours
// relative to either the session creation date or the current time, depending
// on the `ExtendSessionOnActivity` config setting.
func (ps *PlatformService) SetSessionExpireInHours(session *model.Session, hours int) {
if session.CreateAt == 0 || *ps.Config().ServiceSettings.ExtendSessionLengthWithActivity {
session.ExpiresAt = model.GetMillis() + (1000 * 60 * 60 * int64(hours))
} else {
session.ExpiresAt = session.CreateAt + (1000 * 60 * 60 * int64(hours))
}
}
func (ps *PlatformService) ExtendSessionExpiry(session *model.Session, newExpiry int64) error {
if err := ps.Store.Session().UpdateExpiresAt(session.Id, newExpiry); err != nil {
return err
}
// Update local cache. No need to invalidate cache for cluster as the session cache timeout
// ensures each node will get an extended expiry within the next 10 minutes.
// Worst case is another node may generate a redundant expiry update.
session.ExpiresAt = newExpiry
ps.AddSessionToCache(session)
return nil
}
func (ps *PlatformService) UpdateSessionsIsGuest(userID string, isGuest bool) error {
sessions, err := ps.GetSessions(userID)
if err != nil {
return err
}
for _, session := range sessions {
session.AddProp(model.SessionPropIsGuest, fmt.Sprintf("%t", isGuest))
err := ps.Store.Session().UpdateProps(session)
if err != nil {
mlog.Warn("Unable to update isGuest session", mlog.Err(err))
continue
}
ps.AddSessionToCache(session)
}
return nil
}
func (ps *PlatformService) RevokeAllSessions(userID string) error {
sessions, err := ps.Store.Session().GetSessions(userID)
if err != nil {
return fmt.Errorf("%s: %w", err.Error(), GetSessionError)
}
for _, session := range sessions {
if session.IsOAuth {
ps.RevokeAccessToken(session.Token)
} else {
if err := ps.Store.Session().Remove(session.Id); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
}
}
ps.ClearUserSessionCache(userID)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var sharedChannelEventsForSync model.StringArray = []string{
model.WebsocketEventPosted,
model.WebsocketEventPostEdited,
model.WebsocketEventPostDeleted,
model.WebsocketEventReactionAdded,
model.WebsocketEventReactionRemoved,
}
var sharedChannelEventsForInvitation model.StringArray = []string{
model.WebsocketEventDirectAdded,
}
// SharedChannelSyncHandler is called when a websocket event is received by a cluster node.
// Only on the leader node it will notify the sync service to perform necessary updates to the remote for the given
// shared channel.
func (ps *PlatformService) SharedChannelSyncHandler(event *model.WebSocketEvent) {
syncService := ps.sharedChannelService
if syncService == nil {
return
}
if isEligibleForEvents(syncService, event, sharedChannelEventsForSync) {
err := handleContentSync(ps, syncService, event)
if err != nil {
mlog.Warn(
err.Error(),
mlog.String("event", event.EventType()),
mlog.String("action", "content_sync"),
)
}
} else if isEligibleForEvents(syncService, event, sharedChannelEventsForInvitation) {
err := handleInvitation(ps, syncService, event)
if err != nil {
mlog.Warn(
err.Error(),
mlog.String("event", event.EventType()),
mlog.String("action", "invitation"),
)
}
}
}
func isEligibleForEvents(syncService SharedChannelServiceIFace, event *model.WebSocketEvent, events model.StringArray) bool {
return syncServiceEnabled(syncService) &&
eventHasChannel(event) &&
events.Contains(event.EventType())
}
func eventHasChannel(event *model.WebSocketEvent) bool {
return event.GetBroadcast() != nil &&
event.GetBroadcast().ChannelId != ""
}
func syncServiceEnabled(syncService SharedChannelServiceIFace) bool {
return syncService != nil &&
syncService.Active()
}
func handleContentSync(ps *PlatformService, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
channel, err := findChannel(ps, event.GetBroadcast().ChannelId)
if err != nil {
return err
}
if channel != nil && channel.IsShared() {
syncService.NotifyChannelChanged(channel.Id)
}
return nil
}
func handleInvitation(ps *PlatformService, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
channel, err := findChannel(ps, event.GetBroadcast().ChannelId)
if err != nil {
return err
}
if channel == nil || !channel.IsShared() {
return nil
}
creator, err := getUserFromEvent(ps, event, "creator_id")
if err != nil {
return err
}
// This is a termination condition, since on the other end when we are processing
// the invite we are re-triggering a model.WEBSOCKET_EVENT_DIRECT_ADDED, which will call this handler.
// When the creator is remote, it means that this is a DM that was not originated from the current server
// and therefore we do not need to do anything.
if creator == nil || creator.IsRemote() {
return nil
}
participant, err := getUserFromEvent(ps, event, "teammate_id")
if err != nil {
return err
}
if participant == nil || participant.RemoteId == nil {
return nil
}
rc, err := ps.Store.RemoteCluster().Get(*participant.RemoteId)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("couldn't find remote cluster %s, for creating shared channel invitation for a DM", *participant.RemoteId))
}
return syncService.SendChannelInvite(channel, creator.Id, rc, sharedchannel.WithDirectParticipantID(creator.Id), sharedchannel.WithDirectParticipantID(participant.Id))
}
func getUserFromEvent(ps *PlatformService, event *model.WebSocketEvent, key string) (*model.User, error) {
userID, ok := event.GetData()[key].(string)
if !ok || userID == "" {
return nil, fmt.Errorf("received websocket message that is eligible for sending an invitation but message does not have `%s` present", key)
}
user, err := ps.Store.User().Get(context.Background(), userID)
if err != nil {
return nil, errors.Wrap(err, "couldn't find user for creating shared channel invitation for a DM")
}
return user, nil
}
func findChannel(server *PlatformService, channelId string) (*model.Channel, error) {
channel, err := server.Store.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrap(err, "received websocket message that is eligible for shared channel sync but channel does not exist")
}
return channel, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
)
// SharedChannelServiceIFace is the interface to the shared channel service
type SharedChannelServiceIFace interface {
Shutdown() error
Start() error
NotifyChannelChanged(channelId string)
NotifyUserProfileChanged(userID string)
SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error
Active() bool
}
type MockOptionSharedChannelService func(service *mockSharedChannelService)
func MockOptionSharedChannelServiceWithActive(active bool) MockOptionSharedChannelService {
return func(mrcs *mockSharedChannelService) {
mrcs.active = active
}
}
func NewMockSharedChannelService(service SharedChannelServiceIFace, options ...MockOptionSharedChannelService) *mockSharedChannelService {
mrcs := &mockSharedChannelService{service, true, []string{}, []string{}, 0}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockSharedChannelService struct {
SharedChannelServiceIFace
active bool
channelNotifications []string
userProfileNotifications []string
numInvitations int
}
func (mrcs *mockSharedChannelService) NotifyChannelChanged(channelId string) {
mrcs.channelNotifications = append(mrcs.channelNotifications, channelId)
}
func (mrcs *mockSharedChannelService) NotifyUserProfileChanged(userId string) {
mrcs.userProfileNotifications = append(mrcs.userProfileNotifications, userId)
}
func (mrcs *mockSharedChannelService) Shutdown() error {
return nil
}
func (mrcs *mockSharedChannelService) Start() error {
return nil
}
func (mrcs *mockSharedChannelService) Active() bool {
return mrcs.active
}
func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error {
mrcs.numInvitations += 1
return nil
}
func (mrcs *mockSharedChannelService) NumInvitations() int {
return mrcs.numInvitations
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) AddStatusCacheSkipClusterSend(status *model.Status) {
ps.statusCache.Set(status.UserId, status)
}
func (ps *PlatformService) AddStatusCache(status *model.Status) {
ps.AddStatusCacheSkipClusterSend(status)
if ps.Cluster() != nil {
statusJSON, err := json.Marshal(status)
if err != nil {
ps.logger.Warn("Failed to encode status to JSON", mlog.Err(err))
}
msg := &model.ClusterMessage{
Event: model.ClusterEventUpdateStatus,
SendType: model.ClusterSendBestEffort,
Data: statusJSON,
}
ps.Cluster().SendClusterMessage(msg)
}
}
func (ps *PlatformService) GetAllStatuses() map[string]*model.Status {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return map[string]*model.Status{}
}
statusMap := map[string]*model.Status{}
if userIDs, err := ps.statusCache.Keys(); err == nil {
for _, userID := range userIDs {
status := ps.GetStatusFromCache(userID)
if status != nil {
statusMap[userID] = status
}
}
}
return statusMap
}
func (ps *PlatformService) GetStatusesByIds(userIDs []string) (map[string]any, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return map[string]any{}, nil
}
statusMap := map[string]any{}
metrics := ps.Metrics()
missingUserIds := []string{}
for _, userID := range userIDs {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusMap[userID] = status.Status
if metrics != nil {
metrics.IncrementMemCacheHitCounter("Status")
}
} else {
missingUserIds = append(missingUserIds, userID)
if metrics != nil {
metrics.IncrementMemCacheMissCounter("Status")
}
}
}
if len(missingUserIds) > 0 {
statuses, err := ps.Store.Status().GetByIds(missingUserIds)
if err != nil {
return nil, model.NewAppError("GetStatusesByIds", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, s := range statuses {
ps.AddStatusCacheSkipClusterSend(s)
statusMap[s.UserId] = s.Status
}
}
// For the case where the user does not have a row in the Status table and cache
for _, userID := range missingUserIds {
if _, ok := statusMap[userID]; !ok {
statusMap[userID] = model.StatusOffline
}
}
return statusMap, nil
}
// GetUserStatusesByIds used by apiV4
func (ps *PlatformService) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return []*model.Status{}, nil
}
var statusMap []*model.Status
metrics := ps.Metrics()
missingUserIds := []string{}
for _, userID := range userIDs {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusMap = append(statusMap, status)
if metrics != nil {
metrics.IncrementMemCacheHitCounter("Status")
}
} else {
missingUserIds = append(missingUserIds, userID)
if metrics != nil {
metrics.IncrementMemCacheMissCounter("Status")
}
}
}
if len(missingUserIds) > 0 {
statuses, err := ps.Store.Status().GetByIds(missingUserIds)
if err != nil {
return nil, model.NewAppError("GetUserStatusesByIds", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, s := range statuses {
ps.AddStatusCacheSkipClusterSend(s)
}
statusMap = append(statusMap, statuses...)
}
// For the case where the user does not have a row in the Status table and cache
// remove the existing ids from missingUserIds and then create a offline state for the missing ones
// This also return the status offline for the non-existing Ids in the system
for i := 0; i < len(missingUserIds); i++ {
missingUserId := missingUserIds[i]
for _, userMap := range statusMap {
if missingUserId == userMap.UserId {
missingUserIds = append(missingUserIds[:i], missingUserIds[i+1:]...)
i--
break
}
}
}
for _, userID := range missingUserIds {
statusMap = append(statusMap, &model.Status{UserId: userID, Status: "offline"})
}
return statusMap, nil
}
func (ps *PlatformService) BroadcastStatus(status *model.Status) {
if ps.Busy.IsBusy() {
// this is considered a non-critical service and will be disabled when server busy.
return
}
event := model.NewWebSocketEvent(model.WebsocketEventStatusChange, "", "", status.UserId, nil, "")
event.Add("status", status.Status)
event.Add("user_id", status.UserId)
ps.Publish(event)
}
func (ps *PlatformService) SaveAndBroadcastStatus(status *model.Status) {
ps.AddStatusCache(status)
if err := ps.Store.Status().SaveOrUpdate(status); err != nil {
mlog.Warn("Failed to save status", mlog.String("user_id", status.UserId), mlog.Err(err))
}
ps.BroadcastStatus(status)
}
func (ps *PlatformService) GetStatusFromCache(userID string) *model.Status {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusCopy := &model.Status{}
*statusCopy = *status
return statusCopy
}
return nil
}
func (ps *PlatformService) GetStatus(userID string) (*model.Status, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return &model.Status{}, nil
}
status := ps.GetStatusFromCache(userID)
if status != nil {
return status, nil
}
status, err := ps.Store.Status().Get(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetStatus", "app.status.get.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetStatus", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return status, nil
}
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
// while an 'away' device is still connected
func (ps *PlatformService) SetStatusLastActivityAt(userID string, activityAt int64) {
var status *model.Status
var err *model.AppError
if status, err = ps.GetStatus(userID); err != nil {
return
}
status.LastActivityAt = activityAt
ps.AddStatusCacheSkipClusterSend(status)
ps.SetStatusAwayIfNeeded(userID, false)
}
func (ps *PlatformService) UpdateLastActivityAtIfNeeded(session model.Session) {
now := model.GetMillis()
ps.UpdateWebConnUserActivity(session, now)
if now-session.LastActivityAt < model.SessionActivityTimeout {
return
}
if err := ps.Store.Session().UpdateLastActivityAt(session.Id, now); err != nil {
mlog.Warn("Failed to update LastActivityAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
}
session.LastActivityAt = now
ps.AddSessionToCache(&session)
}
func (ps *PlatformService) SetStatusOnline(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
broadcast := false
var oldStatus string = model.StatusOffline
var oldTime int64
var oldManual bool
var status *model.Status
var err *model.AppError
if status, err = ps.GetStatus(userID); err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
broadcast = true
} else {
if status.Manual && !manual {
return // manually set status always overrides non-manual one
}
if status.Status != model.StatusOnline {
broadcast = true
}
oldStatus = status.Status
oldTime = status.LastActivityAt
oldManual = status.Manual
status.Status = model.StatusOnline
status.Manual = false // for "online" there's no manual setting
status.LastActivityAt = model.GetMillis()
}
ps.AddStatusCache(status)
// Only update the database if the status has changed, the status has been manually set,
// or enough time has passed since the previous action
if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.StatusMinUpdateTime {
if broadcast {
if err := ps.Store.Status().SaveOrUpdate(status); err != nil {
mlog.Warn("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
}
} else {
if err := ps.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt); err != nil {
mlog.Error("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
}
}
}
if broadcast {
ps.BroadcastStatus(status)
}
}
func (ps *PlatformService) SetStatusOffline(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err == nil && status.Manual && !manual {
return // manually set status always overrides non-manual one
}
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusAwayIfNeeded(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: 0, ActiveChannel: ""}
}
if !manual && status.Manual {
return // manually set status always overrides non-manual one
}
if !manual {
if status.Status == model.StatusAway {
return
}
if !ps.isUserAway(status.LastActivityAt) {
return
}
}
status.Status = model.StatusAway
status.Manual = manual
status.ActiveChannel = ""
ps.SaveAndBroadcastStatus(status)
}
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
// and sets status of given userId to dnd which will be restored back after endtime
func (ps *PlatformService) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userId)
if err != nil {
status = &model.Status{UserId: userId, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.PrevStatus = status.Status
status.Status = model.StatusDnd
status.Manual = true
status.DNDEndTime = endtime
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusDoNotDisturb(userID string) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.Status = model.StatusDnd
status.Manual = true
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusOutOfOffice(userID string) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOutOfOffice, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.Status = model.StatusOutOfOffice
status.Manual = true
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) isUserAway(lastActivityAt int64) bool {
return model.GetMillis()-lastActivityAt >= *ps.Config().TeamSettings.UserStatusAwayTimeout*1000
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"crypto/sha256"
"encoding/base64"
)
func getKeyHash(key string) string {
hash := sha256.New()
hash.Write([]byte(key))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/vmihailenco/msgpack/v5"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
sendQueueSize = 256
sendSlowWarn = (sendQueueSize * 50) / 100
sendFullWarn = (sendQueueSize * 95) / 100
writeWaitTime = 30 * time.Second
pongWaitTime = 100 * time.Second
pingInterval = (pongWaitTime * 6) / 10
authCheckInterval = 5 * time.Second
webConnMemberCacheTime = 1000 * 60 * 30 // 30 minutes
deadQueueSize = 128 // Approximated from /proc/sys/net/core/wmem_default / 2048 (avg msg size)
websocketSuppressWarnThreshold = time.Minute
)
const (
reconnectFound = "success"
reconnectNotFound = "failure"
reconnectLossless = "lossless"
)
const websocketMessagePluginPrefix = "custom_"
type pluginWSPostedHook struct {
connectionID string
userID string
req *model.WebSocketRequest
}
type WebConnConfig struct {
WebSocket *websocket.Conn
Session model.Session
TFunc i18n.TranslateFunc
Locale string
ConnectionID string
Active bool
ReuseCount int
// These aren't necessary to be exported to api layer.
sequence int
activeQueue chan model.WebSocketMessage
deadQueue []*model.WebSocketEvent
deadQueuePointer int
}
// WebConn represents a single websocket connection to a user.
// It contains all the necessary state to manage sending/receiving data to/from
// a websocket.
type WebConn struct {
sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically
Platform *PlatformService
Suite SuiteIFace
HookRunner HookRunner
WebSocket *websocket.Conn
T i18n.TranslateFunc
Locale string
Sequence int64
UserId string
allChannelMembers map[string]string
lastAllChannelMembersTime int64
lastUserActivityAt int64
send chan model.WebSocketMessage
// deadQueue behaves like a queue of a finite size
// which is used to store all messages that are sent via the websocket.
// It basically acts as the user-space socket buffer, and is used
// to resuscitate any messages that might have got lost when the connection is broken.
// It is implemented by using a circular buffer to keep it fast.
deadQueue []*model.WebSocketEvent
// Pointer which indicates the next slot to insert.
// It is only to be incremented during writing or clearing the queue.
deadQueuePointer int
// active indicates whether there is an open websocket connection attached
// to this webConn or not.
// It is not used as an atomic, because there is no need to.
// So do not use this outside the web hub.
active bool
// reuseCount indicates how many times this connection has been reused.
// This is used to differentiate between a fresh connection and
// a reused connection.
// It's theoretically possible for this number to wrap around. But we
// leave that as an edge-case.
reuseCount int
sessionToken atomic.Value
session atomic.Value
connectionID atomic.Value
endWritePump chan struct{}
pumpFinished chan struct{}
pluginPosted chan pluginWSPostedHook
// These counters are to suppress spammy websocket.slow
// and websocket.full logs which happen continuously, if they
// do happen. To improve the situation, we log them only once
// per minute.
lastLogTimeSlow time.Time
lastLogTimeFull time.Time
}
// CheckConnResult indicates whether a connectionID was present in the hub or not.
// And if so, contains the active and dead queue details.
type CheckConnResult struct {
ConnectionID string
UserID string
ActiveQueue chan model.WebSocketMessage
DeadQueue []*model.WebSocketEvent
DeadQueuePointer int
ReuseCount int
}
// PopulateWebConnConfig checks if the connection id already exists in the hub,
// and if so, accordingly populates the other fields of the webconn.
func (ps *PlatformService) PopulateWebConnConfig(s *model.Session, cfg *WebConnConfig, seqVal string) (*WebConnConfig, error) {
if !model.IsValidId(cfg.ConnectionID) {
return nil, fmt.Errorf("invalid connection id: %s", cfg.ConnectionID)
}
// This does not handle reconnect requests across nodes in a cluster.
// It falls back to the non-reliable case in that scenario.
res := ps.CheckWebConn(s.UserId, cfg.ConnectionID)
if res == nil {
// If the connection is not present, then we assume either timeout,
// or server restart. In that case, we set a new one.
cfg.ConnectionID = model.NewId()
} else {
// Connection is present, we get the active queue, dead queue
cfg.activeQueue = res.ActiveQueue
cfg.deadQueue = res.DeadQueue
cfg.deadQueuePointer = res.DeadQueuePointer
cfg.Active = false
cfg.ReuseCount = res.ReuseCount
// Now we get the sequence number
if seqVal == "" {
// Sequence_number must be sent with connection id.
// A client must be either non-compliant or fully compliant.
return nil, errors.New("sequence number not present in websocket request")
}
var err error
cfg.sequence, err = strconv.Atoi(seqVal)
if err != nil || cfg.sequence < 0 {
return nil, fmt.Errorf("invalid sequence number %s in query param: %v", seqVal, err)
}
}
return cfg, nil
}
// NewWebConn returns a new WebConn instance.
func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runner HookRunner) *WebConn {
if cfg.Session.UserId != "" {
ps.Go(func() {
ps.SetStatusOnline(cfg.Session.UserId, false)
ps.UpdateLastActivityAtIfNeeded(cfg.Session)
})
}
// Disable TCP_NO_DELAY for higher throughput
var tcpConn *net.TCPConn
switch conn := cfg.WebSocket.UnderlyingConn().(type) {
case *net.TCPConn:
tcpConn = conn
case *tls.Conn:
newConn, ok := conn.NetConn().(*net.TCPConn)
if ok {
tcpConn = newConn
}
}
if tcpConn != nil {
err := tcpConn.SetNoDelay(false)
if err != nil {
mlog.Warn("Error in setting NoDelay socket opts", mlog.Err(err))
}
}
if cfg.activeQueue == nil {
cfg.activeQueue = make(chan model.WebSocketMessage, sendQueueSize)
}
if cfg.deadQueue == nil {
cfg.deadQueue = make([]*model.WebSocketEvent, deadQueueSize)
}
wc := &WebConn{
Platform: ps,
Suite: suite,
HookRunner: runner,
send: cfg.activeQueue,
deadQueue: cfg.deadQueue,
deadQueuePointer: cfg.deadQueuePointer,
Sequence: int64(cfg.sequence),
WebSocket: cfg.WebSocket,
lastUserActivityAt: model.GetMillis(),
UserId: cfg.Session.UserId,
T: cfg.TFunc,
Locale: cfg.Locale,
active: cfg.Active,
reuseCount: cfg.ReuseCount,
endWritePump: make(chan struct{}),
pumpFinished: make(chan struct{}),
pluginPosted: make(chan pluginWSPostedHook, 10),
lastLogTimeSlow: time.Now(),
lastLogTimeFull: time.Now(),
}
wc.SetSession(&cfg.Session)
wc.SetSessionToken(cfg.Session.Token)
wc.SetSessionExpiresAt(cfg.Session.ExpiresAt)
wc.SetConnectionID(cfg.ConnectionID)
wc.Platform.Go(func() {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId)
return true
}, plugin.OnWebSocketConnectID)
})
return wc
}
func (wc *WebConn) pluginPostedConsumer(wg *sync.WaitGroup) {
defer wg.Done()
for msg := range wc.pluginPosted {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req)
return true
}, plugin.WebSocketMessageHasBeenPostedID)
}
}
// Close closes the WebConn.
func (wc *WebConn) Close() {
wc.WebSocket.Close()
<-wc.pumpFinished
}
// GetSessionExpiresAt returns the time at which the session expires.
func (wc *WebConn) GetSessionExpiresAt() int64 {
return atomic.LoadInt64(&wc.sessionExpiresAt)
}
// SetSessionExpiresAt sets the time at which the session expires.
func (wc *WebConn) SetSessionExpiresAt(v int64) {
atomic.StoreInt64(&wc.sessionExpiresAt, v)
}
// GetSessionToken returns the session token of the connection.
func (wc *WebConn) GetSessionToken() string {
return wc.sessionToken.Load().(string)
}
// SetSessionToken sets the session token of the connection.
func (wc *WebConn) SetSessionToken(v string) {
wc.sessionToken.Store(v)
}
// SetConnectionID sets the connection id of the connection.
func (wc *WebConn) SetConnectionID(id string) {
wc.connectionID.Store(id)
}
// GetConnectionID returns the connection id of the connection.
func (wc *WebConn) GetConnectionID() string {
return wc.connectionID.Load().(string)
}
// areAllInactive returns whether all of the connections
// are inactive or not.
func areAllInactive(conns []*WebConn) bool {
for _, conn := range conns {
if conn.active {
return false
}
}
return true
}
// GetSession returns the session of the connection.
func (wc *WebConn) GetSession() *model.Session {
return wc.session.Load().(*model.Session)
}
// SetSession sets the session of the connection.
func (wc *WebConn) SetSession(v *model.Session) {
if v != nil {
v = v.DeepCopy()
}
wc.session.Store(v)
}
// Pump starts the WebConn instance. After this, the websocket
// is ready to send/receive messages.
func (wc *WebConn) Pump() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wc.writePump()
}()
wg.Add(1)
go wc.pluginPostedConsumer(&wg)
wc.readPump()
close(wc.endWritePump)
close(wc.pluginPosted)
wg.Wait()
wc.Platform.HubUnregister(wc)
close(wc.pumpFinished)
wc.Platform.Go(func() {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId)
return true
}, plugin.OnWebSocketDisconnectID)
})
}
func (wc *WebConn) readPump() {
defer func() {
wc.WebSocket.Close()
}()
wc.WebSocket.SetReadLimit(model.SocketMaxMessageSizeKb)
wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime))
wc.WebSocket.SetPongHandler(func(string) error {
if err := wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime)); err != nil {
return err
}
if wc.IsAuthenticated() {
wc.Platform.Go(func() {
wc.Platform.SetStatusAwayIfNeeded(wc.UserId, false)
})
}
return nil
})
for {
msgType, rd, err := wc.WebSocket.NextReader()
if err != nil {
wc.logSocketErr("websocket.NextReader", err)
return
}
var decoder interface {
Decode(v any) error
}
if msgType == websocket.TextMessage {
decoder = json.NewDecoder(rd)
} else {
decoder = msgpack.NewDecoder(rd)
}
var req model.WebSocketRequest
if err = decoder.Decode(&req); err != nil {
wc.logSocketErr("websocket.Decode", err)
return
}
// Messages which actions are prefixed with the plugin prefix
// should only be dispatched to the plugins
if !strings.HasPrefix(req.Action, websocketMessagePluginPrefix) {
wc.Platform.WebSocketRouter.ServeWebSocket(wc, &req)
}
clonedReq, err := req.Clone()
if err != nil {
wc.logSocketErr("websocket.cloneRequest", err)
continue
}
wc.pluginPosted <- pluginWSPostedHook{wc.GetConnectionID(), wc.UserId, clonedReq}
}
}
func (wc *WebConn) writePump() {
ticker := time.NewTicker(pingInterval)
authTicker := time.NewTicker(authCheckInterval)
defer func() {
ticker.Stop()
authTicker.Stop()
wc.WebSocket.Close()
}()
if wc.Sequence != 0 {
if ok, index := wc.isInDeadQueue(wc.Sequence); ok {
if err := wc.drainDeadQueue(index); err != nil {
wc.logSocketErr("websocket.drainDeadQueue", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectFound)
}
} else if wc.hasMsgLoss() {
// If the seq number is not in dead queue, but it was supposed to be,
// then generate a different connection ID,
// and set sequence to 0, and clear dead queue.
wc.clearDeadQueue()
wc.SetConnectionID(model.NewId())
wc.Sequence = 0
// Send hello message
msg := wc.createHelloMessage()
wc.addToDeadQueue(msg)
if err := wc.writeMessage(msg); err != nil {
wc.logSocketErr("websocket.sendHello", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectNotFound)
}
} else {
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectLossless)
}
}
}
var buf bytes.Buffer
// 2k is seen to be a good heuristic under which 98.5% of message sizes remain.
buf.Grow(1024 * 2)
enc := json.NewEncoder(&buf)
for {
select {
case msg, ok := <-wc.send:
if !ok {
wc.writeMessageBuf(websocket.CloseMessage, []byte{})
return
}
evt, evtOk := msg.(*model.WebSocketEvent)
buf.Reset()
var err error
if evtOk {
evt = evt.SetSequence(wc.Sequence)
err = evt.Encode(enc)
wc.Sequence++
} else {
err = enc.Encode(msg)
}
if err != nil {
mlog.Warn("Error in encoding websocket message", mlog.Err(err))
continue
}
if len(wc.send) >= sendFullWarn && time.Since(wc.lastLogTimeFull) > websocketSuppressWarnThreshold {
logData := []mlog.Field{
mlog.String("user_id", wc.UserId),
mlog.String("type", msg.EventType()),
mlog.Int("size", buf.Len()),
}
if evtOk {
logData = append(logData, mlog.String("channel_id", evt.GetBroadcast().ChannelId))
}
mlog.Warn("websocket.full", logData...)
wc.lastLogTimeFull = time.Now()
}
if evtOk {
wc.addToDeadQueue(evt)
}
if err := wc.writeMessageBuf(websocket.TextMessage, buf.Bytes()); err != nil {
wc.logSocketErr("websocket.send", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebSocketBroadcast(msg.EventType())
}
case <-ticker.C:
if err := wc.writeMessageBuf(websocket.PingMessage, []byte{}); err != nil {
wc.logSocketErr("websocket.ticker", err)
return
}
case <-wc.endWritePump:
return
case <-authTicker.C:
if wc.GetSessionToken() == "" {
mlog.Debug("websocket.authTicker: did not authenticate", mlog.Any("ip_address", wc.WebSocket.RemoteAddr()))
return
}
authTicker.Stop()
}
}
}
// writeMessageBuf is a helper utility that wraps the write to the socket
// along with setting the write deadline.
func (wc *WebConn) writeMessageBuf(msgType int, data []byte) error {
wc.WebSocket.SetWriteDeadline(time.Now().Add(writeWaitTime))
return wc.WebSocket.WriteMessage(msgType, data)
}
func (wc *WebConn) writeMessage(msg *model.WebSocketEvent) error {
// We don't use the encoder from the write pump because it's unwieldy to pass encoders
// around, and this is only called during initialization of the webConn.
var buf bytes.Buffer
err := msg.Encode(json.NewEncoder(&buf))
if err != nil {
mlog.Warn("Error in encoding websocket message", mlog.Err(err))
return nil
}
wc.Sequence++
return wc.writeMessageBuf(websocket.TextMessage, buf.Bytes())
}
// addToDeadQueue appends a message to the dead queue.
func (wc *WebConn) addToDeadQueue(msg *model.WebSocketEvent) {
wc.deadQueue[wc.deadQueuePointer] = msg
wc.deadQueuePointer = (wc.deadQueuePointer + 1) % deadQueueSize
}
// hasMsgLoss indicates whether the next wanted sequence is right after
// the latest element in the dead queue, which would mean there is no message loss.
func (wc *WebConn) hasMsgLoss() bool {
var index int
// deadQueuePointer = 0 means either no msg written or the pointer
// has rolled over to its starting position.
if wc.deadQueuePointer == 0 {
// If last entry is nil, it means no msg is written.
if wc.deadQueue[deadQueueSize-1] == nil {
return false
}
// If it's not nil, that means it has rolled over to start, and we
// check the last position.
index = deadQueueSize - 1
} else { // deadQueuePointer != 0 means it's somewhere in the middle.
index = wc.deadQueuePointer - 1
}
if wc.deadQueue[index].GetSequence() == wc.Sequence-1 {
return false
}
return true
}
// isInDeadQueue checks whether a given sequence number is in the dead queue or not.
// And if it is, it returns that index.
func (wc *WebConn) isInDeadQueue(seq int64) (bool, int) {
// Can be optimized to traverse backwards from deadQueuePointer
// Hopefully, traversing 128 elements is not too much overhead.
for i := 0; i < deadQueueSize; i++ {
elem := wc.deadQueue[i]
if elem == nil {
return false, 0
}
if elem.GetSequence() == seq {
return true, i
}
}
return false, 0
}
func (wc *WebConn) clearDeadQueue() {
for i := 0; i < deadQueueSize; i++ {
if wc.deadQueue[i] == nil {
break
}
wc.deadQueue[i] = nil
}
wc.deadQueuePointer = 0
}
// drainDeadQueue will write all messages from a given index to the socket.
// It is called with the assumption that the item with wc.Sequence is present
// in it, because otherwise it would have been cleared from WebConn.
func (wc *WebConn) drainDeadQueue(index int) error {
if wc.deadQueue[0] == nil {
// Empty queue
return nil
}
// This means pointer hasn't rolled over.
if wc.deadQueue[wc.deadQueuePointer] == nil {
// Clear till the end of queue.
for i := index; i < wc.deadQueuePointer; i++ {
if err := wc.writeMessage(wc.deadQueue[i]); err != nil {
return err
}
}
return nil
}
// We go on until next sequence number is smaller than previous one.
// Which means it has rolled over.
currPtr := index
for {
if err := wc.writeMessage(wc.deadQueue[currPtr]); err != nil {
return err
}
oldSeq := wc.deadQueue[currPtr].GetSequence() // TODO: possibly move this
currPtr = (currPtr + 1) % deadQueueSize // to for loop condition
newSeq := wc.deadQueue[currPtr].GetSequence()
if oldSeq > newSeq {
break
}
}
return nil
}
// InvalidateCache resets all internal data of the WebConn.
func (wc *WebConn) InvalidateCache() {
wc.allChannelMembers = nil
wc.lastAllChannelMembersTime = 0
wc.SetSession(nil)
wc.SetSessionExpiresAt(0)
}
// IsAuthenticated returns whether the given WebConn is authenticated or not.
func (wc *WebConn) IsAuthenticated() bool {
// Check the expiry to see if we need to check for a new session
if wc.GetSessionExpiresAt() < model.GetMillis() {
if wc.GetSessionToken() == "" {
return false
}
session, err := wc.Suite.GetSession(wc.GetSessionToken())
if err != nil {
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
mlog.Debug("Invalid session.", mlog.Err(err))
} else {
mlog.Error("Could not get session", mlog.String("session_token", wc.GetSessionToken()), mlog.Err(err))
}
wc.SetSessionToken("")
wc.SetSession(nil)
wc.SetSessionExpiresAt(0)
return false
}
wc.SetSession(session)
wc.SetSessionExpiresAt(session.ExpiresAt)
}
return true
}
func (wc *WebConn) createHelloMessage() *model.WebSocketEvent {
ee := wc.Platform.LicenseManager() != nil
msg := model.NewWebSocketEvent(model.WebsocketEventHello, "", "", wc.UserId, nil, "")
msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion,
model.BuildNumber,
wc.Platform.ClientConfigHash(),
ee))
msg.Add("connection_id", wc.connectionID.Load())
return msg
}
func (wc *WebConn) ShouldSendEventToGuest(msg *model.WebSocketEvent) bool {
var userID string
var canSee bool
switch msg.EventType() {
case model.WebsocketEventUserUpdated:
user, ok := msg.GetData()["user"].(*model.User)
if !ok {
mlog.Debug("webhub.shouldSendEvent: user not found in message", mlog.Any("user", msg.GetData()["user"]))
return false
}
userID = user.Id
case model.WebsocketEventNewUser:
userID = msg.GetData()["user_id"].(string)
default:
return true
}
canSee, err := wc.Suite.UserCanSeeOtherUser(wc.UserId, userID)
if err != nil {
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
return false
}
return canSee
}
// ShouldSendEvent returns whether the message should be sent or not.
func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
// IMPORTANT: Do not send event if WebConn does not have a session
if !wc.IsAuthenticated() {
return false
}
// When the pump starts to get slow we'll drop non-critical
// messages. We should skip those frames before they are
// queued to wc.send buffered channel.
if len(wc.send) >= sendSlowWarn {
switch msg.EventType() {
case model.WebsocketEventTyping,
model.WebsocketEventStatusChange,
model.WebsocketEventChannelViewed:
if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold {
mlog.Warn(
"websocket.slow: dropping message",
mlog.String("user_id", wc.UserId),
mlog.String("type", msg.EventType()),
)
// Reset timer to now.
wc.lastLogTimeSlow = time.Now()
}
return false
}
}
// If the event contains sanitized data, only send to users that don't have permission to
// see sensitive data. Prevents admin clients from receiving events with bad data
var hasReadPrivateDataPermission *bool
if msg.GetBroadcast().ContainsSanitizedData {
hasReadPrivateDataPermission = model.NewBool(wc.Suite.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PermissionManageSystem.Id))
if *hasReadPrivateDataPermission {
return false
}
}
// If the event contains sensitive data, only send to users with permission to see it
if msg.GetBroadcast().ContainsSensitiveData {
if hasReadPrivateDataPermission == nil {
hasReadPrivateDataPermission = model.NewBool(wc.Suite.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PermissionManageSystem.Id))
}
if !*hasReadPrivateDataPermission {
return false
}
}
// If the event is destined to a specific connection
if msg.GetBroadcast().ConnectionId != "" {
return wc.GetConnectionID() == msg.GetBroadcast().ConnectionId
}
if wc.GetConnectionID() == msg.GetBroadcast().OmitConnectionId {
return false
}
// If the event is destined to a specific user
if msg.GetBroadcast().UserId != "" {
return wc.UserId == msg.GetBroadcast().UserId
}
// if the user is omitted don't send the message
if len(msg.GetBroadcast().OmitUsers) > 0 {
if _, ok := msg.GetBroadcast().OmitUsers[wc.UserId]; ok {
return false
}
}
// Only report events to users who are in the channel for the event
if msg.GetBroadcast().ChannelId != "" {
if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime {
wc.allChannelMembers = nil
wc.lastAllChannelMembersTime = 0
}
if wc.allChannelMembers == nil {
result, err := wc.Platform.Store.Channel().GetAllChannelMembersForUser(wc.UserId, false, false)
if err != nil {
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
return false
}
wc.allChannelMembers = result
wc.lastAllChannelMembersTime = model.GetMillis()
}
if _, ok := wc.allChannelMembers[msg.GetBroadcast().ChannelId]; ok {
return true
}
return false
}
// Only report events to users who are in the team for the event
if msg.GetBroadcast().TeamId != "" {
return wc.isMemberOfTeam(msg.GetBroadcast().TeamId)
}
if wc.GetSession().Props[model.SessionPropIsGuest] == "true" {
return wc.ShouldSendEventToGuest(msg)
}
return true
}
// IsMemberOfTeam returns whether the user of the WebConn
// is a member of the given teamID or not.
func (wc *WebConn) isMemberOfTeam(teamID string) bool {
currentSession := wc.GetSession()
if currentSession == nil || currentSession.Token == "" {
session, err := wc.Suite.GetSession(wc.GetSessionToken())
if err != nil {
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
mlog.Debug("Invalid session.", mlog.Err(err))
} else {
mlog.Error("Could not get session", mlog.String("session_token", wc.GetSessionToken()), mlog.Err(err))
}
return false
}
wc.SetSession(session)
currentSession = session
}
return currentSession.GetTeamByTeamId(teamID) != nil
}
func (wc *WebConn) logSocketErr(source string, err error) {
// browsers will appear as CloseNoStatusReceived
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
mlog.Debug(source+": client side closed socket", mlog.String("user_id", wc.UserId))
} else {
mlog.Debug(source+": closing websocket", mlog.String("user_id", wc.UserId), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"hash/maphash"
"runtime"
"runtime/debug"
"strconv"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
broadcastQueueSize = 4096
inactiveConnReaperInterval = 5 * time.Minute
)
type SuiteIFace interface {
GetSession(token string) (*model.Session, *model.AppError)
RolesGrantPermission(roleNames []string, permissionId string) bool
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
}
type webConnActivityMessage struct {
userID string
sessionToken string
activityAt int64
}
type webConnDirectMessage struct {
conn *WebConn
msg model.WebSocketMessage
}
type webConnSessionMessage struct {
userID string
sessionToken string
isRegistered chan bool
}
type webConnCheckMessage struct {
userID string
connectionID string
result chan *CheckConnResult
}
// Hub is the central place to manage all websocket connections in the server.
// It handles different websocket events and sending messages to individual
// user connections.
type Hub struct {
// connectionCount should be kept first.
// See https://github.com/mattermost/mattermost-server/pull/7281
connectionCount int64
platform *PlatformService
connectionIndex int
register chan *WebConn
unregister chan *WebConn
broadcast chan *model.WebSocketEvent
stop chan struct{}
didStop chan struct{}
invalidateUser chan string
activity chan *webConnActivityMessage
directMsg chan *webConnDirectMessage
explicitStop bool
checkRegistered chan *webConnSessionMessage
checkConn chan *webConnCheckMessage
}
// newWebHub creates a new Hub.
func newWebHub(ps *PlatformService) *Hub {
return &Hub{
platform: ps,
register: make(chan *WebConn),
unregister: make(chan *WebConn),
broadcast: make(chan *model.WebSocketEvent, broadcastQueueSize),
stop: make(chan struct{}),
didStop: make(chan struct{}),
invalidateUser: make(chan string),
activity: make(chan *webConnActivityMessage),
directMsg: make(chan *webConnDirectMessage),
checkRegistered: make(chan *webConnSessionMessage),
checkConn: make(chan *webConnCheckMessage),
}
}
// hubStart starts all the hubs.
func (ps *PlatformService) hubStart() {
// Total number of hubs is twice the number of CPUs.
numberOfHubs := runtime.NumCPU() * 2
ps.logger.Info("Starting websocket hubs", mlog.Int("number_of_hubs", numberOfHubs))
hubs := make([]*Hub, numberOfHubs)
for i := 0; i < numberOfHubs; i++ {
hubs[i] = newWebHub(ps)
hubs[i].connectionIndex = i
hubs[i].Start()
}
// Assigning to the hubs slice without any mutex is fine because it is only assigned once
// during the start of the program and always read from after that.
ps.hubs = hubs
}
func (ps *PlatformService) InvalidateCacheForWebhook(webhookID string) {
ps.Store.Webhook().InvalidateWebhookCache(webhookID)
}
// HubStop stops all the hubs.
func (ps *PlatformService) HubStop() {
ps.logger.Info("stopping websocket hub connections")
for _, hub := range ps.hubs {
hub.Stop()
}
}
// GetHubForUserId returns the hub for a given user id.
func (ps *PlatformService) GetHubForUserId(userID string) *Hub {
if len(ps.hubs) == 0 {
return nil
}
// TODO: check if caching the userID -> hub mapping
// is worth the memory tradeoff.
// https://mattermost.atlassian.net/browse/MM-26629.
var hash maphash.Hash
hash.SetSeed(ps.hashSeed)
hash.Write([]byte(userID))
index := hash.Sum64() % uint64(len(ps.hubs))
return ps.hubs[int(index)]
}
// HubRegister registers a connection to a hub.
func (ps *PlatformService) HubRegister(webConn *WebConn) {
hub := ps.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := ps.metricsIFace; metrics != nil {
metrics.IncrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Register(webConn)
}
}
// HubUnregister unregisters a connection from a hub.
func (ps *PlatformService) HubUnregister(webConn *WebConn) {
hub := ps.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := ps.metricsIFace; metrics != nil {
metrics.DecrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Unregister(webConn)
}
}
func (ps *PlatformService) InvalidateCacheForChannel(channel *model.Channel) {
ps.Store.Channel().InvalidateChannel(channel.Id)
ps.invalidateCacheForChannelByNameSkipClusterSend(channel.TeamId, channel.Name)
if ps.clusterIFace != nil {
nameMsg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForChannelByName,
SendType: model.ClusterSendBestEffort,
Props: make(map[string]string),
}
nameMsg.Props["name"] = channel.Name
if channel.TeamId == "" {
nameMsg.Props["id"] = "dm"
} else {
nameMsg.Props["id"] = channel.TeamId
}
ps.clusterIFace.SendClusterMessage(nameMsg)
}
}
func (ps *PlatformService) InvalidateCacheForChannelMembers(channelID string) {
ps.Store.User().InvalidateProfilesInChannelCache(channelID)
ps.Store.Channel().InvalidateMemberCount(channelID)
ps.Store.Channel().InvalidateGuestCount(channelID)
}
func (ps *PlatformService) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
ps.invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForChannelMembersNotifyProps,
SendType: model.ClusterSendBestEffort,
Data: []byte(channelID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) InvalidateCacheForChannelPosts(channelID string) {
ps.Store.Channel().InvalidatePinnedPostCount(channelID)
ps.Store.Post().InvalidateLastPostTimeCache(channelID)
}
func (ps *PlatformService) InvalidateCacheForUser(userID string) {
ps.InvalidateCacheForUserSkipClusterSend(userID)
ps.Store.User().InvalidateProfilesInChannelCacheByUser(userID)
ps.Store.User().InvalidateProfileCacheForUser(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUser,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) InvalidateCacheForUserTeams(userID string) {
ps.invalidateWebConnSessionCacheForUser(userID)
ps.Store.Team().InvalidateAllTeamIdsForUser(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUserTeams,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
// UpdateWebConnUserActivity sets the LastUserActivityAt of the hub for the given session.
func (ps *PlatformService) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
hub := ps.GetHubForUserId(session.UserId)
if hub != nil {
hub.UpdateActivity(session.UserId, session.Token, activityAt)
}
}
// SessionIsRegistered determines if a specific session has been registered
func (ps *PlatformService) SessionIsRegistered(session model.Session) bool {
hub := ps.GetHubForUserId(session.UserId)
if hub != nil {
return hub.IsRegistered(session.UserId, session.Token)
}
return false
}
func (ps *PlatformService) CheckWebConn(userID, connectionID string) *CheckConnResult {
hub := ps.GetHubForUserId(userID)
if hub != nil {
return hub.CheckConn(userID, connectionID)
}
return nil
}
// Register registers a connection to the hub.
func (h *Hub) Register(webConn *WebConn) {
select {
case h.register <- webConn:
case <-h.stop:
}
}
// Unregister unregisters a connection from the hub.
func (h *Hub) Unregister(webConn *WebConn) {
select {
case h.unregister <- webConn:
case <-h.stop:
}
}
// Determines if a user's session is registered a connection from the hub.
func (h *Hub) IsRegistered(userID, sessionToken string) bool {
ws := &webConnSessionMessage{
userID: userID,
sessionToken: sessionToken,
isRegistered: make(chan bool),
}
select {
case h.checkRegistered <- ws:
return <-ws.isRegistered
case <-h.stop:
}
return false
}
func (h *Hub) CheckConn(userID, connectionID string) *CheckConnResult {
req := &webConnCheckMessage{
userID: userID,
connectionID: connectionID,
result: make(chan *CheckConnResult),
}
select {
case h.checkConn <- req:
return <-req.result
case <-h.stop:
}
return nil
}
// Broadcast broadcasts the message to all connections in the hub.
func (h *Hub) Broadcast(message *model.WebSocketEvent) {
// XXX: The hub nil check is because of the way we setup our tests. We call
// `app.NewServer()` which returns a server, but only after that, we call
// `wsapi.Init()` to initialize the hub. But in the `NewServer` call
// itself proceeds to broadcast some messages happily. This needs to be
// fixed once the wsapi cyclic dependency with server/app goes away.
// And possibly, we can look into doing the hub initialization inside
// NewServer itself.
if h != nil && message != nil {
if metrics := h.platform.metricsIFace; metrics != nil {
metrics.IncrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
select {
case h.broadcast <- message:
case <-h.stop:
}
}
}
// InvalidateUser invalidates the cache for the given user.
func (h *Hub) InvalidateUser(userID string) {
select {
case h.invalidateUser <- userID:
case <-h.stop:
}
}
// UpdateActivity sets the LastUserActivityAt field for the connection
// of the user.
func (h *Hub) UpdateActivity(userID, sessionToken string, activityAt int64) {
select {
case h.activity <- &webConnActivityMessage{
userID: userID,
sessionToken: sessionToken,
activityAt: activityAt,
}:
case <-h.stop:
}
}
// SendMessage sends the given message to the given connection.
func (h *Hub) SendMessage(conn *WebConn, msg model.WebSocketMessage) {
select {
case h.directMsg <- &webConnDirectMessage{
conn: conn,
msg: msg,
}:
case <-h.stop:
}
}
// Stop stops the hub.
func (h *Hub) Stop() {
close(h.stop)
<-h.didStop
}
// Start starts the hub.
func (h *Hub) Start() {
var doStart func()
var doRecoverableStart func()
var doRecover func()
doStart = func() {
mlog.Debug("Hub is starting", mlog.Int("index", h.connectionIndex))
ticker := time.NewTicker(inactiveConnReaperInterval)
defer ticker.Stop()
connIndex := newHubConnectionIndex(inactiveConnReaperInterval)
for {
select {
case webSessionMessage := <-h.checkRegistered:
conns := connIndex.ForUser(webSessionMessage.userID)
var isRegistered bool
for _, conn := range conns {
if !conn.active {
continue
}
if conn.GetSessionToken() == webSessionMessage.sessionToken {
isRegistered = true
}
}
webSessionMessage.isRegistered <- isRegistered
case req := <-h.checkConn:
var res *CheckConnResult
conn := connIndex.RemoveInactiveByConnectionID(req.userID, req.connectionID)
if conn != nil {
res = &CheckConnResult{
ConnectionID: req.connectionID,
UserID: req.userID,
ActiveQueue: conn.send,
DeadQueue: conn.deadQueue,
DeadQueuePointer: conn.deadQueuePointer,
ReuseCount: conn.reuseCount + 1,
}
}
req.result <- res
case <-ticker.C:
connIndex.RemoveInactiveConnections()
case webConn := <-h.register:
// Mark the current one as active.
// There is no need to check if it was inactive or not,
// we will anyways need to make it active.
webConn.active = true
connIndex.Add(webConn)
atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive()))
if webConn.IsAuthenticated() && webConn.reuseCount == 0 {
// The hello message should only be sent when the reuseCount is 0.
// i.e in server restart, or long timeout, or fresh connection case.
// In case of seq number not found in dead queue, it is handled by
// the webconn write pump.
webConn.send <- webConn.createHelloMessage()
}
case webConn := <-h.unregister:
// If already removed (via queue full), then removing again becomes a noop.
// But if not removed, mark inactive.
webConn.active = false
atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive()))
if webConn.UserId == "" {
continue
}
conns := connIndex.ForUser(webConn.UserId)
if len(conns) == 0 || areAllInactive(conns) {
h.platform.Go(func() {
h.platform.SetStatusOffline(webConn.UserId, false)
})
continue
}
var latestActivity int64 = 0
for _, conn := range conns {
if !conn.active {
continue
}
if conn.lastUserActivityAt > latestActivity {
latestActivity = conn.lastUserActivityAt
}
}
if h.platform.isUserAway(latestActivity) {
h.platform.Go(func() {
h.platform.SetStatusLastActivityAt(webConn.UserId, latestActivity)
})
}
case userID := <-h.invalidateUser:
for _, webConn := range connIndex.ForUser(userID) {
webConn.InvalidateCache()
}
case activity := <-h.activity:
for _, webConn := range connIndex.ForUser(activity.userID) {
if !webConn.active {
continue
}
if webConn.GetSessionToken() == activity.sessionToken {
webConn.lastUserActivityAt = activity.activityAt
}
}
case directMsg := <-h.directMsg:
if !connIndex.Has(directMsg.conn) {
continue
}
select {
case directMsg.conn.send <- directMsg.msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", directMsg.conn.UserId))
close(directMsg.conn.send)
connIndex.Remove(directMsg.conn)
}
case msg := <-h.broadcast:
if metrics := h.platform.metricsIFace; metrics != nil {
metrics.DecrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
msg = msg.PrecomputeJSON()
broadcast := func(webConn *WebConn) {
if !connIndex.Has(webConn) {
return
}
if webConn.ShouldSendEvent(msg) {
select {
case webConn.send <- msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", webConn.UserId))
close(webConn.send)
connIndex.Remove(webConn)
}
}
}
if connID := msg.GetBroadcast().ConnectionId; connID != "" {
if webConn := connIndex.byConnectionId[connID]; webConn != nil {
broadcast(webConn)
continue
}
} else if msg.GetBroadcast().UserId != "" {
candidates := connIndex.ForUser(msg.GetBroadcast().UserId)
for _, webConn := range candidates {
broadcast(webConn)
}
continue
}
candidates := connIndex.All()
for webConn := range candidates {
broadcast(webConn)
}
case <-h.stop:
for webConn := range connIndex.All() {
webConn.Close()
h.platform.SetStatusOffline(webConn.UserId, false)
}
h.explicitStop = true
close(h.didStop)
return
}
}
}
doRecoverableStart = func() {
defer doRecover()
doStart()
}
doRecover = func() {
if !h.explicitStop {
if r := recover(); r != nil {
mlog.Error("Recovering from Hub panic.", mlog.Any("panic", r))
} else {
mlog.Error("Webhub stopped unexpectedly. Recovering.")
}
mlog.Error(string(debug.Stack()))
go doRecoverableStart()
}
}
go doRecoverableStart()
}
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
// It requires 3 functionalities which need to be very fast:
// - check if a connection exists or not.
// - get all connections for a given userID.
// - get all connections.
type hubConnectionIndex struct {
// byUserId stores the list of connections for a given userID
byUserId map[string][]*WebConn
// byConnection serves the dual purpose of storing the index of the webconn
// in the value of byUserId map, and also to get all connections.
byConnection map[*WebConn]int
byConnectionId map[string]*WebConn
// staleThreshold is the limit beyond which inactive connections
// will be deleted.
staleThreshold time.Duration
}
func newHubConnectionIndex(interval time.Duration) *hubConnectionIndex {
return &hubConnectionIndex{
byUserId: make(map[string][]*WebConn),
byConnection: make(map[*WebConn]int),
byConnectionId: make(map[string]*WebConn),
staleThreshold: interval,
}
}
func (i *hubConnectionIndex) Add(wc *WebConn) {
i.byUserId[wc.UserId] = append(i.byUserId[wc.UserId], wc)
i.byConnection[wc] = len(i.byUserId[wc.UserId]) - 1
i.byConnectionId[wc.GetConnectionID()] = wc
}
func (i *hubConnectionIndex) Remove(wc *WebConn) {
wc.Platform.ReturnSessionToPool(wc.GetSession())
userConnIndex, ok := i.byConnection[wc]
if !ok {
return
}
// get the conn slice.
userConnections := i.byUserId[wc.UserId]
// get the last connection.
last := userConnections[len(userConnections)-1]
// set the slot that we are trying to remove to be the last connection.
userConnections[userConnIndex] = last
// remove the last connection pointer from slice.
userConnections[len(userConnections)-1] = nil
// remove the last connection from the slice.
i.byUserId[wc.UserId] = userConnections[:len(userConnections)-1]
// set the index of the connection that was moved to the new index.
i.byConnection[last] = userConnIndex
delete(i.byConnection, wc)
delete(i.byConnectionId, wc.GetConnectionID())
}
func (i *hubConnectionIndex) Has(wc *WebConn) bool {
_, ok := i.byConnection[wc]
return ok
}
// ForUser returns all connections for a user ID.
func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
return i.byUserId[id]
}
// All returns the full webConn index.
func (i *hubConnectionIndex) All() map[*WebConn]int {
return i.byConnection
}
// RemoveInactiveByConnectionID removes an inactive connection for the given
// userID and connectionID.
func (i *hubConnectionIndex) RemoveInactiveByConnectionID(userID, connectionID string) *WebConn {
// To handle empty sessions.
if userID == "" {
return nil
}
for _, conn := range i.ForUser(userID) {
if conn.GetConnectionID() == connectionID && !conn.active {
i.Remove(conn)
return conn
}
}
return nil
}
// RemoveInactiveConnections removes all inactive connections whose lastUserActivityAt
// exceeded staleThreshold.
func (i *hubConnectionIndex) RemoveInactiveConnections() {
now := model.GetMillis()
for conn := range i.byConnection {
if !conn.active && now-conn.lastUserActivityAt > i.staleThreshold.Milliseconds() {
i.Remove(conn)
}
}
}
// AllActive returns the number of active connections.
// This is only called during register/unregister so we can take
// a bit of perf hit here.
func (i *hubConnectionIndex) AllActive() int {
cnt := 0
for conn := range i.byConnection {
if conn.active {
cnt++
}
}
return cnt
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type webSocketHandler interface {
ServeWebSocket(*WebConn, *model.WebSocketRequest)
}
type WebSocketRouter struct {
handlers map[string]webSocketHandler
}
func (wr *WebSocketRouter) Handle(action string, handler webSocketHandler) {
wr.handlers[action] = handler
}
func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
if r.Action == "" {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "", http.StatusBadRequest)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
if r.Seq <= 0 {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "", http.StatusBadRequest)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
if r.Action == model.WebsocketAuthenticationChallenge {
if conn.GetSessionToken() != "" {
return
}
token, ok := r.Data["token"].(string)
if !ok {
conn.WebSocket.Close()
return
}
session, err := conn.Suite.GetSession(token)
if err != nil {
conn.WebSocket.Close()
return
}
conn.SetSession(session)
conn.SetSessionToken(session.Token)
conn.UserId = session.UserId
conn.Platform.HubRegister(conn)
conn.Platform.Go(func() {
conn.Platform.SetStatusOnline(session.UserId, false)
conn.Platform.UpdateLastActivityAtIfNeeded(*session)
})
resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, nil)
hub := conn.Platform.GetHubForUserId(conn.UserId)
if hub == nil {
return
}
hub.SendMessage(conn, resp)
return
}
if !conn.IsAuthenticated() {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "", http.StatusUnauthorized)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
handler, ok := wr.handlers[r.Action]
if !ok {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "", http.StatusInternalServerError)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
handler.ServeWebSocket(conn, r)
}
func returnWebSocketError(ps *PlatformService, conn *WebConn, r *model.WebSocketRequest, err *model.AppError) {
logF := mlog.Error
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
logF = mlog.Debug
}
logF(
"websocket routing error.",
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("system_message", err.SystemMessage(i18n.T)),
mlog.Err(err),
)
hub := ps.GetHubForUserId(conn.UserId)
if hub == nil {
return
}
err.DetailedError = ""
errorResp := model.NewWebSocketError(r.Seq, err)
hub.SendMessage(conn, errorResp)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"github.com/blang/semver"
"github.com/gorilla/mux"
svg "github.com/h2non/go-is-svg"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/marketplace"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const prepackagedPluginsDir = "prepackaged_plugins"
type pluginSignaturePath struct {
pluginID string
path string
signaturePath string
}
// Ensure routerService implements `product.RouterService`
var _ product.RouterService = (*routerService)(nil)
type routerService struct {
mu sync.Mutex
routerMap map[string]*mux.Router
}
func newRouterService() *routerService {
return &routerService{
routerMap: make(map[string]*mux.Router),
}
}
func (rs *routerService) RegisterRouter(productID string, sub *mux.Router) {
rs.mu.Lock()
defer rs.mu.Unlock()
rs.routerMap[productID] = sub
}
func (rs *routerService) getHandler(productID string) (http.Handler, bool) {
handler, ok := rs.routerMap[productID]
return handler, ok
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
func (ch *Channels) GetPluginsEnvironment() *plugin.Environment {
if !*ch.cfgSvc.Config().PluginSettings.Enable {
return nil
}
ch.pluginsLock.RLock()
defer ch.pluginsLock.RUnlock()
return ch.pluginsEnvironment
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
func (a *App) GetPluginsEnvironment() *plugin.Environment {
return a.ch.GetPluginsEnvironment()
}
func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) {
ch.pluginsLock.Lock()
defer ch.pluginsLock.Unlock()
ch.pluginsEnvironment = pluginsEnvironment
ch.srv.Platform().SetPluginsEnvironment(ch)
}
func (ch *Channels) syncPluginsActiveState() {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment == nil {
return
}
config := ch.cfgSvc.Config().PluginSettings
if *config.Enable {
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
ch.srv.Log().Error("Unable to get available plugins", mlog.Err(err))
return
}
// Determine which plugins need to be activated or deactivated.
disabledPlugins := []*model.BundleInfo{}
enabledPlugins := []*model.BundleInfo{}
for _, plugin := range availablePlugins {
pluginID := plugin.Manifest.Id
pluginEnabled := false
if state, ok := config.PluginStates[pluginID]; ok {
pluginEnabled = state.Enable
}
if hasOverride, value := ch.getPluginStateOverride(pluginID); hasOverride {
pluginEnabled = value
}
if pluginEnabled {
enabledPlugins = append(enabledPlugins, plugin)
} else {
disabledPlugins = append(disabledPlugins, plugin)
}
}
// Concurrently activate/deactivate each plugin appropriately.
var wg sync.WaitGroup
// Deactivate any plugins that have been disabled.
for _, plugin := range disabledPlugins {
wg.Add(1)
go func(plugin *model.BundleInfo) {
defer wg.Done()
deactivated := pluginsEnvironment.Deactivate(plugin.Manifest.Id)
if deactivated && plugin.Manifest.HasClient() {
message := model.NewWebSocketEvent(model.WebsocketEventPluginDisabled, "", "", "", nil, "")
message.Add("manifest", plugin.Manifest.ClientManifest())
ch.srv.platform.Publish(message)
}
}(plugin)
}
// Activate any plugins that have been enabled
for _, plugin := range enabledPlugins {
wg.Add(1)
go func(plugin *model.BundleInfo) {
defer wg.Done()
pluginID := plugin.Manifest.Id
updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID)
if err != nil {
plugin.WrapLogger(ch.srv.Log()).Error("Unable to activate plugin", mlog.Err(err))
return
}
if activated {
// Notify all cluster clients if ready
if err := ch.notifyPluginEnabled(updatedManifest); err != nil {
ch.srv.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err))
}
}
}(plugin)
}
wg.Wait()
} else { // If plugins are disabled, shutdown plugins.
pluginsEnvironment.Shutdown()
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
}
func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
return NewPluginAPI(a, c, manifest)
}
func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string) {
a.ch.initPlugins(c, pluginDir, webappPluginDir)
}
func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
defer func() {
ch.srv.Platform().SetPluginsEnvironment(ch)
}()
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment != nil || !*ch.cfgSvc.Config().PluginSettings.Enable {
ch.syncPluginsActiveState()
if pluginsEnvironment != nil {
pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck)
}
return
}
ch.srv.Log().Info("Starting up plugins")
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
newAPIFunc := func(manifest *model.Manifest) plugin.API {
return New(ServerConnector(ch)).NewPluginAPI(c, manifest)
}
env, err := plugin.NewEnvironment(
newAPIFunc,
NewDriverImpl(ch.srv),
pluginDir,
webappPluginDir,
*ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM,
ch.srv.Log(),
ch.srv.GetMetrics(),
)
if err != nil {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
ch.pluginsLock.Lock()
ch.pluginsEnvironment = env
ch.pluginsLock.Unlock()
ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck)
if err := ch.syncPlugins(); err != nil {
mlog.Error("Failed to sync plugins from the file store", mlog.Err(err))
}
plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir)
pluginsEnvironment = ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
mlog.Info("Plugins environment not found, server is likely shutting down")
return
}
pluginsEnvironment.SetPrepackagedPlugins(plugins)
ch.installFeatureFlagPlugins()
// Sync plugin active state when config changes. Also notify plugins.
ch.pluginsLock.Lock()
ch.RemoveConfigListener(ch.pluginConfigListenerID)
ch.pluginConfigListenerID = ch.AddConfigListener(func(old, new *model.Config) {
// If plugin status remains unchanged, only then run this.
// Because (*App).InitPlugins is already run as a config change hook.
if *old.PluginSettings.Enable == *new.PluginSettings.Enable {
ch.installFeatureFlagPlugins()
ch.syncPluginsActiveState()
}
ch.RunMultiHook(func(hooks plugin.Hooks) bool {
if err := hooks.OnConfigurationChange(); err != nil {
ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err))
}
return true
}, plugin.OnConfigurationChangeID)
})
ch.pluginsLock.Unlock()
ch.syncPluginsActiveState()
}
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
func (a *App) SyncPlugins() *model.AppError {
return a.ch.syncPlugins()
}
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
func (ch *Channels) syncPlugins() *model.AppError {
mlog.Info("Syncing plugins from the file store")
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("SyncPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("SyncPlugins", "app.plugin.sync.read_local_folder.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var wg sync.WaitGroup
for _, plugin := range availablePlugins {
wg.Add(1)
go func(pluginID string) {
defer wg.Done()
// Only handle managed plugins with .filestore flag file.
_, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName))
if os.IsNotExist(err) {
mlog.Warn("Skipping sync for unmanaged plugin", mlog.String("plugin_id", pluginID))
} else if err != nil {
mlog.Error("Skipping sync for plugin after failure to check if managed", mlog.String("plugin_id", pluginID), mlog.Err(err))
} else {
mlog.Debug("Removing local installation of managed plugin before sync", mlog.String("plugin_id", pluginID))
if err := ch.removePluginLocally(pluginID); err != nil {
mlog.Error("Failed to remove local installation of managed plugin before sync", mlog.String("plugin_id", pluginID), mlog.Err(err))
}
}
}(plugin.Manifest.Id)
}
wg.Wait()
// Install plugins from the file store.
pluginSignaturePathMap, appErr := ch.getPluginsFromFolder()
if appErr != nil {
return appErr
}
for _, plugin := range pluginSignaturePathMap {
wg.Add(1)
go func(plugin *pluginSignaturePath) {
defer wg.Done()
reader, appErr := ch.srv.fileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
defer reader.Close()
var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return
}
defer signature.Close()
}
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path))
if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err))
}
}(plugin)
}
wg.Wait()
return nil
}
func (ch *Channels) ShutDownPlugins() {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment == nil {
return
}
mlog.Info("Shutting down plugins")
pluginsEnvironment.Shutdown()
ch.RemoveConfigListener(ch.pluginConfigListenerID)
ch.pluginConfigListenerID = ""
// Acquiring lock manually before cleaning up PluginsEnvironment.
ch.pluginsLock.Lock()
defer ch.pluginsLock.Unlock()
if ch.pluginsEnvironment == pluginsEnvironment {
ch.pluginsEnvironment = nil
} else {
mlog.Warn("Another PluginsEnvironment detected while shutting down plugins.")
}
}
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins := pluginsEnvironment.Active()
manifests := make([]*model.Manifest, len(plugins))
for i, plugin := range plugins {
manifests[i] = plugin.Manifest
}
return manifests, nil
}
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
// activation if inactive anywhere in the cluster.
// Notifies cluster peers through config change.
func (a *App) EnablePlugin(id string) *model.AppError {
return a.ch.enablePlugin(id)
}
func (ch *Channels) enablePlugin(id string) *model.AppError {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
id = strings.ToLower(id)
var manifest *model.Manifest
for _, p := range availablePlugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
ch.cfgSvc.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
})
// This call will implicitly invoke SyncPluginsActiveState which will activate enabled plugins.
if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil {
if err.Id == "ent.cluster.save_config.error" {
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
}
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
// Notifies cluster peers through config change.
func (a *App) DisablePlugin(id string) *model.AppError {
appErr := a.ch.disablePlugin(id)
if appErr != nil {
return appErr
}
return nil
}
func (ch *Channels) disablePlugin(id string) *model.AppError {
// find all collectionTypes registered by plugin
for collectionTypeToRemove, existingPluginId := range ch.collectionTypes {
if existingPluginId != id {
continue
}
// find all topicTypes for existing collectionType
for topicTypeToRemove, existingCollectionType := range ch.topicTypes {
if existingCollectionType == collectionTypeToRemove {
delete(ch.topicTypes, topicTypeToRemove)
}
}
delete(ch.collectionTypes, collectionTypeToRemove)
}
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
id = strings.ToLower(id)
var manifest *model.Manifest
for _, p := range availablePlugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
ch.cfgSvc.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
})
ch.unregisterPluginCommands(id)
// This call will implicitly invoke SyncPluginsActiveState which will deactivate disabled plugins.
if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
for _, plugin := range availablePlugins {
if plugin.Manifest == nil {
continue
}
info := &model.PluginInfo{
Manifest: *plugin.Manifest,
}
if pluginsEnvironment.IsActive(plugin.Manifest.Id) {
resp.Active = append(resp.Active, info)
} else {
resp.Inactive = append(resp.Inactive, info)
}
}
return resp, nil
}
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
// and plugins that are installed locally.
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
plugins := map[string]*model.MarketplacePlugin{}
if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly {
p, appErr := a.getRemotePlugins()
if appErr != nil {
return nil, appErr
}
plugins = p
}
if !filter.RemoteOnly {
// Some plugin don't work on cloud. The remote Marketplace is aware of this fact,
// but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins
// shouldn't be shown in the Marketplace modal.
// This is a short term fix. The long term solution is to have a separate set of
// prepacked plugins for cloud: https://mattermost.atlassian.net/browse/MM-31331.
license := a.Srv().License()
if license == nil || !license.IsCloud() {
appErr := a.mergePrepackagedPlugins(plugins)
if appErr != nil {
return nil, appErr
}
}
appErr := a.mergeLocalPlugins(plugins)
if appErr != nil {
return nil, appErr
}
}
// Filter plugins.
var result []*model.MarketplacePlugin
for _, p := range plugins {
if pluginMatchesFilter(p.Manifest, filter.Filter) {
result = append(result, p)
}
}
// Sort result alphabetically.
sort.SliceStable(result, func(i, j int) bool {
return strings.ToLower(result[i].Manifest.Name) < strings.ToLower(result[j].Manifest.Name)
})
return result, nil
}
// getPrepackagedPlugin returns a pre-packaged plugin.
//
// If version is empty, the first matching plugin is returned.
func (ch *Channels) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError)
}
prepackagedPlugins := pluginsEnvironment.PrepackagedPlugins()
for _, p := range prepackagedPlugins {
if p.Manifest.Id == pluginID && (version == "" || p.Manifest.Version == version) {
return p, nil
}
}
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
}
// getRemoteMarketplacePlugin returns plugin from marketplace-server.
//
// If version is empty, the latest compatible version is used.
func (ch *Channels) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) {
marketplaceClient, err := marketplace.NewClient(
*ch.cfgSvc.Config().PluginSettings.MarketplaceURL,
ch.srv.HTTPService(),
)
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filter := ch.getBaseMarketplaceFilter()
filter.PluginId = pluginID
var plugin *model.BaseMarketplacePlugin
if version != "" {
plugin, err = marketplaceClient.GetPlugin(filter, version)
} else {
plugin, err = marketplaceClient.GetLatestPlugin(filter)
}
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return plugin, nil
}
func (a *App) getRemotePlugins() (map[string]*model.MarketplacePlugin, *model.AppError) {
result := map[string]*model.MarketplacePlugin{}
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
marketplaceClient, err := marketplace.NewClient(
*a.Config().PluginSettings.MarketplaceURL,
a.HTTPService(),
)
if err != nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filter := a.getBaseMarketplaceFilter()
// Fetch all plugins from marketplace.
filter.PerPage = -1
marketplacePlugins, err := marketplaceClient.GetPlugins(filter)
if err != nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.failed_to_fetch", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, p := range marketplacePlugins {
if p.Manifest == nil {
continue
}
result[p.Manifest.Id] = &model.MarketplacePlugin{BaseMarketplacePlugin: p}
}
return result, nil
}
// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list.
func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() {
if prepackaged.Manifest == nil {
continue
}
prepackagedMarketplace := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: prepackaged.Manifest.HomepageURL,
IconData: prepackaged.IconData,
ReleaseNotesURL: prepackaged.Manifest.ReleaseNotesURL,
Manifest: prepackaged.Manifest,
},
}
// If not available in marketplace, add the prepackaged
if remoteMarketplacePlugins[prepackaged.Manifest.Id] == nil {
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
continue
}
// If available in the marketplace, only overwrite if newer.
prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id]
marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if prepackagedVersion.GT(marketplaceVersion) {
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
}
}
return nil
}
// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list.
func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
localPlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, plugin := range localPlugins {
if plugin.Manifest == nil {
continue
}
if remoteMarketplacePlugins[plugin.Manifest.Id] != nil {
// Remote plugin is installed.
remoteMarketplacePlugins[plugin.Manifest.Id].InstalledVersion = plugin.Manifest.Version
continue
}
iconData := ""
if plugin.Manifest.IconPath != "" {
iconData, err = getIcon(filepath.Join(plugin.Path, plugin.Manifest.IconPath))
if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
}
}
var labels []model.MarketplaceLabel
if *a.Config().PluginSettings.EnableRemoteMarketplace {
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
labels = append(labels, model.MarketplaceLabel{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
})
}
remoteMarketplacePlugins[plugin.Manifest.Id] = &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: plugin.Manifest.HomepageURL,
IconData: iconData,
ReleaseNotesURL: plugin.Manifest.ReleaseNotesURL,
Labels: labels,
Manifest: plugin.Manifest,
},
InstalledVersion: plugin.Manifest.Version,
}
}
return nil
}
func (a *App) getBaseMarketplaceFilter() *model.MarketplacePluginFilter {
return a.ch.getBaseMarketplaceFilter()
}
func (ch *Channels) getBaseMarketplaceFilter() *model.MarketplacePluginFilter {
filter := &model.MarketplacePluginFilter{
ServerVersion: model.CurrentVersion,
}
license := ch.srv.License()
if license != nil && license.HasEnterpriseMarketplacePlugins() {
filter.EnterprisePlugins = true
}
if license != nil && license.IsCloud() {
filter.Cloud = true
}
if model.BuildEnterpriseReady == "true" {
filter.BuildEnterpriseReady = true
}
filter.Platform = runtime.GOOS + "-" + runtime.GOARCH
return filter
}
func pluginMatchesFilter(manifest *model.Manifest, filter string) bool {
filter = strings.TrimSpace(strings.ToLower(filter))
if filter == "" {
return true
}
if strings.ToLower(manifest.Id) == filter {
return true
}
if strings.Contains(strings.ToLower(manifest.Name), filter) {
return true
}
if strings.Contains(strings.ToLower(manifest.Description), filter) {
return true
}
return false
}
// notifyPluginEnabled notifies connected websocket clients across all peers if the version of the given
// plugin is same across them.
//
// When a peer finds itself in agreement with all other peers as to the version of the given plugin,
// it will notify all connected websocket clients (across all peers) to trigger the (re-)installation.
// There is a small chance that this never occurs, because the last server to finish installing dies before it can announce.
// There is also a chance that multiple servers notify, but the webapp handles this idempotently.
func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return errors.New("pluginsEnvironment is nil")
}
if !manifest.HasClient() || !pluginsEnvironment.IsActive(manifest.Id) {
return nil
}
var statuses model.PluginStatuses
if ch.srv.platform.Cluster() != nil {
var err *model.AppError
statuses, err = ch.srv.platform.Cluster().GetPluginStatuses()
if err != nil {
return err
}
}
localStatus, err := ch.GetPluginStatus(manifest.Id)
if err != nil {
return err
}
statuses = append(statuses, localStatus)
// This will not guard against the race condition of enabling a plugin immediately after installation.
// As GetPluginStatuses() will not return the new plugin (since other peers are racing to install),
// this peer will end up checking status against itself and will notify all webclients (including peer webclients),
// which may result in a 404.
for _, status := range statuses {
if status.PluginId == manifest.Id && status.Version != manifest.Version {
mlog.Debug("Not ready to notify webclients", mlog.String("cluster_id", status.ClusterId), mlog.String("plugin_id", manifest.Id))
return nil
}
}
// Notify all cluster peer clients.
message := model.NewWebSocketEvent(model.WebsocketEventPluginEnabled, "", "", "", nil, "")
message.Add("manifest", manifest.ClientManifest())
ch.srv.platform.Publish(message)
return nil
}
func (ch *Channels) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) {
fileStorePaths, appErr := ch.srv.listDirectory(fileStorePluginFolder, false)
if appErr != nil {
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
return ch.getPluginsFromFilePaths(fileStorePaths), nil
}
func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath {
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
fsPrefix := ""
if *ch.cfgSvc.Config().FileSettings.DriverName == model.ImageDriverS3 {
ptr := ch.cfgSvc.Config().FileSettings.AmazonS3PathPrefix
if ptr != nil && *ptr != "" {
fsPrefix = *ptr + "/"
}
}
for _, path := range fileStorePaths {
path = strings.TrimPrefix(path, fsPrefix)
if strings.HasSuffix(path, ".tar.gz") {
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz")
helper := &pluginSignaturePath{
pluginID: id,
path: path,
signaturePath: "",
}
pluginSignaturePathMap[id] = helper
}
}
for _, path := range fileStorePaths {
path = strings.TrimPrefix(path, fsPrefix)
if strings.HasSuffix(path, ".tar.gz.sig") {
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz.sig")
if val, ok := pluginSignaturePathMap[id]; !ok {
mlog.Warn("Unknown signature", mlog.String("path", path))
} else {
val.signaturePath = path
}
}
}
return pluginSignaturePathMap
}
func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin {
prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir)
if !found {
return nil
}
var fileStorePaths []string
err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
fileStorePaths = append(fileStorePaths, walkPath)
return nil
})
if err != nil {
mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err))
return nil
}
pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths)
plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap))
var wg sync.WaitGroup
for _, psPath := range pluginSignaturePathMap {
wg.Add(1)
go func(psPath *pluginSignaturePath) {
defer wg.Done()
p, err := ch.processPrepackagedPlugin(psPath)
if err != nil {
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err))
return
}
prepackagedPlugins <- p
}(psPath)
}
wg.Wait()
close(prepackagedPlugins)
for p := range prepackagedPlugins {
plugins = append(plugins, p)
}
return plugins
}
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path))
fileReader, err := os.Open(pluginPath.path)
if err != nil {
return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path)
}
defer fileReader.Close()
tmpDir, err := os.MkdirTemp("", "plugintmp")
if err != nil {
return nil, errors.Wrap(err, "Failed to create temp dir plugintmp")
}
defer os.RemoveAll(tmpDir)
plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir)
if err != nil {
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path)
}
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
return plugin, nil
}
// Skip installing if the plugin is has not been previously enabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id]
if pluginState == nil || !pluginState.Enable {
return plugin, nil
}
mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path))
if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path)
}
return plugin, nil
}
// installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags
func (ch *Channels) installFeatureFlagPlugins() {
ffControledPlugins := ch.cfgSvc.Config().FeatureFlags.Plugins()
// Respect the automatic prepackaged disable setting
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
return
}
for pluginID, version := range ffControledPlugins {
// Skip installing if the plugin has been previously disabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[pluginID]
if pluginState != nil && !pluginState.Enable {
ch.srv.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version))
continue
}
// Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well.
pluginStatus, err := ch.GetPluginStatus(pluginID)
pluginExists := err == nil
if pluginExists && pluginStatus.Version == version {
continue
}
if version != "" && version != "control" {
// If we are on-prem skip installation if this is a downgrade
license := ch.srv.License()
inCloud := license != nil && *license.Features.Cloud
if !inCloud && pluginExists {
parsedVersion, err := semver.Parse(version)
if err != nil {
ch.srv.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
return
}
parsedExistingVersion, err := semver.Parse(pluginStatus.Version)
if err != nil {
ch.srv.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
if parsedVersion.LTE(parsedExistingVersion) {
ch.srv.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
}
_, err := ch.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: version,
})
if err != nil {
ch.srv.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
if err := ch.enablePlugin(pluginID); err != nil {
ch.srv.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
ch.srv.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version))
}
}
}
}
}
// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil {
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path)
}
plugin := new(plugin.PrepackagedPlugin)
plugin.Manifest = manifest
plugin.Path = pluginPath.path
if pluginPath.signaturePath != "" {
sig := pluginPath.signaturePath
sigReader, sigErr := os.Open(sig)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig)
}
bytes, sigErr := io.ReadAll(sigReader)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig)
}
plugin.Signature = bytes
}
if manifest.IconPath != "" {
iconData, err := getIcon(filepath.Join(pluginDir, manifest.IconPath))
if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
}
plugin.IconData = iconData
}
return plugin, pluginDir, nil
}
func getIcon(iconPath string) (string, error) {
icon, err := os.ReadFile(iconPath)
if err != nil {
return "", errors.Wrapf(err, "failed to open icon at path %s", iconPath)
}
if !svg.Is(icon) {
return "", errors.Errorf("icon is not svg %s", iconPath)
}
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil
}
func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) {
switch pluginID {
case model.PluginIdApps:
// Tie Apps proxy disabled status to the feature flag.
if !ch.cfgSvc.Config().FeatureFlags.AppsEnabled {
return true, false
}
case model.PluginIdCalls:
if !ch.cfgSvc.Config().FeatureFlags.CallsEnabled {
return true, false
}
}
return false, false
}
func (a *App) IsPluginActive(pluginName string) (bool, error) {
return a.Channels().IsPluginActive(pluginName)
}
func (ch *Channels) IsPluginActive(pluginName string) (bool, error) {
pluginStatus, err := ch.GetPluginStatus(pluginName)
if err != nil {
return false, err
}
return pluginStatus.State == model.PluginStateRunning, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type PluginAPI struct {
id string
app *App
ctx *request.Context
logger mlog.Sugar
manifest *model.Manifest
}
func NewPluginAPI(a *App, c *request.Context, manifest *model.Manifest) *PluginAPI {
return &PluginAPI{
id: manifest.Id,
manifest: manifest,
ctx: c,
app: a,
logger: a.Log().Sugar(mlog.String("plugin_id", manifest.Id)),
}
}
func (api *PluginAPI) LoadPluginConfiguration(dest any) error {
finalConfig := make(map[string]any)
// First set final config to defaults
if api.manifest.SettingsSchema != nil {
for _, setting := range api.manifest.SettingsSchema.Settings {
finalConfig[strings.ToLower(setting.Key)] = setting.Default
}
}
// If we have settings given we override the defaults with them
for setting, value := range api.app.Config().PluginSettings.Plugins[api.id] {
finalConfig[strings.ToLower(setting)] = value
}
pluginSettingsJsonBytes, err := json.Marshal(finalConfig)
if err != nil {
api.logger.Error("Error marshaling config for plugin", mlog.Err(err))
return nil
}
err = json.Unmarshal(pluginSettingsJsonBytes, dest)
if err != nil {
api.logger.Error("Error unmarshaling config for plugin", mlog.Err(err))
}
return nil
}
func (api *PluginAPI) RegisterCommand(command *model.Command) error {
return api.app.RegisterPluginCommand(api.id, command)
}
func (api *PluginAPI) UnregisterCommand(teamID, trigger string) error {
api.app.UnregisterPluginCommand(api.id, teamID, trigger)
return nil
}
func (api *PluginAPI) ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) {
user, appErr := api.app.GetUser(commandArgs.UserId)
if appErr != nil {
return nil, appErr
}
commandArgs.T = i18n.GetUserTranslations(user.Locale)
commandArgs.SiteURL = api.app.GetSiteURL()
response, appErr := api.app.ExecuteCommand(api.ctx, commandArgs)
if appErr != nil {
return response, appErr
}
return response, nil
}
func (api *PluginAPI) GetConfig() *model.Config {
return api.app.GetSanitizedConfig()
}
// GetUnsanitizedConfig gets the configuration for a system admin without removing secrets.
func (api *PluginAPI) GetUnsanitizedConfig() *model.Config {
return api.app.Config().Clone()
}
func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
_, _, err := api.app.SaveConfig(config, true)
return err
}
func (api *PluginAPI) GetPluginConfig() map[string]any {
cfg := api.app.GetSanitizedConfig()
if pluginConfig, isOk := cfg.PluginSettings.Plugins[api.manifest.Id]; isOk {
return pluginConfig
}
return map[string]any{}
}
func (api *PluginAPI) SavePluginConfig(pluginConfig map[string]any) *model.AppError {
cfg := api.app.GetSanitizedConfig()
cfg.PluginSettings.Plugins[api.manifest.Id] = pluginConfig
_, _, err := api.app.SaveConfig(cfg, true)
return err
}
func (api *PluginAPI) GetBundlePath() (string, error) {
bundlePath, err := filepath.Abs(filepath.Join(*api.GetConfig().PluginSettings.Directory, api.manifest.Id))
if err != nil {
return "", err
}
return bundlePath, err
}
func (api *PluginAPI) GetLicense() *model.License {
return api.app.Srv().License()
}
func (api *PluginAPI) IsEnterpriseReady() bool {
result, _ := strconv.ParseBool(model.BuildEnterpriseReady)
return result
}
func (api *PluginAPI) GetServerVersion() string {
return model.CurrentVersion
}
func (api *PluginAPI) GetSystemInstallDate() (int64, *model.AppError) {
return api.app.Srv().Platform().GetSystemInstallDate()
}
func (api *PluginAPI) GetDiagnosticId() string {
return api.app.TelemetryId()
}
func (api *PluginAPI) GetTelemetryId() string {
return api.app.TelemetryId()
}
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
return api.app.CreateTeam(api.ctx, team)
}
func (api *PluginAPI) DeleteTeam(teamID string) *model.AppError {
return api.app.SoftDeleteTeam(teamID)
}
func (api *PluginAPI) GetTeams() ([]*model.Team, *model.AppError) {
return api.app.GetAllTeams()
}
func (api *PluginAPI) GetTeam(teamID string) (*model.Team, *model.AppError) {
return api.app.GetTeam(teamID)
}
func (api *PluginAPI) SearchTeams(term string) ([]*model.Team, *model.AppError) {
teams, _, err := api.app.SearchAllTeams(&model.TeamSearch{Term: term})
return teams, err
}
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
return api.app.GetTeamByName(name)
}
func (api *PluginAPI) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) {
return api.app.GetTeamsUnreadForUser("", userID, false)
}
func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
return api.app.UpdateTeam(team)
}
func (api *PluginAPI) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
return api.app.GetTeamsForUser(userID)
}
func (api *PluginAPI) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.AddTeamMember(api.ctx, teamID, userID)
}
func (api *PluginAPI) CreateTeamMembers(teamID string, userIDs []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
members, err := api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, false)
if err != nil {
return nil, err
}
return model.TeamMembersWithErrorToTeamMembers(members), nil
}
func (api *PluginAPI) CreateTeamMembersGracefully(teamID string, userIDs []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) {
return api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, true)
}
func (api *PluginAPI) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError {
return api.app.RemoveUserFromTeam(api.ctx, teamID, userID, requestorId)
}
func (api *PluginAPI) GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembers(teamID, page*perPage, perPage, nil)
}
func (api *PluginAPI) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.GetTeamMember(teamID, userID)
}
func (api *PluginAPI) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembersForUserWithPagination(userID, page, perPage)
}
func (api *PluginAPI) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) {
return api.app.UpdateTeamMemberRoles(teamID, userID, newRoles)
}
func (api *PluginAPI) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) {
return api.app.GetTeamStats(teamID, nil)
}
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.CreateUser(api.ctx, user)
}
func (api *PluginAPI) DeleteUser(userID string) *model.AppError {
user, err := api.app.GetUser(userID)
if err != nil {
return err
}
_, err = api.app.UpdateActive(api.ctx, user, false)
return err
}
func (api *PluginAPI) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
return api.app.GetUsersFromProfiles(options)
}
func (api *PluginAPI) GetUser(userID string) (*model.User, *model.AppError) {
return api.app.GetUser(userID)
}
func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
return api.app.GetUserByEmail(email)
}
func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
return api.app.GetUserByUsername(name)
}
func (api *PluginAPI) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) {
return api.app.GetUsersByUsernames(usernames, true, nil)
}
func (api *PluginAPI) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) {
options := &model.UserGetOptions{InTeamId: teamID, Page: page, PerPage: perPage}
return api.app.GetUsersInTeam(options)
}
func (api *PluginAPI) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) {
return api.app.GetPreferencesForUser(userID)
}
func (api *PluginAPI) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.UpdatePreferences(userID, preferences)
}
func (api *PluginAPI) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.DeletePreferences(userID, preferences)
}
func (api *PluginAPI) GetSession(sessionID string) (*model.Session, *model.AppError) {
return api.app.GetSessionById(sessionID)
}
func (api *PluginAPI) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
return api.app.CreateSession(session)
}
func (api *PluginAPI) ExtendSessionExpiry(sessionID string, expiresAt int64) *model.AppError {
session, err := api.app.ch.srv.platform.GetSessionByID(sessionID)
if err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := api.app.ch.srv.platform.ExtendSessionExpiry(session, expiresAt); err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.extend_session_expiry.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (api *PluginAPI) RevokeSession(sessionID string) *model.AppError {
return api.app.RevokeSessionById(sessionID)
}
func (api *PluginAPI) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
return api.app.CreateUserAccessToken(token)
}
func (api *PluginAPI) RevokeUserAccessToken(tokenID string) *model.AppError {
accessToken, err := api.app.GetUserAccessToken(tokenID, false)
if err != nil {
return err
}
return api.app.RevokeUserAccessToken(accessToken)
}
func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.UpdateUser(api.ctx, user, true)
}
func (api *PluginAPI) UpdateUserActive(userID string, active bool) *model.AppError {
return api.app.UpdateUserActive(api.ctx, userID, active)
}
func (api *PluginAPI) GetUserStatus(userID string) (*model.Status, *model.AppError) {
return api.app.GetStatus(userID)
}
func (api *PluginAPI) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
return api.app.GetUserStatusesByIds(userIDs)
}
func (api *PluginAPI) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) {
switch status {
case model.StatusOnline:
api.app.SetStatusOnline(userID, true)
case model.StatusOffline:
api.app.SetStatusOffline(userID, true)
case model.StatusAway:
api.app.SetStatusAwayIfNeeded(userID, true)
case model.StatusDnd:
api.app.SetStatusDoNotDisturb(userID)
default:
return nil, model.NewAppError("UpdateUserStatus", "plugin.api.update_user_status.bad_status", nil, "unrecognized status", http.StatusBadRequest)
}
return api.app.GetStatus(userID)
}
func (api *PluginAPI) SetUserStatusTimedDND(userID string, endTime int64) (*model.Status, *model.AppError) {
// read-after-write bug which will fail if there are replicas.
// it works for now because we have a cache in between.
// FIXME: make SetStatusDoNotDisturbTimed return updated status
api.app.SetStatusDoNotDisturbTimed(userID, endTime)
return api.app.GetStatus(userID)
}
func (api *PluginAPI) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError {
return api.app.SetCustomStatus(api.ctx, userID, customStatus)
}
func (api *PluginAPI) RemoveUserCustomStatus(userID string) *model.AppError {
return api.app.RemoveCustomStatus(api.ctx, userID)
}
func (api *PluginAPI) GetUserCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
return api.app.GetCustomStatus(userID)
}
func (api *PluginAPI) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
switch sortBy {
case model.ChannelSortByUsername:
return api.app.GetUsersInChannel(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
case model.ChannelSortByStatus:
return api.app.GetUsersInChannelByStatus(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
default:
return nil, model.NewAppError("GetUsersInChannel", "plugin.api.get_users_in_channel", nil, "invalid sort option", http.StatusBadRequest)
}
}
func (api *PluginAPI) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) {
if api.app.Ldap() == nil {
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
if user.AuthData == nil {
return map[string]string{}, nil
}
// Only bother running the query if the user's auth service is LDAP or it's SAML and sync is enabled.
if user.AuthService == model.UserAuthServiceLdap ||
(user.AuthService == model.UserAuthServiceSaml && *api.app.Config().SamlSettings.EnableSyncWithLdap) {
return api.app.Ldap().GetUserAttributes(*user.AuthData, attributes)
}
return map[string]string{}, nil
}
func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return api.app.CreateChannel(api.ctx, channel, false)
}
func (api *PluginAPI) DeleteChannel(channelID string) *model.AppError {
channel, err := api.app.GetChannel(api.ctx, channelID)
if err != nil {
return err
}
return api.app.DeleteChannel(api.ctx, channel, "")
}
func (api *PluginAPI) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetPublicChannelsForTeam(api.ctx, teamID, page*perPage, perPage)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannel(channelID string) (*model.Channel, *model.AppError) {
return api.app.GetChannel(api.ctx, channelID)
}
func (api *PluginAPI) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByName(api.ctx, name, teamID, includeDeleted)
}
func (api *PluginAPI) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByNameForTeamName(api.ctx, channelName, teamName, includeDeleted)
}
func (api *PluginAPI) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetChannelsForTeamForUser(api.ctx, teamID, userID, &model.ChannelSearchOpts{
IncludeDeleted: includeDeleted,
LastDeleteAt: 0,
})
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannelStats(channelID string) (*model.ChannelStats, *model.AppError) {
memberCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
guestCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
return &model.ChannelStats{ChannelId: channelID, MemberCount: memberCount, GuestCount: guestCount}, nil
}
func (api *PluginAPI) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return api.app.GetOrCreateDirectChannel(api.ctx, userID1, userID2)
}
func (api *PluginAPI) GetGroupChannel(userIDs []string) (*model.Channel, *model.AppError) {
return api.app.CreateGroupChannel(api.ctx, userIDs, "")
}
func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return api.app.UpdateChannel(api.ctx, channel)
}
func (api *PluginAPI) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) {
channels, err := api.app.SearchChannels(api.ctx, teamID, term)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.CreateSidebarCategory(api.ctx, userID, teamID, newCategory)
}
func (api *PluginAPI) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return api.app.GetSidebarCategoriesForTeamForUser(api.ctx, userID, teamID)
}
func (api *PluginAPI) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.UpdateSidebarCategories(api.ctx, userID, teamID, categories)
}
func (api *PluginAPI) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) {
pluginSearchUsersOptions := &model.UserSearchOptions{
IsAdmin: true,
AllowInactive: search.AllowInactive,
Limit: search.Limit,
}
return api.app.SearchUsers(search, pluginSearchUsersOptions)
}
func (api *PluginAPI) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) {
postList, err := api.app.SearchPostsInTeam(teamID, paramsList)
if err != nil {
return nil, err
}
return postList.ForPlugin().ToSlice(), nil
}
func (api *PluginAPI) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) {
var terms string
if searchParams.Terms != nil {
terms = *searchParams.Terms
}
timeZoneOffset := 0
if searchParams.TimeZoneOffset != nil {
timeZoneOffset = *searchParams.TimeZoneOffset
}
isOrSearch := false
if searchParams.IsOrSearch != nil {
isOrSearch = *searchParams.IsOrSearch
}
page := 0
if searchParams.Page != nil {
page = *searchParams.Page
}
perPage := 100
if searchParams.PerPage != nil {
perPage = *searchParams.PerPage
}
includeDeletedChannels := false
if searchParams.IncludeDeletedChannels != nil {
includeDeletedChannels = *searchParams.IncludeDeletedChannels
}
results, appErr := api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, model.ModifierMessages)
if results != nil {
results = results.ForPlugin()
}
return results, appErr
}
func (api *PluginAPI) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.GetChannel(channelID)
if err != nil {
return nil, err
}
return api.app.AddChannelMember(api.ctx, userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (api *PluginAPI) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.GetChannel(channelID)
if err != nil {
return nil, err
}
return api.app.AddChannelMember(api.ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (api *PluginAPI) GetChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
return api.app.GetChannelMember(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersPage(api.ctx, channelID, page, perPage)
}
func (api *PluginAPI) GetChannelMembersByIds(channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersByIds(api.ctx, channelID, userIDs)
}
func (api *PluginAPI) GetChannelMembersForUser(_, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
// The team ID parameter was never used in the SQL query.
// But we keep this to maintain compatibility.
return api.app.GetChannelMembersForUserWithPagination(api.ctx, userID, page, perPage)
}
func (api *PluginAPI) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return api.app.UpdateChannelMemberRoles(api.ctx, channelID, userID, newRoles)
}
func (api *PluginAPI) UpdateChannelMemberNotifications(channelID, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
return api.app.UpdateChannelMemberNotifyProps(api.ctx, notifications, channelID, userID)
}
func (api *PluginAPI) DeleteChannelMember(channelID, userID string) *model.AppError {
return api.app.LeaveChannel(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) {
return api.app.GetGroup(groupId, nil, nil)
}
func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) {
return api.app.GetGroupByName(name, model.GroupSearchOpts{})
}
func (api *PluginAPI) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := api.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
func (api *PluginAPI) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsBySource(groupSource)
}
func (api *PluginAPI) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsByUserId(userID)
}
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
post.AddProp("from_plugin", "true")
post, appErr := api.app.CreatePostMissingChannel(api.ctx, post, true, true)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) {
return api.app.SaveReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) RemoveReaction(reaction *model.Reaction) *model.AppError {
return api.app.DeleteReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) GetReactions(postID string) ([]*model.Reaction, *model.AppError) {
return api.app.GetReactionsForPost(postID)
}
func (api *PluginAPI) SendEphemeralPost(userID string, post *model.Post) *model.Post {
return api.app.SendEphemeralPost(api.ctx, userID, post).ForPlugin()
}
func (api *PluginAPI) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
return api.app.UpdateEphemeralPost(api.ctx, userID, post).ForPlugin()
}
func (api *PluginAPI) DeleteEphemeralPost(userID, postID string) {
api.app.DeleteEphemeralPost(userID, postID)
}
func (api *PluginAPI) DeletePost(postID string) *model.AppError {
_, err := api.app.DeletePost(api.ctx, postID, api.id)
return err
}
func (api *PluginAPI) GetPostThread(postID string) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostThread(postID, model.GetPostsOptions{}, "")
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPost(postID string) (*model.Post, *model.AppError) {
post, appErr := api.app.GetSinglePost(postID, false)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetPostsSince(channelID string, time int64) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelID, Time: time})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsAfter(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsBefore(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsPage(model.GetPostsOptions{ChannelId: channelID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
post, appErr := api.app.UpdatePost(api.ctx, post, false)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetProfileImage(userID string) ([]byte, *model.AppError) {
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
data, _, err := api.app.GetProfileImage(user)
return data, err
}
func (api *PluginAPI) SetProfileImage(userID string, data []byte) *model.AppError {
if _, err := api.app.GetUser(userID); err != nil {
return err
}
return api.app.SetProfileImageFromFile(api.ctx, userID, bytes.NewReader(data))
}
func (api *PluginAPI) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) {
return api.app.GetEmojiList(api.ctx, page, perPage, sortBy)
}
func (api *PluginAPI) GetEmojiByName(name string) (*model.Emoji, *model.AppError) {
return api.app.GetEmojiByName(api.ctx, name)
}
func (api *PluginAPI) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
return api.app.GetEmoji(api.ctx, emojiId)
}
func (api *PluginAPI) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
return api.app.CopyFileInfos(userID, fileIDs)
}
func (api *PluginAPI) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return api.app.GetFileInfo(fileID)
}
func (api *PluginAPI) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
return api.app.GetFileInfos(page, perPage, opt)
}
func (api *PluginAPI) GetFileLink(fileID string) (string, *model.AppError) {
if !*api.app.Config().FileSettings.EnablePublicLink {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.disabled.app_error", nil, "", http.StatusNotImplemented)
}
info, err := api.app.GetFileInfo(fileID)
if err != nil {
return "", err
}
if info.PostId == "" {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
}
return api.app.GeneratePublicLink(api.app.GetSiteURL(), info), nil
}
func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) {
return api.app.ReadFile(path)
}
func (api *PluginAPI) GetFile(fileID string) ([]byte, *model.AppError) {
return api.app.GetFile(fileID)
}
func (api *PluginAPI) UploadFile(data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
return api.app.UploadFile(api.ctx, data, channelID, filename)
}
func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
return api.app.GetEmojiImage(api.ctx, emojiId)
}
func (api *PluginAPI) GetTeamIcon(teamID string) ([]byte, *model.AppError) {
team, err := api.app.GetTeam(teamID)
if err != nil {
return nil, err
}
data, err := api.app.GetTeamIcon(team)
if err != nil {
return nil, err
}
return data, nil
}
func (api *PluginAPI) SetTeamIcon(teamID string, data []byte) *model.AppError {
team, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
return api.app.SetTeamIconFromFile(team, bytes.NewReader(data))
}
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
return api.app.OpenInteractiveDialog(dialog)
}
func (api *PluginAPI) RemoveTeamIcon(teamID string) *model.AppError {
_, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
err = api.app.RemoveTeamIcon(teamID)
if err != nil {
return err
}
return nil
}
// Mail Section
func (api *PluginAPI) SendMail(to, subject, htmlBody string) *model.AppError {
if to == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_to", nil, "", http.StatusBadRequest)
}
if subject == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_subject", nil, "", http.StatusBadRequest)
}
if htmlBody == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusBadRequest)
}
if err := api.app.Srv().EmailService.SendNotificationMail(to, subject, htmlBody); err != nil {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// Plugin Section
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {
plugins, err := api.app.GetPlugins()
if err != nil {
return nil, err
}
var manifests []*model.Manifest
for _, manifest := range plugins.Active {
manifests = append(manifests, &manifest.Manifest)
}
for _, manifest := range plugins.Inactive {
manifests = append(manifests, &manifest.Manifest)
}
return manifests, nil
}
func (api *PluginAPI) EnablePlugin(id string) *model.AppError {
return api.app.EnablePlugin(id)
}
func (api *PluginAPI) DisablePlugin(id string) *model.AppError {
return api.app.DisablePlugin(id)
}
func (api *PluginAPI) RemovePlugin(id string) *model.AppError {
return api.app.Channels().RemovePlugin(id)
}
func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
return api.app.GetPluginStatus(id)
}
func (api *PluginAPI) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
if !*api.app.Config().PluginSettings.Enable || !*api.app.Config().PluginSettings.EnableUploads {
return nil, model.NewAppError("installPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
}
fileBuffer, err := io.ReadAll(file)
if err != nil {
return nil, model.NewAppError("InstallPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
}
return api.app.InstallPlugin(bytes.NewReader(fileBuffer), replace)
}
// KV Store Section
func (api *PluginAPI) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
return api.app.SetPluginKeyWithOptions(api.id, key, value, options)
}
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
return api.app.SetPluginKey(api.id, key, value)
}
func (api *PluginAPI) KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) {
return api.app.CompareAndSetPluginKey(api.id, key, oldValue, newValue)
}
func (api *PluginAPI) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) {
return api.app.CompareAndDeletePluginKey(api.id, key, oldValue)
}
func (api *PluginAPI) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError {
return api.app.SetPluginKeyWithExpiry(api.id, key, value, expireInSeconds)
}
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
return api.app.GetPluginKey(api.id, key)
}
func (api *PluginAPI) KVDelete(key string) *model.AppError {
return api.app.DeletePluginKey(api.id, key)
}
func (api *PluginAPI) KVDeleteAll() *model.AppError {
return api.app.DeleteAllKeysForPlugin(api.id)
}
func (api *PluginAPI) KVList(page, perPage int) ([]string, *model.AppError) {
return api.app.ListPluginKeys(api.id, page, perPage)
}
func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
ev := model.NewWebSocketEvent(fmt.Sprintf("custom_%v_%v", api.id, event), "", "", "", nil, "")
ev = ev.SetBroadcast(broadcast).SetData(payload)
api.app.Publish(ev)
}
func (api *PluginAPI) HasPermissionTo(userID string, permission *model.Permission) bool {
return api.app.HasPermissionTo(userID, permission)
}
func (api *PluginAPI) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool {
return api.app.HasPermissionToTeam(userID, teamID, permission)
}
func (api *PluginAPI) HasPermissionToChannel(userID, channelID string, permission *model.Permission) bool {
return api.app.HasPermissionToChannel(api.ctx, userID, channelID, permission)
}
func (api *PluginAPI) RolesGrantPermission(roleNames []string, permissionId string) bool {
return api.app.RolesGrantPermission(roleNames, permissionId)
}
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...any) {
api.logger.Debugw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...any) {
api.logger.Infow(msg, keyValuePairs...)
}
func (api *PluginAPI) LogError(msg string, keyValuePairs ...any) {
api.logger.Errorw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...any) {
api.logger.Warnw(msg, keyValuePairs...)
}
func (api *PluginAPI) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
// Bots created by a plugin should use the plugin's ID for the creator field, unless
// otherwise specified by the plugin.
if bot.OwnerId == "" {
bot.OwnerId = api.id
}
// Bots cannot be owners of other bots
if user, err := api.app.GetUser(bot.OwnerId); err == nil {
if user.IsBot {
return nil, model.NewAppError("CreateBot", "plugin_api.bot_cant_create_bot", nil, "", http.StatusBadRequest)
}
}
return api.app.CreateBot(api.ctx, bot)
}
func (api *PluginAPI) PatchBot(userID string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
return api.app.PatchBot(userID, botPatch)
}
func (api *PluginAPI) GetBot(userID string, includeDeleted bool) (*model.Bot, *model.AppError) {
return api.app.GetBot(userID, includeDeleted)
}
func (api *PluginAPI) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) {
bots, err := api.app.GetBots(options)
return []*model.Bot(bots), err
}
func (api *PluginAPI) UpdateBotActive(userID string, active bool) (*model.Bot, *model.AppError) {
return api.app.UpdateBotActive(api.ctx, userID, active)
}
func (api *PluginAPI) PermanentDeleteBot(userID string) *model.AppError {
return api.app.PermanentDeleteBot(userID)
}
func (api *PluginAPI) EnsureBotUser(bot *model.Bot) (string, error) {
// Bots created by a plugin should use the plugin's ID for the creator field.
bot.OwnerId = api.id
return api.app.EnsureBot(api.ctx, api.id, bot)
}
func (api *PluginAPI) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
return api.app.PublishUserTyping(userID, channelID, parentId)
}
func (api *PluginAPI) PluginHTTP(request *http.Request) *http.Response {
split := strings.SplitN(request.URL.Path, "/", 3)
if len(split) != 3 {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString("Not enough URL. Form of URL should be /<pluginid>/*")),
}
}
destinationPluginId := split[1]
newURL, err := url.Parse("/" + split[2])
newURL.RawQuery = request.URL.Query().Encode()
request.URL = newURL
if destinationPluginId == "" || err != nil {
message := "No plugin specified. Form of URL should be /<pluginid>/*"
if err != nil {
message = "Form of URL should be /<pluginid>/* Error: " + err.Error()
}
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(message)),
}
}
responseTransfer := &PluginResponseWriter{}
api.app.ServeInterPluginRequest(responseTransfer, request, api.id, destinationPluginId)
return responseTransfer.GenerateResponse()
}
func (api *PluginAPI) CreateCommand(cmd *model.Command) (*model.Command, error) {
cmd.CreatorId = ""
cmd.PluginId = api.id
cmd, appErr := api.app.createCommand(cmd)
if appErr != nil {
return cmd, appErr
}
return cmd, nil
}
func (api *PluginAPI) ListCommands(teamID string) ([]*model.Command, error) {
ret := make([]*model.Command, 0)
cmds, err := api.ListPluginCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListBuiltInCommands()
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListCustomCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
return ret, nil
}
func (api *PluginAPI) ListCustomCommands(teamID string) ([]*model.Command, error) {
// Plugins are allowed to bypass the a.Config().ServiceSettings.EnableCommands setting.
return api.app.Srv().Store().Command().GetByTeam(teamID)
}
func (api *PluginAPI) ListPluginCommands(teamID string) ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, cmd := range api.app.CommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
return commands, nil
}
func (api *PluginAPI) ListBuiltInCommands() ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(api.app, i18n.T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
return commands, nil
}
func (api *PluginAPI) GetCommand(commandID string) (*model.Command, error) {
return api.app.Srv().Store().Command().Get(commandID)
}
func (api *PluginAPI) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) {
oldCmd, err := api.GetCommand(commandID)
if err != nil {
return nil, err
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.PluginId = api.id
if updatedCmd.TeamId == "" {
updatedCmd.TeamId = oldCmd.TeamId
}
return api.app.Srv().Store().Command().Update(updatedCmd)
}
func (api *PluginAPI) DeleteCommand(commandID string) error {
err := api.app.Srv().Store().Command().Delete(commandID, model.GetMillis())
if err != nil {
return err
}
return nil
}
func (api *PluginAPI) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
return api.app.CreateOAuthApp(app)
}
func (api *PluginAPI) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
return api.app.GetOAuthApp(appID)
}
func (api *PluginAPI) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
oldApp, err := api.GetOAuthApp(app.Id)
if err != nil {
return nil, err
}
return api.app.UpdateOAuthApp(oldApp, app)
}
func (api *PluginAPI) DeleteOAuthApp(appID string) *model.AppError {
return api.app.DeleteOAuthApp(appID)
}
// PublishPluginClusterEvent broadcasts a plugin event to all other running instances of
// the calling plugin.
func (api *PluginAPI) PublishPluginClusterEvent(ev model.PluginClusterEvent,
opts model.PluginClusterEventSendOptions) error {
if api.app.Cluster() == nil {
return nil
}
msg := &model.ClusterMessage{
Event: model.ClusterEventPluginEvent,
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"PluginID": api.id,
"EventID": ev.Id,
},
Data: ev.Data,
}
// If TargetId is empty we broadcast to all other cluster nodes.
if opts.TargetId == "" {
api.app.Cluster().SendClusterMessage(msg)
} else {
if err := api.app.Cluster().SendClusterMessageToNode(opts.TargetId, msg); err != nil {
return fmt.Errorf("failed to send message to cluster node %q: %w", opts.TargetId, err)
}
}
return nil
}
// RequestTrialLicense requests a trial license and installs it in the server
func (api *PluginAPI) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *api.app.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
}
// GetCloudLimits returns any limits associated with the cloud instance
func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
if api.app.Cloud() == nil {
return &model.ProductLimits{}, nil
}
limits, err := api.app.Cloud().GetCloudLimits("")
return limits, err
}
// RegisterCollectionAndTopic informs the server that this plugin handles
// the given collection and topic types.
func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error {
return api.app.RegisterCollectionAndTopic(api.id, collectionType, topicType)
}
func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
us, err := api.app.CreateUploadSession(api.ctx, us)
if err != nil {
return nil, err
}
return us, nil
}
func (api *PluginAPI) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
fi, err := api.app.UploadData(api.ctx, us, rd)
if err != nil {
return nil, err
}
return fi, nil
}
func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, error) {
// We want to fetch from master DB to avoid a potential read-after-write on the plugin side.
api.ctx.SetContext(WithMaster(api.ctx.Context()))
fi, err := api.app.GetUploadSession(api.ctx, uploadID)
if err != nil {
return nil, err
}
return fi, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type PluginCommand struct {
Command *model.Command
PluginId string
}
func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) error {
if command.Trigger == "" {
return errors.New("invalid command")
}
if command.AutocompleteData != nil {
if err := command.AutocompleteData.IsValid(); err != nil {
return errors.Wrap(err, "invalid autocomplete data in command")
}
}
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
} else {
baseURL, err := url.Parse("/plugins/" + pluginID)
if err != nil {
return errors.Wrapf(err, "Can't parse url %s", "/plugins/"+pluginID)
}
err = command.AutocompleteData.UpdateRelativeURLsForPluginCommands(baseURL)
if err != nil {
return errors.Wrap(err, "Can't update relative urls for plugin commands")
}
}
command = &model.Command{
Trigger: strings.ToLower(command.Trigger),
TeamId: command.TeamId,
AutoComplete: command.AutoComplete,
AutoCompleteDesc: command.AutoCompleteDesc,
AutoCompleteHint: command.AutoCompleteHint,
DisplayName: command.DisplayName,
AutocompleteData: command.AutocompleteData,
AutocompleteIconData: command.AutocompleteIconData,
}
a.ch.pluginCommandsLock.Lock()
defer a.ch.pluginCommandsLock.Unlock()
for _, pc := range a.ch.pluginCommands {
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
if pc.PluginId == pluginID {
pc.Command = command
return nil
}
}
}
a.ch.pluginCommands = append(a.ch.pluginCommands, &PluginCommand{
Command: command,
PluginId: pluginID,
})
return nil
}
func (a *App) UnregisterPluginCommand(pluginID, teamID, trigger string) {
trigger = strings.ToLower(trigger)
a.ch.pluginCommandsLock.Lock()
defer a.ch.pluginCommandsLock.Unlock()
var remaining []*PluginCommand
for _, pc := range a.ch.pluginCommands {
if pc.Command.TeamId != teamID || pc.Command.Trigger != trigger {
remaining = append(remaining, pc)
}
}
a.ch.pluginCommands = remaining
}
func (ch *Channels) unregisterPluginCommands(pluginID string) {
ch.pluginCommandsLock.Lock()
defer ch.pluginCommandsLock.Unlock()
var remaining []*PluginCommand
for _, pc := range ch.pluginCommands {
if pc.PluginId != pluginID {
remaining = append(remaining, pc)
}
}
ch.pluginCommands = remaining
}
// CommandsForTeam returns all the plugin and product commands for the given team.
func (a *App) CommandsForTeam(teamID string) []*model.Command {
var commands []*model.Command
a.ch.pluginCommandsLock.RLock()
defer a.ch.pluginCommandsLock.RUnlock()
for _, pc := range a.ch.pluginCommands {
if pc.Command.TeamId == "" || pc.Command.TeamId == teamID {
commands = append(commands, pc.Command)
}
}
a.ch.productCommandsLock.RLock()
defer a.ch.productCommandsLock.RUnlock()
for _, pc := range a.ch.productCommands {
if pc.Command.TeamId == "" || pc.Command.TeamId == teamID {
commands = append(commands, pc.Command)
}
}
return commands
}
// tryExecutePluginCommand attempts to run a command provided by a plugin based on the given arguments. If no such
// command can be found, returns nil for all arguments.
func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
parts := strings.Split(args.Command, " ")
trigger := parts[0][1:]
trigger = strings.ToLower(trigger)
var matched *PluginCommand
a.ch.pluginCommandsLock.RLock()
for _, pc := range a.ch.pluginCommands {
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
matched = pc
break
}
}
a.ch.pluginCommandsLock.RUnlock()
if matched == nil {
return nil, nil, nil
}
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, nil, nil
}
// Checking if plugin is working or not
if err := pluginsEnvironment.PerformHealthCheck(matched.PluginId); err != nil {
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command_error.error.app_error", map[string]any{"Command": trigger}, "err= Plugin has recently crashed: "+matched.PluginId, http.StatusInternalServerError)
}
pluginHooks, err := pluginsEnvironment.HooksForPlugin(matched.PluginId)
if err != nil {
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
}
for username, userID := range a.MentionsToTeamMembers(c, args.Command, args.TeamId) {
args.AddUserMention(username, userID)
}
for channelName, channelID := range a.MentionsToPublicChannels(c, args.Command, args.TeamId) {
args.AddChannelMention(channelName, channelID)
}
response, appErr := pluginHooks.ExecuteCommand(pluginContext(c), args)
// Checking if plugin crashed after running the command
if err := pluginsEnvironment.PerformHealthCheck(matched.PluginId); err != nil {
errMessage := fmt.Sprintf("err= Plugin %s crashed due to /%s command", matched.PluginId, trigger)
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command_crash.error.app_error", map[string]any{"Command": trigger, "PluginId": matched.PluginId}, errMessage, http.StatusInternalServerError)
}
// This is a response from the plugin, which may set an incorrect status code;
// e.g setting a status code of 0 will crash the server. So we always bucket everything under 500.
if appErr != nil && (appErr.StatusCode < 100 || appErr.StatusCode > 999) {
mlog.Warn("Invalid status code returned from plugin. Converting to internal server error.", mlog.String("plugin_id", matched.PluginId), mlog.Int("status_code", appErr.StatusCode))
appErr.StatusCode = http.StatusInternalServerError
}
return matched.Command, response, appErr
}
// Support for slash commands to MPA
//
// Key differences/points with plugin commands:
// - There's no need of health checks or unregisterProductCommands on products, they are compiled and assumed as active server side
// - HooksForProduct still returns a plugin.Hooks struct, it might make sense to improve the name/package
// - Plugin code had a check for a plugin crash after a command was executed, that has been omitted for products
type ProductCommand struct {
Command *model.Command
ProductID string
}
func (a *App) RegisterProductCommand(ProductID string, command *model.Command) error {
if command.Trigger == "" {
return errors.New("invalid command")
}
if command.AutocompleteData != nil {
if err := command.AutocompleteData.IsValid(); err != nil {
return errors.Wrap(err, "invalid autocomplete data in command")
}
}
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
} else {
baseURL, err := url.Parse("/plugins/" + ProductID)
if err != nil {
return errors.Wrapf(err, "Can't parse url %s", "/plugins/"+ProductID)
}
err = command.AutocompleteData.UpdateRelativeURLsForPluginCommands(baseURL)
if err != nil {
return errors.Wrap(err, "Can't update relative urls for plugin commands")
}
}
command = &model.Command{
Trigger: strings.ToLower(command.Trigger),
TeamId: command.TeamId,
AutoComplete: command.AutoComplete,
AutoCompleteDesc: command.AutoCompleteDesc,
AutoCompleteHint: command.AutoCompleteHint,
DisplayName: command.DisplayName,
AutocompleteData: command.AutocompleteData,
AutocompleteIconData: command.AutocompleteIconData,
}
a.ch.productCommandsLock.Lock()
defer a.ch.productCommandsLock.Unlock()
for _, pc := range a.ch.productCommands {
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
if pc.ProductID == ProductID {
pc.Command = command
return nil
}
}
}
a.ch.productCommands = append(a.ch.productCommands, &ProductCommand{
Command: command,
ProductID: ProductID,
})
return nil
}
// tryExecuteProductCommand attempts to run a command provided by a product based on the given arguments. If no such
// command can be found, returns nil for all arguments.
func (a *App) tryExecuteProductCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
parts := strings.Split(args.Command, " ")
trigger := parts[0][1:]
trigger = strings.ToLower(trigger)
var matched *ProductCommand
a.ch.productCommandsLock.RLock()
for _, pc := range a.ch.productCommands {
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
matched = pc
break
}
}
a.ch.productCommandsLock.RUnlock()
if matched == nil {
return nil, nil, nil
}
// The type returned is still plugin.Hooks, could make sense in the future to move Hooks
// to another package or change the abstraction
productHooks := a.HooksManager().HooksForProduct(matched.ProductID)
if productHooks == nil {
return matched.Command, nil, model.NewAppError("ExecutePropductCommand", "model.plugin_command.error.app_error", nil, "", http.StatusInternalServerError)
}
for username, userID := range a.MentionsToTeamMembers(c, args.Command, args.TeamId) {
args.AddUserMention(username, userID)
}
for channelName, channelID := range a.MentionsToPublicChannels(c, args.Command, args.TeamId) {
args.AddChannelMention(channelName, channelID)
}
response, appErr := productHooks.ExecuteCommand(pluginContext(c), args)
// This is a response from the product, which may set an incorrect status code;
// e.g setting a status code of 0 will crash the server. So we always bucket everything under 500.
if appErr != nil && (appErr.StatusCode < 100 || appErr.StatusCode > 999) {
mlog.Warn("Invalid status code returned from plugin. Converting to internal server error.", mlog.String("plugin_id", matched.ProductID), mlog.Int("status_code", appErr.StatusCode))
appErr.StatusCode = http.StatusInternalServerError
}
return matched.Command, response, appErr
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"database/sql"
"database/sql/driver"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
)
// DriverImpl implements the plugin.Driver interface on the server-side.
// Each new request for a connection/statement/transaction etc, generates
// a new entry tracked centrally in a map. Further requests operate on the
// object ID.
type DriverImpl struct {
s *Server
connMut sync.RWMutex
connMap map[string]*sql.Conn
txMut sync.Mutex
txMap map[string]driver.Tx
stMut sync.RWMutex
stMap map[string]driver.Stmt
rowsMut sync.RWMutex
rowsMap map[string]driver.Rows
}
func NewDriverImpl(s *Server) *DriverImpl {
return &DriverImpl{
s: s,
connMap: make(map[string]*sql.Conn),
txMap: make(map[string]driver.Tx),
stMap: make(map[string]driver.Stmt),
rowsMap: make(map[string]driver.Rows),
}
}
func (d *DriverImpl) Conn(isMaster bool) (string, error) {
dbFunc := d.s.Platform().Store.GetInternalMasterDB
if !isMaster {
dbFunc = d.s.Platform().Store.GetInternalReplicaDB
}
timeout := time.Duration(*d.s.Config().SqlSettings.QueryTimeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := dbFunc().Conn(ctx)
if err != nil {
return "", err
}
connID := model.NewId()
d.connMut.Lock()
d.connMap[connID] = conn
d.connMut.Unlock()
return connID, nil
}
// According to https://golang.org/pkg/database/sql/#Conn, a client can call
// Close on a connection, concurrently while running a query.
//
// Therefore, we have to handle the case where the connection is no longer
// present in the map because it has been closed. ErrBadConn is a good choice
// here which indicates the sql package to retry on a new connection.
//
// ConnPing, ConnQuery, ConnClose, Tx, and Stmt do this.
func (d *DriverImpl) ConnPing(connID string) error {
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return driver.ErrBadConn
}
return conn.Raw(func(innerConn any) error {
return innerConn.(driver.Pinger).Ping(context.Background())
})
}
func (d *DriverImpl) ConnQuery(connID, q string, args []driver.NamedValue) (_ string, err error) {
var rows driver.Rows
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
rows, err = innerConn.(driver.QueryerContext).QueryContext(context.Background(), q, args)
return err
})
if err != nil {
return "", err
}
rowsID := model.NewId()
d.rowsMut.Lock()
d.rowsMap[rowsID] = rows
d.rowsMut.Unlock()
return rowsID, nil
}
func (d *DriverImpl) ConnExec(connID, q string, args []driver.NamedValue) (_ plugin.ResultContainer, err error) {
var res driver.Result
var ret plugin.ResultContainer
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return ret, driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
res, err = innerConn.(driver.ExecerContext).ExecContext(context.Background(), q, args)
return err
})
if err != nil {
return ret, err
}
ret.LastID, ret.LastIDError = res.LastInsertId()
ret.RowsAffected, ret.RowsAffectedError = res.RowsAffected()
return ret, nil
}
func (d *DriverImpl) ConnClose(connID string) error {
d.connMut.Lock()
conn, ok := d.connMap[connID]
if !ok {
d.connMut.Unlock()
return driver.ErrBadConn
}
delete(d.connMap, connID)
d.connMut.Unlock()
return conn.Close()
}
func (d *DriverImpl) Tx(connID string, opts driver.TxOptions) (_ string, err error) {
var tx driver.Tx
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
tx, err = innerConn.(driver.ConnBeginTx).BeginTx(context.Background(), opts)
return err
})
if err != nil {
return "", err
}
txID := model.NewId()
d.txMut.Lock()
d.txMap[txID] = tx
d.txMut.Unlock()
return txID, nil
}
func (d *DriverImpl) TxCommit(txID string) error {
d.txMut.Lock()
tx := d.txMap[txID]
delete(d.txMap, txID)
d.txMut.Unlock()
return tx.Commit()
}
func (d *DriverImpl) TxRollback(txID string) error {
d.txMut.Lock()
tx := d.txMap[txID]
delete(d.txMap, txID)
d.txMut.Unlock()
return tx.Rollback()
}
func (d *DriverImpl) Stmt(connID, q string) (_ string, err error) {
var stmt driver.Stmt
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
stmt, err = innerConn.(driver.Conn).Prepare(q)
return err
})
if err != nil {
return "", err
}
stID := model.NewId()
d.stMut.Lock()
d.stMap[stID] = stmt
d.stMut.Unlock()
return stID, nil
}
func (d *DriverImpl) StmtClose(stID string) error {
d.stMut.Lock()
err := d.stMap[stID].Close()
delete(d.stMap, stID)
d.stMut.Unlock()
return err
}
func (d *DriverImpl) StmtNumInput(stID string) int {
d.stMut.RLock()
defer d.stMut.RUnlock()
return d.stMap[stID].NumInput()
}
func (d *DriverImpl) StmtQuery(stID string, args []driver.NamedValue) (string, error) {
argVals := make([]driver.Value, len(args))
for i, a := range args {
argVals[i] = a.Value
}
d.stMut.RLock()
st := d.stMap[stID]
d.stMut.RUnlock()
rows, err := st.Query(argVals) //nolint:staticcheck
if err != nil {
return "", err
}
rowsID := model.NewId()
d.rowsMut.Lock()
d.rowsMap[rowsID] = rows
d.rowsMut.Unlock()
return rowsID, nil
}
func (d *DriverImpl) StmtExec(stID string, args []driver.NamedValue) (plugin.ResultContainer, error) {
argVals := make([]driver.Value, len(args))
for i, a := range args {
argVals[i] = a.Value
}
var ret plugin.ResultContainer
d.stMut.RLock()
st := d.stMap[stID]
d.stMut.RUnlock()
res, err := st.Exec(argVals) //nolint:staticcheck
if err != nil {
return ret, err
}
ret.LastID, ret.LastIDError = res.LastInsertId()
ret.RowsAffected, ret.RowsAffectedError = res.RowsAffected()
return ret, nil
}
func (d *DriverImpl) RowsColumns(rowsID string) []string {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].Columns()
}
func (d *DriverImpl) RowsClose(rowsID string) error {
d.rowsMut.Lock()
defer d.rowsMut.Unlock()
err := d.rowsMap[rowsID].Close()
delete(d.rowsMap, rowsID)
return err
}
func (d *DriverImpl) RowsNext(rowsID string, dest []driver.Value) error {
d.rowsMut.RLock()
rows := d.rowsMap[rowsID]
d.rowsMut.RUnlock()
return rows.Next(dest)
}
func (d *DriverImpl) RowsHasNextResultSet(rowsID string) bool {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsNextResultSet).HasNextResultSet()
}
func (d *DriverImpl) RowsNextResultSet(rowsID string) error {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsNextResultSet).NextResultSet()
}
func (d *DriverImpl) RowsColumnTypeDatabaseTypeName(rowsID string, index int) string {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsColumnTypeDatabaseTypeName).ColumnTypeDatabaseTypeName(index)
}
func (d *DriverImpl) RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool) {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsColumnTypePrecisionScale).ColumnTypePrecisionScale(index)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
)
func (ch *Channels) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) {
buf, _ := json.Marshal(data)
if ch.srv.platform.Cluster() != nil {
ch.srv.platform.Cluster().SendClusterMessage(&model.ClusterMessage{
Event: event,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
})
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Installing a managed plugin consists of copying the uploaded plugin (*.tar.gz) to the filestore,
// unpacking to the configured local directory (PluginSettings.Directory), and copying any webapp bundle therein
// to the configured local client directory (PluginSettings.ClientDirectory). The unpacking and copy occurs
// each time the server starts, ensuring it remains synchronized with the set of installed plugins.
//
// When a plugin is enabled, all connected websocket clients are notified so as to fetch any webapp bundle and
// load the client-side portion of the plugin. This works well in a single-server system, but requires careful
// coordination in a high-availability cluster with multiple servers. In particular, websocket clients must not be
// notified of the newly enabled plugin until all servers in the cluster have finished unpacking the plugin, otherwise
// the webapp bundle might not yet be available. Ideally, each server would just notify its own set of connected peers
// after it finishes this process, but nothing prevents those clients from re-connecting to a different server behind
// the load balancer that hasn't finished unpacking.
//
// To achieve this coordination, each server instead checks the status of its peers after unpacking. If it finds peers with
// differing versions of the plugin, it skips the notification. If it finds all peers with the same version of the plugin,
// it notifies all websocket clients connected to all peers. There's a small chance that this never occurs if the last
// server to finish unpacking dies before it can announce. There is also a chance that multiple servers decide to notify,
// but the webapp handles this idempotently.
//
// Complicating this flow further are the various means of notifying. In addition to websocket events, there are cluster
// messages between peers. There is a cluster message when the config changes and a plugin is enabled or disabled.
// There is a cluster message when installing or uninstalling a plugin. There is a cluster message when peer's plugin change
// its status. And finally the act of notifying websocket clients is propagated itself via a cluster message.
//
// The key methods involved in handling these notifications are notifyPluginEnabled and notifyPluginStatusesChanged.
// Note that none of this complexity applies to single-server systems or to plugins without a webapp bundle.
//
// Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins.
// Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory).
// Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded
// to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged
// plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/blang/semver"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// managedPluginFileName is the file name of the flag file that marks
// a local plugin folder as "managed" by the file store.
const managedPluginFileName = ".filestore"
// fileStorePluginFolder is the folder name in the file store of the plugin bundles installed.
const fileStorePluginFolder = "plugins"
func (ch *Channels) installPluginFromData(data model.PluginEventData) {
mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id))
pluginSignaturePathMap, appErr := ch.getPluginsFromFolder()
if appErr != nil {
mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr))
return
}
plugin, ok := pluginSignaturePathMap[data.Id]
if !ok {
mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id))
return
}
reader, appErr := ch.srv.fileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
defer reader.Close()
var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return
}
defer signature.Close()
}
manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways)
if appErr != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
if err := ch.notifyPluginEnabled(manifest); err != nil {
mlog.Error("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
}
}
func (ch *Channels) removePluginFromData(data model.PluginEventData) {
mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id))
if err := ch.removePluginLocally(data.Id); err != nil {
mlog.Warn("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
}
// InstallPluginWithSignature verifies and installs plugin.
func (ch *Channels) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) {
return ch.installPlugin(pluginFile, signature, installPluginLocallyAlways)
}
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
installationStrategy := installPluginLocallyOnlyIfNew
if replace {
installationStrategy = installPluginLocallyAlways
}
return a.installPlugin(pluginFile, nil, installationStrategy)
}
func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
return a.ch.installPlugin(pluginFile, signature, installationStrategy)
}
func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
manifest, appErr := ch.installPluginLocally(pluginFile, signature, installationStrategy)
if appErr != nil {
return nil, appErr
}
if manifest == nil {
return nil, nil
}
if signature != nil {
signature.Seek(0, 0)
if _, appErr = ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
}
// Store bundle in the file store to allow access from other servers.
pluginFile.Seek(0, 0)
if _, appErr := ch.srv.writeFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
ch.notifyClusterPluginEvent(
model.ClusterEventInstallPlugin,
model.PluginEventData{
Id: manifest.Id,
},
)
if err := ch.notifyPluginEnabled(manifest); err != nil {
mlog.Warn("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return manifest, nil
}
// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle
// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true.
func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
var pluginFile, signatureFile io.ReadSeeker
prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version)
if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
return nil, appErr
}
if prepackagedPlugin != nil {
fileReader, err := os.Open(prepackagedPlugin.Path)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, fmt.Sprintf("failed to open prepackaged plugin %s: %s", prepackagedPlugin.Path, err.Error()), http.StatusInternalServerError)
}
defer fileReader.Close()
pluginFile = fileReader
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
}
if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace {
var plugin *model.BaseMarketplacePlugin
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
if appErr != nil {
return nil, appErr
}
var prepackagedVersion semver.Version
if prepackagedPlugin != nil {
var err error
prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
marketplaceVersion, err := semver.Parse(plugin.Manifest.Version)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found
downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
signature, err := plugin.DecodeSignature()
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
pluginFile = bytes.NewReader(downloadedPluginBytes)
signatureFile = signature
}
}
if pluginFile == nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
}
if signatureFile == nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
}
manifest, appErr := ch.installPluginWithSignature(pluginFile, signatureFile)
if appErr != nil {
return nil, appErr
}
return manifest, nil
}
type pluginInstallationStrategy int
const (
// installPluginLocallyOnlyIfNew installs the given plugin locally only if no plugin with the same id has been unpacked.
installPluginLocallyOnlyIfNew pluginInstallationStrategy = iota
// installPluginLocallyOnlyIfNewOrUpgrade installs the given plugin locally only if no plugin with the same id has been unpacked, or if such a plugin is older.
installPluginLocallyOnlyIfNewOrUpgrade
// installPluginLocallyAlways unconditionally installs the given plugin locally only, clobbering any existing plugin with the same id.
installPluginLocallyAlways
)
func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
// verify signature
if signature != nil {
if err := ch.verifyPlugin(pluginFile, signature); err != nil {
return nil, err
}
}
tmpDir, err := os.MkdirTemp("", "plugintmp")
if err != nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer os.RemoveAll(tmpDir)
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil {
return nil, appErr
}
manifest, appErr = ch.installExtractedPlugin(manifest, pluginDir, installationStrategy)
if appErr != nil {
return nil, appErr
}
return manifest, nil
}
func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
pluginFile.Seek(0, 0)
if err := extractTarGz(pluginFile, extractDir); err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
dir, err := os.ReadDir(extractDir)
if err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(dir) == 1 && dir[0].IsDir() {
extractDir = filepath.Join(extractDir, dir[0].Name())
}
manifest, _, err := model.FindManifest(extractDir)
if err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if !model.IsValidPluginId(manifest.Id) {
return nil, "", model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]any{"Min": model.MinIdLength, "Max": model.MaxIdLength, "Regex": model.ValidIdRegex}, "", http.StatusBadRequest)
}
return manifest, extractDir, nil
}
func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
bundles, err := pluginsEnvironment.Available()
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Check plugin id is not blocked
if plugin.PluginIDIsBlocked(manifest.Id) {
mlog.Debug("Skipping installation of plugin since plugin is on blocklist", mlog.String("plugin_id", manifest.Id))
return nil, nil
}
// Check for plugins installed with the same ID.
var existingManifest *model.Manifest
for _, bundle := range bundles {
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
existingManifest = bundle.Manifest
break
}
}
if existingManifest != nil {
// Return an error if already installed and strategy disallows installation.
if installationStrategy == installPluginLocallyOnlyIfNew {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
}
// Skip installation if already installed and newer.
if installationStrategy == installPluginLocallyOnlyIfNewOrUpgrade {
var version, existingVersion semver.Version
version, err = semver.Parse(manifest.Version)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
}
existingVersion, err = semver.Parse(existingManifest.Version)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
}
if version.LTE(existingVersion) {
mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id))
return nil, nil
}
}
// Otherwise remove the existing installation prior to install below.
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
if err := ch.removePluginLocally(existingManifest.Id); err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
}
}
pluginPath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id)
err = utils.CopyDir(fromPluginDir, pluginPath)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Flag plugin locally as managed by the filestore.
f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName))
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
f.Close()
if manifest.HasWebapp() {
updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
manifest = updatedManifest
}
// Activate the plugin if enabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[manifest.Id]
if pluginState != nil && pluginState.Enable {
if hasOverride, enabled := ch.getPluginStateOverride(manifest.Id); hasOverride && !enabled {
return manifest, nil
}
updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if updatedManifest == nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, "failed to activate plugin: plugin already active", http.StatusInternalServerError)
}
manifest = updatedManifest
}
mlog.Debug("Installing plugin", mlog.String("plugin_id", manifest.Id), mlog.String("version", manifest.Version))
return manifest, nil
}
func (ch *Channels) RemovePlugin(id string) *model.AppError {
// Disable plugin before removal to make sure this
// plugin remains disabled on re-install.
if err := ch.disablePlugin(id); err != nil {
return err
}
if err := ch.removePluginLocally(id); err != nil {
return err
}
// Remove bundle from the file store.
storePluginFileName := getBundleStorePath(id)
bundleExist, err := ch.srv.fileExists(storePluginFileName)
if err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !bundleExist {
return nil
}
if err = ch.srv.removeFile(storePluginFileName); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err = ch.removeSignature(id); err != nil {
mlog.Warn("Can't remove signature", mlog.Err(err))
}
ch.notifyClusterPluginEvent(
model.ClusterEventRemovePlugin,
model.PluginEventData{
Id: id,
},
)
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return nil
}
func (ch *Channels) removePluginLocally(id string) *model.AppError {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
var manifest *model.Manifest
var pluginPath string
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
manifest = p.Manifest
pluginPath = filepath.Dir(p.ManifestPath)
break
}
}
if manifest == nil {
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
pluginsEnvironment.Deactivate(id)
pluginsEnvironment.RemovePlugin(id)
ch.unregisterPluginCommands(id)
if err := os.RemoveAll(pluginPath); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (ch *Channels) removeSignature(pluginID string) *model.AppError {
filePath := getSignatureStorePath(pluginID)
exists, err := ch.srv.fileExists(filePath)
if err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !exists {
mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginID))
return nil
}
if err = ch.srv.removeFile(filePath); err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func getBundleStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
}
func getSignatureStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz.sig", id))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/sha256"
"encoding/base64"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func getKeyHash(key string) string {
hash := sha256.New()
hash.Write([]byte(key))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func (a *App) SetPluginKey(pluginID string, key string, value []byte) *model.AppError {
return a.SetPluginKeyWithExpiry(pluginID, key, value, 0)
}
func (a *App) SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError {
options := model.PluginKVSetOptions{
ExpireInSeconds: expireInSeconds,
}
_, err := a.SetPluginKeyWithOptions(pluginID, key, value, options)
return err
}
func (a *App) CompareAndSetPluginKey(pluginID string, key string, oldValue, newValue []byte) (bool, *model.AppError) {
options := model.PluginKVSetOptions{
Atomic: true,
OldValue: oldValue,
}
return a.SetPluginKeyWithOptions(pluginID, key, newValue, options)
}
func (a *App) SetPluginKeyWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
return a.Srv().Platform().SetPluginKeyWithOptions(pluginID, key, value, options)
}
func (a *App) CompareAndDeletePluginKey(pluginID string, key string, oldValue []byte) (bool, *model.AppError) {
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
}
deleted, err := a.Srv().Store().Plugin().CompareAndDelete(kv, oldValue)
if err != nil {
mlog.Error("Failed to compare and delete plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return deleted, appErr
default:
return false, model.NewAppError("CompareAndDeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Clean up a previous entry using the hashed key, if it exists.
if err := a.Srv().Store().Plugin().Delete(pluginID, getKeyHash(key)); err != nil {
mlog.Warn("Failed to clean up previously hashed plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
}
return deleted, nil
}
// TODO: platform: remove
func (s *Server) getPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
if kv, err := s.Store().Plugin().Get(pluginID, key); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Lookup using the hashed version of the key for keys written prior to v5.6.
if kv, err := s.Store().Plugin().Get(pluginID, getKeyHash(key)); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value using hashed key", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, nil
}
func (a *App) GetPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
return a.Srv().getPluginKey(pluginID, key)
}
func (a *App) DeletePluginKey(pluginID string, key string) *model.AppError {
return a.Srv().Platform().DeletePluginKey(pluginID, key)
}
func (a *App) DeleteAllKeysForPlugin(pluginID string) *model.AppError {
if err := a.Srv().Store().Plugin().DeleteAllForPlugin(pluginID); err != nil {
mlog.Error("Failed to delete all plugin key values", mlog.String("plugin_id", pluginID), mlog.Err(err))
return model.NewAppError("DeleteAllKeysForPlugin", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) DeleteAllExpiredPluginKeys() *model.AppError {
if a.Srv() == nil {
return nil
}
if err := a.Srv().Store().Plugin().DeleteAllExpired(); err != nil {
mlog.Error("Failed to delete all expired plugin key values", mlog.Err(err))
return model.NewAppError("DeleteAllExpiredPluginKeys", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) ListPluginKeys(pluginID string, page, perPage int) ([]string, *model.AppError) {
return a.Srv().Platform().ListPluginKeys(pluginID, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"io"
"net/http"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
if handler, ok := ch.routerSvc.getHandler(params["plugin_id"]); ok {
ch.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) {
handler.ServeHTTP(w, r)
})
return
}
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
mlog.Error(err.Error())
w.WriteHeader(err.StatusCode)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(err.ToJSON()))
return
}
hooks, err := pluginsEnvironment.HooksForPlugin(params["plugin_id"])
if err != nil {
mlog.Debug("Access to route for non-existent plugin",
mlog.String("missing_plugin_id", params["plugin_id"]),
mlog.String("url", r.URL.String()),
mlog.Err(err))
http.NotFound(w, r)
return
}
ch.servePluginRequest(w, r, hooks.ServeHTTP)
}
func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) {
pluginsEnvironment := a.ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
err := model.NewAppError("ServeInterPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented)
a.Log().Error(err.Error())
w.WriteHeader(err.StatusCode)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(err.ToJSON()))
return
}
hooks, err := pluginsEnvironment.HooksForPlugin(destinationPluginId)
if err != nil {
a.Log().Error("Access to route for non-existent plugin in inter plugin request",
mlog.String("source_plugin_id", sourcePluginId),
mlog.String("destination_plugin_id", destinationPluginId),
mlog.String("url", r.URL.String()),
mlog.Err(err),
)
http.NotFound(w, r)
return
}
context := &plugin.Context{
RequestId: model.NewId(),
UserAgent: r.UserAgent(),
}
r.Header.Set("Mattermost-Plugin-ID", sourcePluginId)
hooks.ServeHTTP(context, w, r)
}
// ServePluginPublicRequest serves public plugin files
// at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything}
func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
// Should be in the form of /(subpath/)?/plugins/{plugin_id}/public/* by the time we get here
vars := mux.Vars(r)
pluginID := vars["plugin_id"]
pluginsEnv := ch.GetPluginsEnvironment()
// Check if someone has nullified the pluginsEnv in the meantime
if pluginsEnv == nil {
http.NotFound(w, r)
return
}
publicFilesPath, err := pluginsEnv.PublicFilesPath(pluginID)
if err != nil {
http.NotFound(w, r)
return
}
subpath, err := utils.GetSubpathFromConfig(ch.cfgSvc.Config())
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
publicFilePath := path.Clean(r.URL.Path)
prefix := path.Join(subpath, "plugins", pluginID, "public")
if !strings.HasPrefix(publicFilePath, prefix) {
http.NotFound(w, r)
return
}
publicFile := filepath.Join(publicFilesPath, strings.TrimPrefix(publicFilePath, prefix))
http.ServeFile(w, r, publicFile)
}
func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) {
token := ""
context := &plugin.Context{
RequestId: model.NewId(),
IPAddress: utils.GetIPAddress(r, ch.cfgSvc.Config().ServiceSettings.TrustedProxyIPHeader),
AcceptLanguage: r.Header.Get("Accept-Language"),
UserAgent: r.UserAgent(),
}
cookieAuth := false
authHeader := r.Header.Get(model.HeaderAuth)
if strings.HasPrefix(strings.ToUpper(authHeader), model.HeaderBearer+" ") {
token = authHeader[len(model.HeaderBearer)+1:]
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HeaderToken+" ") {
token = authHeader[len(model.HeaderToken)+1:]
} else if cookie, _ := r.Cookie(model.SessionCookieToken); cookie != nil {
token = cookie.Value
cookieAuth = true
} else {
token = r.URL.Query().Get("access_token")
}
// Mattermost-Plugin-ID can only be set by inter-plugin requests
r.Header.Del("Mattermost-Plugin-ID")
r.Header.Del("Mattermost-User-Id")
if token != "" {
session, err := New(ServerConnector(ch)).GetSession(token)
defer ch.srv.platform.ReturnSessionToPool(session)
csrfCheckPassed := false
if session != nil && err == nil && cookieAuth && r.Method != "GET" {
sentToken := ""
if r.Header.Get(model.HeaderCsrfToken) == "" {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
r.ParseForm()
sentToken = r.FormValue("csrf")
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
} else {
sentToken = r.Header.Get(model.HeaderCsrfToken)
}
expectedToken := session.GetCSRF()
if sentToken == expectedToken {
csrfCheckPassed = true
}
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML && !csrfCheckPassed {
csrfErrorMessage := "CSRF Check failed for request - Please migrate your plugin to either send a CSRF Header or Form Field, XMLHttpRequest is deprecated"
sid := ""
userID := ""
if session.Id != "" {
sid = session.Id
userID = session.UserId
}
fields := []mlog.Field{
mlog.String("path", r.URL.Path),
mlog.String("ip", r.RemoteAddr),
mlog.String("session_id", sid),
mlog.String("user_id", userID),
}
if *ch.cfgSvc.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
mlog.Warn(csrfErrorMessage, fields...)
} else {
mlog.Debug(csrfErrorMessage, fields...)
csrfCheckPassed = true
}
}
} else {
csrfCheckPassed = true
}
if (session != nil && session.Id != "") && err == nil && csrfCheckPassed {
r.Header.Set("Mattermost-User-Id", session.UserId)
context.SessionId = session.Id
}
}
cookies := r.Cookies()
r.Header.Del("Cookie")
for _, c := range cookies {
if c.Name != model.SessionCookieToken {
r.AddCookie(c)
}
}
r.Header.Del(model.HeaderAuth)
r.Header.Del("Referer")
params := mux.Vars(r)
subpath, _ := utils.GetSubpathFromConfig(ch.cfgSvc.Config())
newQuery := r.URL.Query()
newQuery.Del("access_token")
r.URL.RawQuery = newQuery.Encode()
r.URL.Path = strings.TrimPrefix(r.URL.Path, path.Join(subpath, "plugins", params["plugin_id"]))
handler(context, w, r)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"io"
"net/http"
"path/filepath"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp" //nolint:staticcheck
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GetPublicKey will return the actual public key saved in the `name` file.
func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) {
return a.Srv().getPublicKey(name)
}
func (s *Server) getPublicKey(name string) ([]byte, *model.AppError) {
data, err := s.platform.GetConfigFile(name)
if err != nil {
return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
// AddPublicKey will add plugin public key to the config. Overwrites the previous file
func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError {
if isSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
data, err := io.ReadAll(key)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(name, data)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.UpdateConfig(func(cfg *model.Config) {
if !utils.StringInSlice(name, cfg.PluginSettings.SignaturePublicKeyFiles) {
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, name)
}
})
return nil
}
// DeletePublicKey will delete plugin public key from the config.
func (a *App) DeletePublicKey(name string) *model.AppError {
if isSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
filename := filepath.Base(name)
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.SignaturePublicKeyFiles = utils.RemoveStringFromSlice(filename, cfg.PluginSettings.SignaturePublicKeyFiles)
})
return nil
}
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
return a.ch.verifyPlugin(plugin, signature)
}
func (ch *Channels) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
return nil
}
publicKeys := ch.cfgSvc.Config().PluginSettings.SignaturePublicKeyFiles
for _, pk := range publicKeys {
pkBytes, appErr := ch.srv.getPublicKey(pk)
if appErr != nil {
mlog.Warn("Unable to get public key for ", mlog.String("filename", pk))
continue
}
publicKey := bytes.NewReader(pkBytes)
plugin.Seek(0, 0)
signature.Seek(0, 0)
if err := verifySignature(publicKey, plugin, signature); err == nil {
return nil
}
}
return model.NewAppError("VerifyPlugin", "api.plugin.verify_plugin.app_error", nil, "", http.StatusInternalServerError)
}
func verifySignature(publicKey, message, signature io.Reader) error {
pk, err := decodeIfArmored(publicKey)
if err != nil {
return errors.Wrap(err, "can't decode public key")
}
s, err := decodeIfArmored(signature)
if err != nil {
return errors.Wrap(err, "can't decode signature")
}
return verifyBinarySignature(pk, message, s)
}
func verifyBinarySignature(publicKey, signedFile, signature io.Reader) error {
keyring, err := openpgp.ReadKeyRing(publicKey)
if err != nil {
return errors.Wrap(err, "can't read public key")
}
if _, err = openpgp.CheckDetachedSignature(keyring, signedFile, signature); err != nil {
return errors.Wrap(err, "error while checking the signature")
}
return nil
}
func decodeIfArmored(reader io.Reader) (io.Reader, error) {
readBytes, err := io.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "can't read the file")
}
block, err := armor.Decode(bytes.NewReader(readBytes))
if err != nil {
return bytes.NewReader(readBytes), nil
}
return block.Body, nil
}
// isSamlFile checks if filename is a SAML file.
func isSamlFile(saml *model.SamlSettings, filename string) bool {
return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
// GetPluginStatus returns the status for a plugin installed on this server.
func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPluginStatus", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := pluginsEnvironment.Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatus", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, status := range pluginStatuses {
if status.PluginId == id {
// Add our cluster ID
if ch.srv.platform.Cluster() != nil {
status.ClusterId = ch.srv.platform.Cluster().GetClusterId()
}
return status, nil
}
}
return nil, model.NewAppError("GetPluginStatus", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
// GetPluginStatus returns the status for a plugin installed on this server.
func (a *App) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
return a.ch.GetPluginStatus(id)
}
// GetPluginStatuses returns the status for plugins installed on this server.
func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := pluginsEnvironment.Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Add our cluster ID
for _, status := range pluginStatuses {
if ch.srv.platform.Cluster() != nil {
status.ClusterId = ch.srv.platform.Cluster().GetClusterId()
} else {
status.ClusterId = ""
}
}
return pluginStatuses, nil
}
// GetPluginStatuses returns the status for plugins installed on this server.
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
return a.ch.GetPluginStatuses()
}
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
return a.ch.getClusterPluginStatuses()
}
func (ch *Channels) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
pluginStatuses, err := ch.GetPluginStatuses()
if err != nil {
return nil, err
}
if ch.srv.platform.Cluster() != nil && *ch.cfgSvc.Config().ClusterSettings.Enable {
clusterPluginStatuses, err := ch.srv.platform.Cluster().GetPluginStatuses()
if err != nil {
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
}
return pluginStatuses, nil
}
func (ch *Channels) notifyPluginStatusesChanged() error {
pluginStatuses, err := ch.getClusterPluginStatuses()
if err != nil {
return err
}
// Notify any system admins.
message := model.NewWebSocketEvent(model.WebsocketEventPluginStatusesChanged, "", "", "", nil, "")
message.Add("plugin_statuses", pluginStatuses)
message.GetBroadcast().ContainsSensitiveData = true
ch.srv.platform.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
PendingPostIDsCacheSize = 25000
PendingPostIDsCacheTTL = 30 * time.Second
PageDefault = 0
)
var atMentionPattern = regexp.MustCompile(`\B@`)
// Ensure post service wrapper implements `product.PostService`
var _ product.PostService = (*postServiceWrapper)(nil)
// postServiceWrapper provides an implementation of `product.PostService` for use by products.
type postServiceWrapper struct {
app AppIface
}
func (s *postServiceWrapper) CreatePost(ctx *request.Context, post *model.Post) (*model.Post, *model.AppError) {
return s.app.CreatePostMissingChannel(ctx, post, true, true)
}
func (s *postServiceWrapper) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
return s.app.GetPostsByIds(postIDs)
}
func (s *postServiceWrapper) SendEphemeralPost(ctx *request.Context, userID string, post *model.Post) *model.Post {
return s.app.SendEphemeralPost(ctx, userID, post)
}
func (s *postServiceWrapper) GetPost(postID string) (*model.Post, *model.AppError) {
return s.app.GetSinglePost(postID, false)
}
func (s *postServiceWrapper) DeletePost(ctx *request.Context, postID, productID string) (*model.Post, *model.AppError) {
return s.app.DeletePost(ctx, postID, productID)
}
func (s *postServiceWrapper) UpdatePost(ctx *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
return s.app.UpdatePost(ctx, post, false)
}
func (a *App) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError) {
// Check that channel has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(post.ChannelId, true)
if errCh != nil {
err := model.NewAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]any{"Name": "post.channel_id"}, "", http.StatusBadRequest).Wrap(errCh)
return nil, err
}
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
rp, err := a.CreatePost(c, post, channel, true, setOnline)
if err != nil {
if err.Id == "api.post.create_post.root_id.app_error" ||
err.Id == "api.post.create_post.channel_root_id.app_error" {
err.StatusCode = http.StatusBadRequest
}
return nil, err
}
// Update the Channel LastViewAt only if:
// the post does NOT have from_webhook prop set (e.g. Zapier app), and
// the post does NOT have from_bot set (e.g. from discovering the user is a bot within CreatePost), and
// the post is NOT a reply post with CRT enabled
_, fromWebhook := post.GetProps()["from_webhook"]
_, fromBot := post.GetProps()["from_bot"]
isCRTReply := post.RootId != "" && a.IsCRTEnabledForUser(c, post.UserId)
if !fromWebhook && !fromBot && !isCRTReply {
if _, err := a.MarkChannelsAsViewed(c, []string{post.ChannelId}, post.UserId, currentSessionId, true); err != nil {
c.Logger().Warn(
"Encountered error updating last viewed",
mlog.String("channel_id", post.ChannelId),
mlog.String("user_id", post.UserId),
mlog.Err(err),
)
}
}
return rp, nil
}
func (a *App) CreatePostMissingChannel(c request.CTX, post *model.Post, triggerWebhooks bool, setOnline bool) (*model.Post, *model.AppError) {
channel, err := a.Srv().Store().Channel().Get(post.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CreatePostMissingChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CreatePostMissingChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return a.CreatePost(c, post, channel, triggerWebhooks, setOnline)
}
// deduplicateCreatePost attempts to make posting idempotent within a caching window.
func (a *App) deduplicateCreatePost(post *model.Post) (foundPost *model.Post, err *model.AppError) {
// We rely on the client sending the pending post id across "duplicate" requests. If there
// isn't one, we can't deduplicate, so allow creation normally.
if post.PendingPostId == "" {
return nil, nil
}
const unknownPostId = ""
// Query the cache atomically for the given pending post id, saving a record if
// it hasn't previously been seen.
var postID string
nErr := a.Srv().seenPendingPostIdsCache.Get(post.PendingPostId, &postID)
if nErr == cache.ErrKeyNotFound {
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, unknownPostId, PendingPostIDsCacheTTL)
return nil, nil
}
if nErr != nil {
return nil, model.NewAppError("errorGetPostId", "api.post.error_get_post_id.pending", nil, "", http.StatusInternalServerError)
}
// If another thread saved the cache record, but hasn't yet updated it with the actual post
// id (because it's still saving), notify the client with an error. Ideally, we'd wait
// for the other thread, but coordinating that adds complexity to the happy path.
if postID == unknownPostId {
return nil, model.NewAppError("deduplicateCreatePost", "api.post.deduplicate_create_post.pending", nil, "", http.StatusInternalServerError)
}
// If the other thread finished creating the post, return the created post back to the
// client, making the API call feel idempotent.
actualPost, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, model.NewAppError("deduplicateCreatePost", "api.post.deduplicate_create_post.failed_to_get", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Debug("Deduplicated create post", mlog.String("post_id", actualPost.Id), mlog.String("pending_post_id", post.PendingPostId))
return actualPost, nil
}
func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks, setOnline bool) (savedPost *model.Post, err *model.AppError) {
foundPost, err := a.deduplicateCreatePost(post)
if err != nil {
return nil, err
}
if foundPost != nil {
return foundPost, nil
}
// If we get this far, we've recorded the client-provided pending post id to the cache.
// Remove it if we fail below, allowing a proper retry by the client.
defer func() {
if post.PendingPostId == "" {
return
}
if err != nil {
a.Srv().seenPendingPostIdsCache.Remove(post.PendingPostId)
return
}
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, savedPost.Id, PendingPostIDsCacheTTL)
}()
post.SanitizeProps()
var pchan chan store.StoreResult
if post.RootId != "" {
pchan = make(chan store.StoreResult, 1)
go func() {
r, pErr := a.Srv().Store().Post().Get(sqlstore.WithMaster(context.Background()), post.RootId, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
pchan <- store.StoreResult{Data: r, NErr: pErr}
close(pchan)
}()
}
user, nErr := a.Srv().Store().User().Get(context.Background(), post.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreatePost", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreatePost", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.IsBot {
post.AddProp("from_bot", "true")
}
if c.Session().IsOAuth {
post.AddProp("from_oauth_app", "true")
}
var ephemeralPost *model.Post
if post.Type == "" && !a.HasPermissionToChannel(c, user.Id, channel.Id, model.PermissionUseChannelMentions) {
mention := post.DisableMentionHighlights()
if mention != "" {
T := i18n.GetUserTranslations(user.Locale)
ephemeralPost = &model.Post{
UserId: user.Id,
RootId: post.RootId,
ChannelId: channel.Id,
Message: T("model.post.channel_notifications_disabled_in_channel.message", model.StringInterface{"ChannelName": channel.Name, "Mention": mention}),
Props: model.StringInterface{model.PostPropsMentionHighlightDisabled: true},
}
}
}
// Verify the parent/child relationships are correct
var parentPostList *model.PostList
if pchan != nil {
result := <-pchan
if result.NErr != nil {
return nil, model.NewAppError("createPost", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
}
parentPostList = result.Data.(*model.PostList)
if len(parentPostList.Posts) == 0 || !parentPostList.IsChannelId(post.ChannelId) {
return nil, model.NewAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "", http.StatusInternalServerError)
}
rootPost := parentPostList.Posts[post.RootId]
if rootPost.RootId != "" {
return nil, model.NewAppError("createPost", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
}
}
post.Hashtags, _ = model.ParseHashtags(post.Message)
if err = a.FillInPostProps(c, post, channel); err != nil {
return nil, err
}
// Temporary fix so old plugins don't clobber new fields in SlackAttachment struct, see MM-13088
if attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment); ok {
jsonAttachments, err := json.Marshal(attachments)
if err == nil {
attachmentsInterface := []any{}
err = json.Unmarshal(jsonAttachments, &attachmentsInterface)
post.AddProp("attachments", attachmentsInterface)
}
if err != nil {
c.Logger().Warn("Could not convert post attachments to map interface.", mlog.Err(err))
}
}
if !a.isPostPriorityEnabled() && post.GetPriority() != nil {
post.Metadata.Priority = nil
}
var metadata *model.PostMetadata
if post.Metadata != nil {
metadata = post.Metadata.Copy()
}
var rejectionError *model.AppError
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin())
if rejectionReason != "" {
id := "Post rejected by plugin. " + rejectionReason
if rejectionReason == plugin.DismissPostError {
id = plugin.DismissPostError
}
rejectionError = model.NewAppError("createPost", id, nil, "", http.StatusBadRequest)
return false
}
if replacementPost != nil {
post = replacementPost
if post.Metadata != nil && metadata != nil {
post.Metadata.Priority = metadata.Priority
} else {
post.Metadata = metadata
}
}
return true
}, plugin.MessageWillBePostedID)
if rejectionError != nil {
return nil, rejectionError
}
// Pre-fill the CreateAt field for link previews to get the correct timestamp.
if post.CreateAt == 0 {
post.CreateAt = model.GetMillis()
}
post = a.getEmbedsAndImages(c, post, true)
previewPost := post.GetPreviewPost()
if previewPost != nil {
post.AddProp(model.PostPropsPreviewedPost, previewPost.PostID)
}
rpost, nErr := a.Srv().Store().Post().Save(post)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
return nil, model.NewAppError("CreatePost", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreatePost", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Update the mapping from pending post id to the actual post id, for any clients that
// might be duplicating requests.
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, rpost.Id, PendingPostIDsCacheTTL)
// We make a copy of the post for the plugin hook to avoid a race condition,
// and to remove the non-GOB-encodable Metadata from it.
pluginPost := rpost.ForPlugin()
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.MessageHasBeenPosted(pluginContext, pluginPost)
return true
}, plugin.MessageHasBeenPostedID)
})
if a.Metrics() != nil {
a.Metrics().IncrementPostCreate()
}
if len(post.FileIds) > 0 {
if err = a.attachFilesToPost(post); err != nil {
c.Logger().Warn("Encountered error attaching files to post", mlog.String("post_id", post.Id), mlog.Any("file_ids", post.FileIds), mlog.Err(err))
}
if a.Metrics() != nil {
a.Metrics().IncrementPostFileAttachment(len(post.FileIds))
}
}
// Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs
// to be done when we send the post over the websocket in handlePostEvents
// PS: we don't want to include PostPriority from the db to avoid the replica lag,
// so we just return the one that was passed with post
rpost = a.PreparePostForClient(c, rpost, true, false, false)
// Make sure poster is following the thread
if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" {
_, err := a.Srv().Store().Thread().MaintainMembership(user.Id, rpost.RootId, store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
})
if err != nil {
c.Logger().Warn("Failed to update thread membership", mlog.Err(err))
}
}
if err := a.handlePostEvents(c, rpost, user, channel, triggerWebhooks, parentPostList, setOnline); err != nil {
c.Logger().Warn("Failed to handle post events", mlog.Err(err))
}
// Send any ephemeral posts after the post is created to ensure it shows up after the latest post created
if ephemeralPost != nil {
a.SendEphemeralPost(c, post.UserId, ephemeralPost)
}
rpost, err = a.SanitizePostMetadataForUser(c, rpost, c.Session().UserId)
if err != nil {
return nil, err
}
return rpost, nil
}
func (a *App) addPostPreviewProp(post *model.Post) (*model.Post, error) {
previewPost := post.GetPreviewPost()
if previewPost != nil {
updatedPost := post.Clone()
updatedPost.AddProp(model.PostPropsPreviewedPost, previewPost.PostID)
updatedPost, err := a.Srv().Store().Post().Update(updatedPost, post)
return updatedPost, err
}
return post, nil
}
func (a *App) attachFilesToPost(post *model.Post) *model.AppError {
var attachedIds []string
for _, fileID := range post.FileIds {
err := a.Srv().Store().FileInfo().AttachToPost(fileID, post.Id, post.ChannelId, post.UserId)
if err != nil {
mlog.Warn("Failed to attach file to post", mlog.String("file_id", fileID), mlog.String("post_id", post.Id), mlog.Err(err))
continue
}
attachedIds = append(attachedIds, fileID)
}
if len(post.FileIds) != len(attachedIds) {
// We couldn't attach all files to the post, so ensure that post.FileIds reflects what was actually attached
post.FileIds = attachedIds
if _, err := a.Srv().Store().Post().Overwrite(post); err != nil {
return model.NewAppError("attachFilesToPost", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
// FillInPostProps should be invoked before saving posts to fill in properties such as
// channel_mentions.
//
// If channel is nil, FillInPostProps will look up the channel corresponding to the post.
func (a *App) FillInPostProps(c request.CTX, post *model.Post, channel *model.Channel) *model.AppError {
channelMentions := post.ChannelMentions()
channelMentionsProp := make(map[string]any)
if len(channelMentions) > 0 {
if channel == nil {
postChannel, err := a.Srv().Store().Channel().GetForPost(post.Id)
if err != nil {
return model.NewAppError("FillInPostProps", "api.context.invalid_param.app_error", map[string]any{"Name": "post.channel_id"}, "", http.StatusBadRequest).Wrap(err)
}
channel = postChannel
}
mentionedChannels, err := a.GetChannelsByNames(c, channelMentions, channel.TeamId)
if err != nil {
return err
}
for _, mentioned := range mentionedChannels {
if mentioned.Type == model.ChannelTypeOpen {
team, err := a.Srv().Store().Team().Get(mentioned.TeamId)
if err != nil {
mlog.Warn("Failed to get team of the channel mention", mlog.String("team_id", channel.TeamId), mlog.String("channel_id", channel.Id), mlog.Err(err))
continue
}
channelMentionsProp[mentioned.Name] = map[string]any{
"display_name": mentioned.DisplayName,
"team_name": team.Name,
}
}
}
}
if len(channelMentionsProp) > 0 {
post.AddProp("channel_mentions", channelMentionsProp)
} else if post.GetProps() != nil {
post.DelProp("channel_mentions")
}
matched := atMentionPattern.MatchString(post.Message)
if a.Srv().License() != nil && *a.Srv().License().Features.LDAPGroups && matched && !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseGroupMentions) {
post.AddProp(model.PostPropsGroupHighlightDisabled, true)
}
return nil
}
func (a *App) handlePostEvents(c request.CTX, post *model.Post, user *model.User, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList, setOnline bool) error {
var team *model.Team
if channel.TeamId != "" {
t, err := a.Srv().Store().Team().Get(channel.TeamId)
if err != nil {
return err
}
team = t
} else {
// Blank team for DMs
team = &model.Team{}
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
a.invalidateCacheForChannelPosts(channel.Id)
if _, err := a.SendNotifications(c, post, team, channel, user, parentPostList, setOnline); err != nil {
return err
}
if post.Type != model.PostTypeAutoResponder { // don't respond to an auto-responder
a.Srv().Go(func() {
_, err := a.SendAutoResponseIfNecessary(c, channel, user, post)
if err != nil {
mlog.Error("Failed to send auto response", mlog.String("user_id", user.Id), mlog.String("post_id", post.Id), mlog.Err(err))
}
})
}
if triggerWebhooks {
a.Srv().Go(func() {
if err := a.handleWebhookEvents(c, post, team, channel, user); err != nil {
mlog.Error(err.Error())
}
})
}
return nil
}
func (a *App) SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
post.Type = model.PostTypeEphemeral
// fill in fields which haven't been specified which have sensible defaults
if post.Id == "" {
post.Id = model.NewId()
}
if post.CreateAt == 0 {
post.CreateAt = model.GetMillis()
}
if post.GetProps() == nil {
post.SetProps(make(model.StringInterface))
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", post.ChannelId, userID, nil, "")
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return post
}
func (a *App) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
post.Type = model.PostTypeEphemeral
post.UpdateAt = model.GetMillis()
if post.GetProps() == nil {
post.SetProps(make(model.StringInterface))
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", post.ChannelId, userID, nil, "")
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return post
}
func (a *App) DeleteEphemeralPost(userID, postID string) {
post := &model.Post{
Id: postID,
UserId: userID,
Type: model.PostTypeEphemeral,
DeleteAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
message := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", "", userID, nil, "")
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
}
func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
post.SanitizeProps()
postLists, nErr := a.Srv().Store().Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
if nErr != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
oldPost := postLists.Posts[post.Id]
var err *model.AppError
if oldPost == nil {
err = model.NewAppError("UpdatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id, http.StatusBadRequest)
return nil, err
}
if oldPost.DeleteAt != 0 {
err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_details.app_error", map[string]any{"PostId": post.Id}, "", http.StatusBadRequest)
return nil, err
}
if oldPost.IsSystemMessage() {
err = model.NewAppError("UpdatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id, http.StatusBadRequest)
return nil, err
}
channel, err := a.GetChannel(c, oldPost.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt != 0 {
return nil, model.NewAppError("UpdatePost", "api.post.update_post.can_not_update_post_in_deleted.error", nil, "", http.StatusBadRequest)
}
newPost := oldPost.Clone()
if newPost.Message != post.Message {
newPost.Message = post.Message
newPost.EditAt = model.GetMillis()
newPost.Hashtags, _ = model.ParseHashtags(post.Message)
}
if !safeUpdate {
newPost.IsPinned = post.IsPinned
newPost.HasReactions = post.HasReactions
newPost.FileIds = post.FileIds
newPost.SetProps(post.GetProps())
}
// Avoid deep-equal checks if EditAt was already modified through message change
if newPost.EditAt == oldPost.EditAt && (!oldPost.FileIds.Equals(newPost.FileIds) || !oldPost.AttachmentsEqual(newPost)) {
newPost.EditAt = model.GetMillis()
}
if err = a.FillInPostProps(c, post, nil); err != nil {
return nil, err
}
if post.IsRemote() {
oldPost.RemoteId = model.NewString(*post.RemoteId)
}
var rejectionReason string
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin())
return post != nil
}, plugin.MessageWillBeUpdatedID)
if newPost == nil {
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
}
// Restore the post metadata that was stripped by the plugin. Set it to
// the last known good.
newPost.Metadata = oldPost.Metadata
rpost, nErr := a.Srv().Store().Post().Update(newPost, oldPost)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdatePost", "app.post.update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
pluginOldPost := oldPost.ForPlugin()
pluginNewPost := newPost.ForPlugin()
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost)
return true
}, plugin.MessageHasBeenUpdatedID)
})
rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true, true)
// Ensure IsFollowing is nil since this updated post will be broadcast to all users
// and we don't want to have to populate it for every single user and broadcast to each
// individually.
rpost.IsFollowing = nil
rpost, nErr = a.addPostPreviewProp(rpost)
if nErr != nil {
return nil, model.NewAppError("UpdatePost", "app.post.update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", rpost.ChannelId, "", nil, "")
postJSON, jsonErr := rpost.ToJSON()
if jsonErr != nil {
return nil, model.NewAppError("UpdatePost", "app.post.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("post", postJSON)
published, err := a.publishWebsocketEventForPermalinkPost(c, rpost, message)
if err != nil {
return nil, err
}
if !published {
a.Publish(message)
}
a.invalidateCacheForChannelPosts(rpost.ChannelId)
return rpost, nil
}
func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.Post, message *model.WebSocketEvent) (published bool, err *model.AppError) {
var previewedPostID string
if val, ok := post.GetProp(model.PostPropsPreviewedPost).(string); ok {
previewedPostID = val
} else {
return false, nil
}
if !model.IsValidId(previewedPostID) {
mlog.Warn("invalid post prop value", mlog.String("prop_key", model.PostPropsPreviewedPost), mlog.String("prop_value", previewedPostID))
return false, nil
}
previewedPost, err := a.GetSinglePost(previewedPostID, false)
if err != nil {
if err.StatusCode == http.StatusNotFound {
mlog.Warn("permalinked post not found", mlog.String("referenced_post_id", previewedPostID))
return false, nil
}
return false, err
}
channelMembers, err := a.GetChannelMembersPage(c, post.ChannelId, 0, 10000000)
if err != nil {
return false, err
}
permalinkPreviewedChannel, err := a.GetChannel(c, previewedPost.ChannelId)
if err != nil {
if err.StatusCode == http.StatusNotFound {
mlog.Warn("channel containing permalinked post not found", mlog.String("referenced_channel_id", previewedPost.ChannelId))
return false, nil
}
return false, err
}
permalinkPreviewedPost := post.GetPreviewPost()
for _, cm := range channelMembers {
if permalinkPreviewedPost != nil {
post.Metadata.Embeds[0].Data = permalinkPreviewedPost
}
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, cm.UserId)
// Using DeepCopy here to avoid a race condition
// between publishing the event and setting the "post" data value below.
messageCopy := message.DeepCopy()
broadcastCopy := messageCopy.GetBroadcast()
broadcastCopy.UserId = cm.UserId
messageCopy.SetBroadcast(broadcastCopy)
postJSON, jsonErr := postForUser.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
messageCopy.Add("post", postJSON)
a.Publish(messageCopy)
}
return true, nil
}
func (a *App) PatchPost(c *request.Context, postID string, patch *model.PostPatch) (*model.Post, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt != 0 {
err = model.NewAppError("PatchPost", "api.post.patch_post.can_not_update_post_in_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseChannelMentions) {
patch.DisableMentionHighlights()
}
post.Patch(patch)
updatedPost, err := a.UpdatePost(c, post, false)
if err != nil {
return nil, err
}
return updatedPost, nil
}
func (a *App) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPosts(options, false, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsPage", "app.post.get_posts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsPage", "app.post.get_root_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// The postList is sorted as only rootPosts Order is included
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPosts(model.GetPostsOptions{ChannelId: channelID, Page: offset, PerPage: limit}, true, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPosts", "app.post.get_posts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPosts", "app.post.get_root_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsEtag(channelID string, collapsedThreads bool) string {
return a.Srv().Store().Post().GetEtag(channelID, true, collapsedThreads)
}
func (a *App) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsSince(options, true, a.Config().GetSanitizeOptions())
if err != nil {
return nil, model.NewAppError("GetPostsSince", "app.post.get_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetSingle(postID, includeDeleted)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSinglePost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSinglePost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
firstInaccessiblePostTime, appErr := a.isInaccessiblePost(post)
if appErr != nil {
return nil, appErr
}
if firstInaccessiblePostTime != 0 {
return nil, model.NewAppError("GetSinglePost", "app.post.cloud.get.app_error", nil, "", http.StatusForbidden)
}
return post, nil
}
func (a *App) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) {
posts, err := a.Srv().Store().Post().Get(context.Background(), postID, opts, userID, a.Config().GetSanitizeOptions())
if err != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Get inserts the requested post first in the list, then adds the sorted threadPosts.
// So, the whole postList.Order is not sorted.
// The fully sorted list comes only when the CollapsedThreads is true and the Directions is not empty.
filterOptions := filterPostOptions{}
if opts.CollapsedThreads && opts.Direction != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(posts, filterOptions); appErr != nil {
return nil, appErr
}
return posts, nil
}
func (a *App) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPosts(userID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPosts", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPostsForTeam", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPostsForChannel", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPermalinkPost(c request.CTX, postID string, userID string) (*model.PostList, *model.AppError) {
list, nErr := a.Srv().Store().Post().Get(context.Background(), postID, model.GetPostsOptions{}, userID, a.Config().GetSanitizeOptions())
if nErr != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if len(list.Order) != 1 {
return nil, model.NewAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "", http.StatusNotFound)
}
post := list.Posts[list.Order[0]]
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if err = a.JoinChannel(c, channel, userID); err != nil {
return nil, err
}
if appErr := a.filterInaccessiblePosts(list, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return list, nil
}
func (a *App) GetPostsBeforePost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsBefore(options, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsBeforePost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsBeforePost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsBefore orders by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsAfter(options, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsAfterPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsAfterPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsAfter orders by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsAroundPost(before bool, options model.GetPostsOptions) (*model.PostList, *model.AppError) {
var postList *model.PostList
var err error
sanitize := a.Config().GetSanitizeOptions()
if before {
postList, err = a.Srv().Store().Post().GetPostsBefore(options, sanitize)
} else {
postList, err = a.Srv().Store().Post().GetPostsAfter(options, sanitize)
}
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsAroundPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsAroundPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsBefore and GetPostsAfter order by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostAfterTime(channelID string, time int64, collapsedThreads bool) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetPostAfterTime(channelID, time, collapsedThreads)
if err != nil {
return nil, model.NewAppError("GetPostAfterTime", "app.post.get_post_after_time.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return post, nil
}
func (a *App) GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
postID, err := a.Srv().Store().Post().GetPostIdAfterTime(channelID, time, collapsedThreads)
if err != nil {
return "", model.NewAppError("GetPostIdAfterTime", "app.post.get_post_id_around.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return postID, nil
}
func (a *App) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
postID, err := a.Srv().Store().Post().GetPostIdBeforeTime(channelID, time, collapsedThreads)
if err != nil {
return "", model.NewAppError("GetPostIdBeforeTime", "app.post.get_post_id_around.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return postID, nil
}
func (a *App) GetNextPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
if len(postList.Order) > 0 {
firstPostId := postList.Order[0]
firstPost := postList.Posts[firstPostId]
nextPostId, err := a.GetPostIdAfterTime(firstPost.ChannelId, firstPost.CreateAt, collapsedThreads)
if err != nil {
mlog.Warn("GetNextPostIdFromPostList: failed in getting next post", mlog.Err(err))
}
return nextPostId
}
return ""
}
func (a *App) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
if len(postList.Order) > 0 {
lastPostId := postList.Order[len(postList.Order)-1]
lastPost := postList.Posts[lastPostId]
previousPostId, err := a.GetPostIdBeforeTime(lastPost.ChannelId, lastPost.CreateAt, collapsedThreads)
if err != nil {
mlog.Warn("GetPrevPostIdFromPostList: failed in getting previous post", mlog.Err(err))
}
return previousPostId
}
return ""
}
// AddCursorIdsForPostList adds NextPostId and PrevPostId as cursor to the PostList.
// The conditional blocks ensure that it sets those cursor IDs immediately as afterPost, beforePost or empty,
// and only query to database whenever necessary.
func (a *App) AddCursorIdsForPostList(originalList *model.PostList, afterPost, beforePost string, since int64, page, perPage int, collapsedThreads bool) {
prevPostIdSet := false
prevPostId := ""
nextPostIdSet := false
nextPostId := ""
if since > 0 { // "since" query to return empty NextPostId and PrevPostId
nextPostIdSet = true
prevPostIdSet = true
} else if afterPost != "" {
if page == 0 {
prevPostId = afterPost
prevPostIdSet = true
}
if len(originalList.Order) < perPage {
nextPostIdSet = true
}
} else if beforePost != "" {
if page == 0 {
nextPostId = beforePost
nextPostIdSet = true
}
if len(originalList.Order) < perPage {
prevPostIdSet = true
}
}
if !nextPostIdSet {
nextPostId = a.GetNextPostIdFromPostList(originalList, collapsedThreads)
}
if !prevPostIdSet {
prevPostId = a.GetPrevPostIdFromPostList(originalList, collapsedThreads)
}
originalList.NextPostId = nextPostId
originalList.PrevPostId = prevPostId
}
func (a *App) GetPostsForChannelAroundLastUnread(c request.CTX, channelID, userID string, limitBefore, limitAfter int, skipFetchThreads bool, collapsedThreads, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
if member, err = a.GetChannelMember(c, channelID, userID); err != nil {
return nil, err
} else if member.LastViewedAt == 0 {
return model.NewPostList(), nil
}
lastUnreadPostId, err := a.GetPostIdAfterTime(channelID, member.LastViewedAt, collapsedThreads)
if err != nil {
return nil, err
} else if lastUnreadPostId == "" {
return model.NewPostList(), nil
}
opts := model.GetPostsOptions{
SkipFetchThreads: skipFetchThreads,
CollapsedThreads: collapsedThreads,
CollapsedThreadsExtended: collapsedThreadsExtended,
}
postList, err := a.GetPostThread(lastUnreadPostId, opts, userID)
if err != nil {
return nil, err
}
// Reset order to only include the last unread post: if the thread appears in the centre
// channel organically, those replies will be added below.
postList.Order = []string{}
// Add lastUnreadPostId in order, only if it hasn't been filtered as per the cloud plan's limit
if _, ok := postList.Posts[lastUnreadPostId]; ok {
postList.Order = []string{lastUnreadPostId}
// BeforePosts will only be accessible if the lastUnreadPostId is itself accessible
if postListBefore, err := a.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelID, PostId: lastUnreadPostId, Page: PageDefault, PerPage: limitBefore, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: userID}); err != nil {
return nil, err
} else if postListBefore != nil {
postList.Extend(postListBefore)
}
}
if postListAfter, err := a.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelID, PostId: lastUnreadPostId, Page: PageDefault, PerPage: limitAfter - 1, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: userID}); err != nil {
return nil, err
} else if postListAfter != nil {
postList.Extend(postListAfter)
}
postList.SortByCreateAt()
return postList, nil
}
func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetSingle(postID, false)
if err != nil {
return nil, model.NewAppError("DeletePost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel, appErr := a.GetChannel(c, post.ChannelId)
if appErr != nil {
return nil, appErr
}
if channel.DeleteAt != 0 {
appErr := model.NewAppError("DeletePost", "api.post.delete_post.can_not_delete_post_in_deleted.error", nil, "", http.StatusBadRequest)
return nil, appErr
}
err = a.Srv().Store().Post().Delete(postID, model.GetMillis(), deleteByID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeletePost", "app.post.delete.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeletePost", "app.post.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
postJSON, err := json.Marshal(post)
if err != nil {
return nil, model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userMessage := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", post.ChannelId, "", nil, "")
userMessage.Add("post", string(postJSON))
userMessage.GetBroadcast().ContainsSanitizedData = true
a.Publish(userMessage)
adminMessage := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", post.ChannelId, "", nil, "")
adminMessage.Add("post", string(postJSON))
adminMessage.Add("delete_by", deleteByID)
adminMessage.GetBroadcast().ContainsSensitiveData = true
a.Publish(adminMessage)
if len(post.FileIds) > 0 {
a.Srv().Go(func() {
a.deletePostFiles(post.Id)
})
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, true)
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, false)
}
a.Srv().Go(func() {
a.deleteFlaggedPosts(post.Id)
})
a.invalidateCacheForChannelPosts(post.ChannelId)
return post, nil
}
func (a *App) deleteFlaggedPosts(postID string) {
if err := a.Srv().Store().Preference().DeleteCategoryAndName(model.PreferenceCategoryFlaggedPost, postID); err != nil {
a.Log().Warn("Unable to delete flagged post preference when deleting post.", mlog.Err(err))
return
}
}
func (a *App) deletePostFiles(postID string) {
if _, err := a.Srv().Store().FileInfo().DeleteForPost(postID); err != nil {
a.Log().Warn("Encountered error when deleting files for post", mlog.String("post_id", postID), mlog.Err(err))
}
}
func (a *App) parseAndFetchChannelIdByNameFromInFilter(c *request.Context, channelName, userID, teamID string, includeDeleted bool) (*model.Channel, error) {
if strings.HasPrefix(channelName, "@") && strings.Contains(channelName, ",") {
var userIDs []string
users, err := a.GetUsersByUsernames(strings.Split(channelName[1:], ","), false, nil)
if err != nil {
return nil, err
}
for _, user := range users {
userIDs = append(userIDs, user.Id)
}
channel, err := a.GetGroupChannel(c, userIDs)
if err != nil {
return nil, err
}
return channel, nil
}
if strings.HasPrefix(channelName, "@") && !strings.Contains(channelName, ",") {
user, err := a.GetUserByUsername(channelName[1:])
if err != nil {
return nil, err
}
channel, err := a.GetOrCreateDirectChannel(c, userID, user.Id)
if err != nil {
return nil, err
}
return channel, nil
}
channel, err := a.GetChannelByName(c, channelName, teamID, includeDeleted)
if err != nil {
return nil, err
}
return channel, nil
}
func (a *App) searchPostsInTeam(teamID string, userID string, paramsList []*model.SearchParams, modifierFun func(*model.SearchParams)) (*model.PostList, *model.AppError) {
var wg sync.WaitGroup
pchan := make(chan store.StoreResult, len(paramsList))
for _, params := range paramsList {
// Don't allow users to search for everything.
if params.Terms == "*" {
continue
}
modifierFun(params)
wg.Add(1)
go func(params *model.SearchParams) {
defer wg.Done()
postList, err := a.Srv().Store().Post().Search(teamID, userID, params)
pchan <- store.StoreResult{Data: postList, NErr: err}
}(params)
}
wg.Wait()
close(pchan)
posts := model.NewPostList()
for result := range pchan {
if result.NErr != nil {
return nil, model.NewAppError("searchPostsInTeam", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
data := result.Data.(*model.PostList)
posts.Extend(data)
}
posts.SortByCreateAt()
a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true})
return posts, nil
}
func (a *App) convertChannelNamesToChannelIds(c *request.Context, channels []string, userID string, teamID string, includeDeletedChannels bool) []string {
for idx, channelName := range channels {
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(c, channelName, userID, teamID, includeDeletedChannels)
if err != nil {
a.Log().Warn("error getting channel id by name from in filter", mlog.Err(err))
continue
}
channels[idx] = channel.Id
}
return channels
}
func (a *App) convertUserNameToUserIds(usernames []string) []string {
for idx, username := range usernames {
user, err := a.GetUserByUsername(username)
if err != nil {
a.Log().Warn("error getting user by username", mlog.String("user_name", username), mlog.Err(err))
continue
}
usernames[idx] = user.Id
}
return usernames
}
// GetLastAccessiblePostTime returns CreateAt time(from cache) of the last accessible post as per the cloud limit
func (a *App) GetLastAccessiblePostTime() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
system, err := a.Srv().Store().System().GetByName(model.SystemLastAccessiblePostTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All posts are accessible
return 0, nil
default:
return 0, model.NewAppError("GetLastAccessiblePostTime", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
lastAccessiblePostTime, err := strconv.ParseInt(system.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("GetLastAccessiblePostTime", "common.parse_error_int64", map[string]interface{}{"Value": system.Value}, "", http.StatusInternalServerError).Wrap(err)
}
return lastAccessiblePostTime, nil
}
// ComputeLastAccessiblePostTime updates cache with CreateAt time of the last accessible post as per the cloud plan's limit.
// Use GetLastAccessiblePostTime() to access the result.
func (a *App) ComputeLastAccessiblePostTime() error {
limit, appErr := a.getCloudMessagesHistoryLimit()
if appErr != nil {
return appErr
}
if limit == 0 {
// All posts are accessible - we must check if a previous value was set so we can clear it
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessiblePostTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// There was no previous value, nothing to do
return nil
default:
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if systemValue != nil {
// Previous value was set, so we must clear it
if _, err = a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessiblePostTime); err != nil {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Cloud limit is not applicable
return nil
}
createdAt, err := a.Srv().GetStore().Post().GetNthRecentPostTime(limit)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Update Cache
err = a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemLastAccessiblePostTime,
Value: strconv.FormatInt(createdAt, 10),
})
if err != nil {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) getCloudMessagesHistoryLimit() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
limits, err := a.Cloud().GetCloudLimits("")
if err != nil {
return 0, model.NewAppError("getCloudMessagesHistoryLimit", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if limits == nil || limits.Messages == nil || limits.Messages.History == nil {
// Cloud limit is not applicable
return 0, nil
}
return int64(*limits.Messages.History), nil
}
func (a *App) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) {
if !*a.Config().ServiceSettings.EnablePostSearch {
return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v", teamID), http.StatusNotImplemented)
}
return a.searchPostsInTeam(teamID, "", paramsList, func(params *model.SearchParams) {
params.SearchWithoutUserId = true
})
}
func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) {
var postSearchResults *model.PostSearchResults
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
if !*a.Config().ServiceSettings.EnablePostSearch {
return nil, model.NewAppError("SearchPostsForUser", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamID, userID), http.StatusNotImplemented)
}
finalParamsList := []*model.SearchParams{}
for _, params := range paramsList {
params.Modifier = modifier
params.OrTerms = isOrSearch
params.IncludeDeletedChannels = includeDeleted
// Don't allow users to search for "*"
if params.Terms != "*" {
// TODO: we have to send channel ids
// from the front-end. Otherwise it's not possible to distinguish
// from just the channel name at a cross-team level.
// Convert channel names to channel IDs
params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userID, teamID, includeDeletedChannels)
params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userID, teamID, includeDeletedChannels)
// Convert usernames to user IDs
params.FromUsers = a.convertUserNameToUserIds(params.FromUsers)
params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers)
finalParamsList = append(finalParamsList, params)
}
}
// If the processed search params are empty, return empty search results.
if len(finalParamsList) == 0 {
return model.MakePostSearchResults(model.NewPostList(), nil), nil
}
postSearchResults, err := a.Srv().Store().Post().SearchPostsForUser(finalParamsList, userID, teamID, page, perPage)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SearchPostsForUser", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.filterInaccessiblePosts(postSearchResults.PostList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postSearchResults, nil
}
func (a *App) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) {
searchParams, err := a.Srv().Store().Post().GetRecentSearchesForUser(userID)
if err != nil {
return nil, model.NewAppError("GetRecentSearchesForUser", "app.recent_searches.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return searchParams, nil
}
func (a *App) GetFileInfosForPostWithMigration(postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
pchan := make(chan store.StoreResult, 1)
go func() {
post, err := a.Srv().Store().Post().GetSingle(postID, includeDeleted)
pchan <- store.StoreResult{Data: post, NErr: err}
close(pchan)
}()
infos, firstInaccessibleFileTime, err := a.GetFileInfosForPost(postID, false, includeDeleted)
if err != nil {
return nil, err
}
if len(infos) == 0 && firstInaccessibleFileTime == 0 {
// No FileInfos were returned so check if they need to be created for this post
result := <-pchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, model.NewAppError("GetFileInfosForPostWithMigration", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, model.NewAppError("GetFileInfosForPostWithMigration", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
post := result.Data.(*model.Post)
if len(post.Filenames) > 0 {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, false)
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, true)
// The post has Filenames that need to be replaced with FileInfos
infos = a.MigrateFilenamesToFileInfos(post)
}
}
return infos, nil
}
// GetFileInfosForPost also returns firstInaccessibleFileTime based on cloud plan's limit.
func (a *App) GetFileInfosForPost(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, int64, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(postID, fromMaster, includeDeleted, true)
if err != nil {
return nil, 0, model.NewAppError("GetFileInfosForPost", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
firstInaccessibleFileTime, appErr := a.removeInaccessibleContentFromFilesSlice(fileInfos)
if appErr != nil {
return nil, 0, appErr
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, firstInaccessibleFileTime, nil
}
func (a *App) getFileInfosForPostIgnoreCloudLimit(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(postID, fromMaster, includeDeleted, true)
if err != nil {
return nil, model.NewAppError("getFileInfosForPostIgnoreCloudLimit", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
if f := a.ImageProxyAdder(); f != nil {
return post.WithRewrittenImageURLs(f)
}
return post
}
func (a *App) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post {
if f := a.ImageProxyRemover(); f != nil {
return post.WithRewrittenImageURLs(f)
}
return post
}
func (a *App) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch {
if f := a.ImageProxyRemover(); f != nil {
return patch.WithRewrittenImageURLs(f)
}
return patch
}
func (a *App) ImageProxyAdder() func(string) string {
if !*a.Config().ImageProxySettings.Enable {
return nil
}
return func(url string) string {
return a.ImageProxy().GetProxiedImageURL(url)
}
}
func (a *App) ImageProxyRemover() (f func(string) string) {
if !*a.Config().ImageProxySettings.Enable {
return nil
}
return func(url string) string {
return a.ImageProxy().GetUnproxiedImageURL(url)
}
}
func (a *App) MaxPostSize() int {
return a.Srv().Platform().MaxPostSize()
}
// countThreadMentions returns the number of times the user is mentioned in a specified thread after the timestamp.
func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.Post, teamID string, timestamp int64) (int64, *model.AppError) {
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return 0, err
}
keywords := addMentionKeywordsForUser(
map[string][]string{},
user,
map[string]string{},
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
true, // Assume channel mentions are always allowed for simplicity
)
posts, nErr := a.Srv().Store().Post().GetPostsByThread(post.Id, timestamp)
if nErr != nil {
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
count := 0
if channel.Type == model.ChannelTypeDirect {
// In a DM channel, every post made by the other user is a mention
otherId := channel.GetOtherUserIdForDM(user.Id)
for _, p := range posts {
if p.UserId == otherId {
count++
}
}
return int64(count), nil
}
var team *model.Team
if teamID != "" {
team, err = a.GetTeam(teamID)
if err != nil {
return 0, err
}
}
groups, nErr := a.getGroupsAllowedForReferenceInChannel(channel, team)
if nErr != nil {
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, p := range posts {
if p.CreateAt >= timestamp {
mentions := getExplicitMentions(p, keywords, groups)
if _, ok := mentions.Mentions[user.Id]; ok {
count += 1
}
}
}
return int64(count), nil
}
// countMentionsFromPost returns the number of posts in the post's channel that mention the user after and including the
// given post.
func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, int, *model.AppError) {
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return 0, 0, 0, err
}
if channel.Type == model.ChannelTypeDirect {
// In a DM channel, every post made by the other user is a mention
count, countRoot, nErr := a.Srv().Store().Channel().CountPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var urgentCount int
if a.isPostPriorityEnabled() {
urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return count, countRoot, urgentCount, nil
}
channelMember, err := a.GetChannelMember(c, channel.Id, user.Id)
if err != nil {
return 0, 0, 0, err
}
keywords := addMentionKeywordsForUser(
map[string][]string{},
user,
channelMember.NotifyProps,
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
true, // Assume channel mentions are always allowed for simplicity
)
commentMentions := user.NotifyProps[model.CommentsNotifyProp]
checkForCommentMentions := commentMentions == model.CommentsNotifyRoot || commentMentions == model.CommentsNotifyAny
// A mapping of thread root IDs to whether or not a post in that thread mentions the user
mentionedByThread := make(map[string]bool)
thread, err := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id)
if err != nil {
return 0, 0, 0, err
}
count := 0
countRoot := 0
urgentCount := 0
if isPostMention(user, post, keywords, thread.Posts, mentionedByThread, checkForCommentMentions) {
count += 1
if post.RootId == "" {
countRoot += 1
if a.isPostPriorityEnabled() {
priority, err := a.GetPriorityForPost(post.Id)
if err != nil {
return 0, 0, 0, err
}
if priority != nil && *priority.Priority == model.PostPriorityUrgent {
urgentCount += 1
}
}
}
}
page := 0
perPage := 200
for {
postList, err := a.GetPostsAfterPost(model.GetPostsOptions{
ChannelId: post.ChannelId,
PostId: post.Id,
Page: page,
PerPage: perPage,
})
if err != nil {
return 0, 0, 0, err
}
mentionPostIds := make([]string, 0)
for _, postID := range postList.Order {
if isPostMention(user, postList.Posts[postID], keywords, postList.Posts, mentionedByThread, checkForCommentMentions) {
count += 1
if postList.Posts[postID].RootId == "" {
mentionPostIds = append(mentionPostIds, postID)
countRoot += 1
}
}
}
if a.isPostPriorityEnabled() {
priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds)
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, priority := range priorityList {
if *priority.Priority == model.PostPriorityUrgent {
urgentCount += 1
}
}
}
if len(postList.Order) < perPage {
break
}
page += 1
}
return count, countRoot, urgentCount, nil
}
func isCommentMention(user *model.User, post *model.Post, otherPosts map[string]*model.Post, mentionedByThread map[string]bool) bool {
if post.RootId == "" {
// Not a comment
return false
}
if mentioned, ok := mentionedByThread[post.RootId]; ok {
// We've already figured out if the user was mentioned by this thread
return mentioned
}
// Whether or not the user was mentioned because they started the thread
mentioned := otherPosts[post.RootId].UserId == user.Id
// Or because they commented on it before this post
if !mentioned && user.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyAny {
for _, otherPost := range otherPosts {
if otherPost.Id == post.Id {
continue
}
if otherPost.RootId != post.RootId {
continue
}
if otherPost.UserId == user.Id && otherPost.CreateAt < post.CreateAt {
// Found a comment made by the user from before this post
mentioned = true
break
}
}
}
mentionedByThread[post.RootId] = mentioned
return mentioned
}
func isPostMention(user *model.User, post *model.Post, keywords map[string][]string, otherPosts map[string]*model.Post, mentionedByThread map[string]bool, checkForCommentMentions bool) bool {
// Prevent the user from mentioning themselves
if post.UserId == user.Id && post.GetProp("from_webhook") != "true" {
return false
}
// Check for keyword mentions
mentions := getExplicitMentions(post, keywords, make(map[string]*model.Group))
if _, ok := mentions.Mentions[user.Id]; ok {
return true
}
// Check for mentions caused by being added to the channel
if post.Type == model.PostTypeAddToChannel {
if addedUserId, ok := post.GetProp(model.PostPropsAddedUserId).(string); ok && addedUserId == user.Id {
return true
}
}
// Check for comment mentions
if checkForCommentMentions && isCommentMention(user, post, otherPosts, mentionedByThread) {
return true
}
return false
}
func (a *App) GetThreadMembershipsForUser(userID, teamID string) ([]*model.ThreadMembership, error) {
return a.Srv().Store().Thread().GetMembershipsForUser(userID, teamID)
}
func (a *App) GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) {
post, err := a.GetSinglePost(postID, includeDeleted)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if !a.SessionHasPermissionToChannel(c, *session, channel.Id, model.PermissionReadChannel) {
if channel.Type == model.ChannelTypeOpen {
if !a.SessionHasPermissionToTeam(*session, channel.TeamId, model.PermissionReadPublicChannel) {
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadPublicChannel})
}
} else {
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadChannel})
}
}
return post, nil
}
// GetPostsByIds response bool value indicates, if the post is inaccessible due to cloud plan's limit.
func (a *App) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
posts, err := a.Srv().Store().Post().GetPostsByIds(postIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, 0, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, 0, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
posts, firstInaccessiblePostTime, appErr := a.getFilteredAccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true})
if appErr != nil {
return nil, 0, appErr
}
return posts, firstInaccessiblePostTime, nil
}
func (a *App) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) {
posts, err := a.Srv().Store().Post().GetEditHistoryForPost(postID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return posts, nil
}
func (a *App) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", "app.post.get_top_threads_for_user_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topDMs, err := a.Srv().Store().Post().GetTopDMsForUserSince(userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.post.get_top_dms_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topDMs, nil
}
func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.AppError {
// Store the reminder in the DB
reminder := &model.PostReminder{
PostId: postID,
UserId: userID,
TargetTime: targetTime,
}
err := a.Srv().Store().Post().SetPostReminder(reminder)
if err != nil {
return model.NewAppError("SetPostReminder", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
metadata, err := a.Srv().Store().Post().GetPostReminderMetadata(postID)
if err != nil {
return model.NewAppError("SetPostReminder", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
parsedTime := time.Unix(targetTime, 0).UTC().Format(time.RFC822)
siteURL := *a.Config().ServiceSettings.SiteURL
var permalink string
if metadata.TeamName == "" {
permalink = fmt.Sprintf("%s/pl/%s", siteURL, postID)
} else {
permalink = fmt.Sprintf("%s/%s/pl/%s", siteURL, metadata.TeamName, postID)
}
// Send an ack message.
ephemeralPost := &model.Post{
Type: model.PostTypeEphemeral,
Id: model.NewId(),
CreateAt: model.GetMillis(),
UserId: userID,
RootId: postID,
ChannelId: metadata.ChannelId,
// It's okay to keep this non-translated. This is just a fallback.
// The webapp will parse the timestamp and show that in user's local timezone.
Message: fmt.Sprintf("You will be reminded about %s by @%s at %s", permalink, metadata.Username, parsedTime),
Props: model.StringInterface{
"target_time": targetTime,
"team_name": metadata.TeamName,
"post_id": postID,
"username": metadata.Username,
"type": model.PostTypeReminder,
},
}
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", ephemeralPost.ChannelId, userID, nil, "")
ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false, true)
ephemeralPost = model.AddPostActionCookies(ephemeralPost, a.PostActionCookieSecret())
postJSON, jsonErr := ephemeralPost.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return nil
}
func (a *App) CheckPostReminders() {
systemBot, appErr := a.GetSystemBot()
if appErr != nil {
mlog.Error("Failed to get system bot", mlog.Err(appErr))
return
}
// This will return the reminders and also delete them from the DB.
// In case, any of the next steps fail, those reminders would be lost.
// Alternatively, if we delete those reminders _after_ it has been sent,
// then in case of any temporary failure, they would get sent in the next batch.
// MM-45595.
reminders, err := a.Srv().Store().Post().GetPostReminders(time.Now().UTC().Unix())
if err != nil {
mlog.Error("Failed to get post reminders", mlog.Err(err))
return
}
// We group multiple reminders for a single user.
groupedReminders := make(map[string][]string)
for _, r := range reminders {
if groupedReminders[r.UserId] == nil {
groupedReminders[r.UserId] = []string{r.PostId}
} else {
groupedReminders[r.UserId] = append(groupedReminders[r.UserId], r.PostId)
}
}
siteURL := *a.Config().ServiceSettings.SiteURL
for userID, postIDs := range groupedReminders {
ch, appErr := a.GetOrCreateDirectChannel(request.EmptyContext(a.Log()), userID, systemBot.UserId)
if appErr != nil {
mlog.Error("Failed to get direct channel", mlog.Err(appErr))
return
}
for _, postID := range postIDs {
metadata, err := a.Srv().Store().Post().GetPostReminderMetadata(postID)
if err != nil {
mlog.Error("Failed to get post reminder metadata", mlog.Err(err), mlog.String("post_id", postID))
continue
}
T := i18n.GetUserTranslations(metadata.UserLocale)
dm := &model.Post{
ChannelId: ch.Id,
Message: T("app.post_reminder_dm", model.StringInterface{
"SiteURL": siteURL,
"TeamName": metadata.TeamName,
"PostId": postID,
"Username": metadata.Username,
}),
Type: model.PostTypeReminder,
UserId: systemBot.UserId,
Props: model.StringInterface{
"team_name": metadata.TeamName,
"post_id": postID,
"username": metadata.Username,
},
}
if _, err := a.CreatePost(request.EmptyContext(a.Log()), dm, ch, false, true); err != nil {
mlog.Error("Failed to post reminder message", mlog.Err(err))
}
}
}
}
func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
userID := c.Session().UserId
post, appErr := a.GetSinglePost(postID, false)
if appErr != nil {
return nil, appErr
}
channel, appErr := a.GetChannel(c, post.ChannelId)
if appErr != nil {
return nil, appErr
}
notFoundError := model.NewAppError("GetPostInfo", "app.post.get.app_error", nil, "", http.StatusNotFound)
var team *model.Team
hasPermissionToAccessTeam := false
if channel.TeamId != "" {
team, appErr = a.GetTeam(channel.TeamId)
if appErr != nil {
return nil, appErr
}
if team.Type == model.TeamOpen {
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPublicTeams)
} else if team.Type == model.TeamInvite {
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPrivateTeams)
}
} else {
// This happens in case of DMs and GMs.
hasPermissionToAccessTeam = true
}
if !hasPermissionToAccessTeam {
return nil, notFoundError
}
hasPermissionToAccessChannel := false
if channel.Type == model.ChannelTypeOpen {
hasPermissionToAccessChannel = true
} else if channel.Type == model.ChannelTypePrivate {
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionManagePrivateChannelMembers)
} else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel)
}
if !hasPermissionToAccessChannel {
return nil, notFoundError
}
_, channelMemberErr := a.GetChannelMember(c, channel.Id, userID)
info := model.PostInfo{
ChannelId: channel.Id,
ChannelType: channel.Type,
ChannelDisplayName: channel.DisplayName,
HasJoinedChannel: channelMemberErr == nil,
}
if team != nil {
_, teamMemberErr := a.GetTeamMember(team.Id, userID)
info.TeamId = team.Id
info.TeamType = team.Type
info.TeamDisplayName = team.DisplayName
info.HasJoinedTeam = teamMemberErr == nil
}
return &info, nil
}
func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
for _, topThread := range topThreadList.Items {
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true)
sanitizedPost, err := a.SanitizePostMetadataForUser(c, topThread.Post, userID)
if err != nil {
return nil, err
}
topThread.Post = sanitizedPost
}
return topThreadList, nil
}
func (a *App) isPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("SaveAcknowledgementForPost", "api.acknowledgement.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
acknowledgedAt := model.GetMillis()
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveAcknowledgementForPost", "app.acknowledgement.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementAdded, acknowledgement, post)
})
return acknowledgement, nil
}
func (a *App) DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return err
}
if channel.DeleteAt > 0 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
}
oldAck, nErr := a.Srv().Store().PostAcknowledgement().Get(postID, userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if model.GetMillis()-oldAck.AcknowledgedAt > 5*60*1000 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.deadline.app_error", nil, "", http.StatusForbidden)
}
nErr = a.Srv().Store().PostAcknowledgement().Delete(oldAck)
if nErr != nil {
return model.NewAppError("DeleteAcknowledgementForPost", "app.acknowledgement.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementRemoved, oldAck, post)
})
return nil
}
func (a *App) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, nErr := a.Srv().Store().PostAcknowledgement().GetForPost(postID)
if nErr != nil {
return nil, model.NewAppError("GetAcknowledgementsForPost", "app.acknowledgement.getforpost.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return acknowledgements, nil
}
func (a *App) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, err := a.Srv().Store().PostAcknowledgement().GetForPosts(postList.Order)
if err != nil {
return nil, model.NewAppError("GetPostAcknowledgementsForPostList", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
acknowledgementsMap := make(map[string][]*model.PostAcknowledgement)
for _, ack := range acknowledgements {
acknowledgementsMap[ack.PostId] = append(acknowledgementsMap[ack.PostId], ack)
}
return acknowledgementsMap, nil
}
func (a *App) sendAcknowledgementEvent(event string, acknowledgement *model.PostAcknowledgement, post *model.Post) {
// send out that a acknowledgement has been added/removed
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
acknowledgementJSON, err := json.Marshal(acknowledgement)
if err != nil {
a.Log().Warn("Failed to encode acknowledgement to JSON", mlog.Err(err))
}
message.Add("acknowledgement", string(acknowledgementJSON))
a.Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"sort"
"github.com/mattermost/mattermost-server/v6/model"
)
type filterPostOptions struct {
assumeSortedCreatedAt bool
}
type accessibleBounds struct {
start int
end int
}
func (b accessibleBounds) allAccessible(lenPosts int) bool {
return b.start == allAccessibleBounds(lenPosts).start && b.end == allAccessibleBounds(lenPosts).end
}
func (b accessibleBounds) noAccessible() bool {
return b.start == noAccessibleBounds.start && b.end == noAccessibleBounds.end
}
// assumes checking was already performed that at least one post is inaccessible
func (b accessibleBounds) getInaccessibleRange(listLength int) (int, int) {
var start, end int
if b.start == 0 {
start = b.end + 1
end = listLength - 1
} else {
start = 0
end = b.start - 1
}
return start, end
}
var noAccessibleBounds = accessibleBounds{start: -1, end: -1}
var allAccessibleBounds = func(lenPosts int) accessibleBounds { return accessibleBounds{start: 0, end: lenPosts - 1} }
// getTimeSortedPostAccessibleBounds returns what the boundaries are for accessible posts.
// It assumes that CreateAt time for posts is monotonically increasing or decreasing.
// It could be either because posts can be returned in ascending or descending time order.
// Special values (which can be checked with methods `allAccessible` and `allInaccessible`)
// denote if all or none of the posts are accessible.
func getTimeSortedPostAccessibleBounds(earliestAccessibleTime int64, lenPosts int, getCreateAt func(int) int64) accessibleBounds {
if lenPosts == 0 {
return allAccessibleBounds(lenPosts)
}
if lenPosts == 1 {
if getCreateAt(0) >= earliestAccessibleTime {
return allAccessibleBounds(lenPosts)
}
return noAccessibleBounds
}
ascending := getCreateAt(0) < getCreateAt(lenPosts-1)
idx := sort.Search(lenPosts, func(i int) bool {
if ascending {
// Ascending order automatically picks the left most post(at idx),
// in case multiple posts at idx, idx+1, idx+2... have the same time.
return getCreateAt(i) >= earliestAccessibleTime
}
// Special case(subtracting 1) for descending order to include the right most post(at idx+k),
// in case multiple posts at idx, idx+1, idx+2...idx+k have the same time.
return getCreateAt(i) <= earliestAccessibleTime-1
})
if ascending {
if idx == lenPosts {
return noAccessibleBounds
}
return accessibleBounds{start: idx, end: lenPosts - 1}
}
if idx == 0 {
return noAccessibleBounds
}
return accessibleBounds{start: 0, end: idx - 1}
}
// linearFilterPostList make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostList(postList *model.PostList, earliestAccessibleTime int64) {
// filter Posts
posts := postList.Posts
order := postList.Order
n := 0
for i, postId := range order {
if createAt := posts[postId].CreateAt; createAt >= earliestAccessibleTime {
order[n] = order[i]
n++
} else {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postId)
}
}
postList.Order = order[:n]
// it can happen that some post list results don't have all posts in the Order field.
// for example GetPosts in the CollapsedThreads = false path, parents are not added
// to Order
for postId := range posts {
if createAt := posts[postId].CreateAt; createAt < earliestAccessibleTime {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postId)
}
}
}
// linearFilterPostsSlice make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostsSlice(posts []*model.Post, earliestAccessibleTime int64) ([]*model.Post, int64) {
var firstInaccessiblePostTime int64 = 0
n := 0
for i := range posts {
if createAt := posts[i].CreateAt; createAt >= earliestAccessibleTime {
posts[n] = posts[i]
n++
} else {
if createAt > firstInaccessiblePostTime {
firstInaccessiblePostTime = createAt
}
}
}
return posts[:n], firstInaccessiblePostTime
}
// filterInaccessiblePosts filters out the posts, past the cloud limit
func (a *App) filterInaccessiblePosts(postList *model.PostList, options filterPostOptions) *model.AppError {
if postList == nil || postList.Posts == nil || len(postList.Posts) == 0 {
return nil
}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return model.NewAppError("filterInaccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return nil
}
if len(postList.Posts) == len(postList.Order) && options.assumeSortedCreatedAt {
lenPosts := len(postList.Posts)
getCreateAt := func(i int) int64 { return postList.Posts[postList.Order[i]].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return nil
}
if bounds.noAccessible() {
if lenPosts > 0 {
firstPostCreatedAt := postList.Posts[postList.Order[0]].CreateAt
lastPostCreatedAt := postList.Posts[postList.Order[len(postList.Order)-1]].CreateAt
postList.FirstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
postList.Posts = map[string]*model.Post{}
postList.Order = []string{}
return nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(postList.Order))
startInaccessibleCreatedAt := postList.Posts[postList.Order[startInaccessibleIndex]].CreateAt
endInaccessibleCreatedAt := postList.Posts[postList.Order[endInaccessibleIndex]].CreateAt
postList.FirstInaccessiblePostTime = max(startInaccessibleCreatedAt, endInaccessibleCreatedAt)
posts := postList.Posts
order := postList.Order
accessibleCount := bounds.end - bounds.start + 1
inaccessibleCount := lenPosts - accessibleCount
// Linearly cover shorter route to traverse posts map
if inaccessibleCount < accessibleCount {
for i := 0; i < bounds.start; i++ {
delete(posts, order[i])
}
for i := bounds.end + 1; i < lenPosts; i++ {
delete(posts, order[i])
}
} else {
accessiblePosts := make(map[string]*model.Post, accessibleCount)
for i := bounds.start; i <= bounds.end; i++ {
accessiblePosts[order[i]] = posts[order[i]]
}
postList.Posts = accessiblePosts
}
postList.Order = postList.Order[bounds.start : bounds.end+1]
} else {
linearFilterPostList(postList, lastAccessiblePostTime)
}
return nil
}
// isInaccessiblePost indicates if the post is past the cloud plan's limit.
func (a *App) isInaccessiblePost(post *model.Post) (int64, *model.AppError) {
if post == nil {
return 0, nil
}
pl := &model.PostList{
Order: []string{post.Id},
Posts: map[string]*model.Post{post.Id: post},
}
return pl.FirstInaccessiblePostTime, a.filterInaccessiblePosts(pl, filterPostOptions{assumeSortedCreatedAt: true})
}
// getFilteredAccessiblePosts returns accessible posts filtered as per the cloud plan's limit and also indicates if there were any inaccessible posts
func (a *App) getFilteredAccessiblePosts(posts []*model.Post, options filterPostOptions) ([]*model.Post, int64, *model.AppError) {
if len(posts) == 0 {
return posts, 0, nil
}
filteredPosts := []*model.Post{}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return filteredPosts, 0, model.NewAppError("getFilteredAccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
} else if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return posts, 0, nil
}
if options.assumeSortedCreatedAt {
lenPosts := len(posts)
getCreateAt := func(i int) int64 { return posts[i].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return posts, 0, nil
}
if bounds.noAccessible() {
var firstInaccessiblePostTime int64 = 0
if lenPosts > 0 {
firstPostCreatedAt := posts[0].CreateAt
lastPostCreatedAt := posts[len(posts)-1].CreateAt
firstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
return filteredPosts, firstInaccessiblePostTime, nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(posts))
firstPostCreatedAt := posts[startInaccessibleIndex].CreateAt
lastPostCreatedAt := posts[endInaccessibleIndex].CreateAt
firstInaccessiblePostTime := max(firstPostCreatedAt, lastPostCreatedAt)
filteredPosts = posts[bounds.start : bounds.end+1]
return filteredPosts, firstInaccessiblePostTime, nil
}
filteredPosts, firstInaccessiblePostTime := linearFilterPostsSlice(posts, lastAccessiblePostTime)
return filteredPosts, firstInaccessiblePostTime, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"image"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"golang.org/x/net/idna"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/imgutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type linkMetadataCache struct {
OpenGraph *opengraph.OpenGraph
PostImage *model.PostImage
Permalink *model.Permalink
}
const MaxMetadataImageSize = MaxOpenGraphResponseSize
func (s *Server) initPostMetadata() {
// Dump any cached links if the proxy settings have changed so image URLs can be updated
s.platform.AddConfigListener(func(before, after *model.Config) {
if (before.ImageProxySettings.Enable != after.ImageProxySettings.Enable) ||
(before.ImageProxySettings.ImageProxyType != after.ImageProxySettings.ImageProxyType) ||
(before.ImageProxySettings.RemoteImageProxyURL != after.ImageProxySettings.RemoteImageProxyURL) ||
(before.ImageProxySettings.RemoteImageProxyOptions != after.ImageProxySettings.RemoteImageProxyOptions) {
platform.PurgeLinkCache()
}
})
}
func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList {
list := &model.PostList{
Posts: make(map[string]*model.Post, len(originalList.Posts)),
Order: originalList.Order,
NextPostId: originalList.NextPostId,
PrevPostId: originalList.PrevPostId,
HasNext: originalList.HasNext,
FirstInaccessiblePostTime: originalList.FirstInaccessiblePostTime,
}
for id, originalPost := range originalList.Posts {
post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false, false)
list.Posts[id] = post
}
if a.isPostPriorityEnabled() {
priority, _ := a.GetPriorityForPostList(list)
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
for _, id := range list.Order {
if _, ok := priority[id]; ok {
list.Posts[id].Metadata.Priority = priority[id]
}
if _, ok := acknowledgements[id]; ok {
list.Posts[id].Metadata.Acknowledgements = acknowledgements[id]
}
}
}
return list
}
// OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon,
// so that it points to the URL (relative) of the emoji - static if emoji is default, /api if custom.
func (a *App) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) {
prop, ok := post.GetProps()[model.PostPropsOverrideIconEmoji]
if !ok || prop == nil {
return
}
emojiName, ok := prop.(string)
if !ok {
return
}
if !*a.Config().ServiceSettings.EnablePostIconOverride || emojiName == "" {
return
}
emojiName = strings.ReplaceAll(emojiName, ":", "")
if emojiURL, err := a.GetEmojiStaticURL(c, emojiName); err == nil {
post.AddProp(model.PostPropsOverrideIconURL, emojiURL)
} else {
mlog.Warn("Failed to retrieve URL for overridden profile icon (emoji)", mlog.String("emojiName", emojiName), mlog.Err(err))
}
}
func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
post := originalPost.Clone()
// Proxy image links before constructing metadata so that requests go through the proxy
post = a.PostWithProxyAddedToImageURLs(post)
a.OverrideIconURLIfEmoji(c, post)
if post.Metadata == nil {
post.Metadata = &model.PostMetadata{}
}
if post.DeleteAt > 0 {
// For deleted posts we don't fill out metadata nor do we return the post content
post.Message = ""
post.Metadata = &model.PostMetadata{}
return post
}
// Emojis and reaction counts
if emojis, reactions, err := a.getEmojisAndReactionsForPost(c, post); err != nil {
mlog.Warn("Failed to get emojis and reactions for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Emojis = emojis
post.Metadata.Reactions = reactions
}
// Files
if fileInfos, _, err := a.getFileMetadataForPost(post, isNewPost || isEditPost); err != nil {
mlog.Warn("Failed to get files for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Files = fileInfos
}
if includePriority && a.isPostPriorityEnabled() && post.RootId == "" {
// Post's Priority if any
if priority, err := a.GetPriorityForPost(post.Id); err != nil {
mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Priority = priority
}
// Post's acknowledgements if any
if acknowledgements, err := a.GetAcknowledgementsForPost(post.Id); err != nil {
mlog.Warn("Failed to get post acknowledgements for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Acknowledgements = acknowledgements
}
}
return post
}
func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
post = a.getEmbedsAndImages(c, post, isNewPost)
return post
}
func (a *App) getEmbedsAndImages(c request.CTX, post *model.Post, isNewPost bool) *model.Post {
if post.Metadata == nil {
post.Metadata = &model.PostMetadata{}
}
// Embeds and image dimensions
firstLink, images := a.getFirstLinkAndImages(post.Message)
if post.Metadata.Embeds == nil {
post.Metadata.Embeds = []*model.PostEmbed{}
}
if embed, err := a.getEmbedForPost(c, post, firstLink, isNewPost); err != nil {
appErr, ok := err.(*model.AppError)
isNotFound := ok && appErr.StatusCode == http.StatusNotFound
// Ignore NotFound errors.
if !isNotFound {
mlog.Debug("Failed to get embedded content for a post", mlog.String("post_id", post.Id), mlog.Err(err))
}
} else if embed != nil {
post.Metadata.Embeds = append(post.Metadata.Embeds, embed)
}
post.Metadata.Images = a.getImagesForPost(c, post, images, isNewPost)
return post
}
func (a *App) sanitizePostMetadataForUserAndChannel(c request.CTX, post *model.Post, previewedPost *model.PreviewPost, previewedChannel *model.Channel, userID string) *model.Post {
if post.Metadata == nil || len(post.Metadata.Embeds) == 0 || previewedPost == nil {
return post
}
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
post.Metadata.Embeds[0].Data = nil
}
return post
}
func (a *App) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userID string) (*model.Post, *model.AppError) {
if post.Metadata == nil || len(post.Metadata.Embeds) == 0 {
return post, nil
}
previewPost := post.GetPreviewPost()
if previewPost == nil {
return post, nil
}
previewedChannel, err := a.GetChannel(c, previewPost.Post.ChannelId)
if err != nil {
return nil, err
}
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
for _, embed := range post.Metadata.Embeds {
embed.Data = nil
}
}
return post, nil
}
func (a *App) SanitizePostListMetadataForUser(c request.CTX, postList *model.PostList, userID string) (*model.PostList, *model.AppError) {
clonedPostList := postList.Clone()
for postID, post := range clonedPostList.Posts {
sanitizedPost, err := a.SanitizePostMetadataForUser(c, post, userID)
if err != nil {
return nil, err
}
clonedPostList.Posts[postID] = sanitizedPost
}
return clonedPostList, nil
}
func (a *App) getFileMetadataForPost(post *model.Post, fromMaster bool) ([]*model.FileInfo, int64, *model.AppError) {
if len(post.FileIds) == 0 {
return nil, 0, nil
}
return a.GetFileInfosForPost(post.Id, fromMaster, false)
}
func (a *App) getEmojisAndReactionsForPost(c request.CTX, post *model.Post) ([]*model.Emoji, []*model.Reaction, *model.AppError) {
var reactions []*model.Reaction
if post.HasReactions {
var err *model.AppError
reactions, err = a.GetReactionsForPost(post.Id)
if err != nil {
return nil, nil, err
}
}
emojis, err := a.getCustomEmojisForPost(c, post, reactions)
if err != nil {
return nil, nil, err
}
return emojis, reactions, nil
}
func (a *App) getEmbedForPost(c request.CTX, post *model.Post, firstLink string, isNewPost bool) (*model.PostEmbed, error) {
if _, ok := post.GetProps()["attachments"]; ok {
return &model.PostEmbed{
Type: model.PostEmbedMessageAttachment,
}, nil
}
if _, ok := post.GetProps()["boards"]; ok {
return &model.PostEmbed{
Type: model.PostEmbedBoards,
Data: post.GetProps()["boards"],
}, nil
}
if firstLink == "" {
return nil, nil
}
// Permalink previews are not toggled via the ServiceSettings.EnableLinkPreviews config setting.
if !*a.Config().ServiceSettings.EnableLinkPreviews && !looksLikeAPermalink(firstLink, *a.Config().ServiceSettings.SiteURL) {
return nil, nil
}
og, image, permalink, err := a.getLinkMetadata(c, firstLink, post.CreateAt, isNewPost, post.GetPreviewedPostProp())
if err != nil {
return nil, err
}
if !*a.Config().ServiceSettings.EnablePermalinkPreviews || !a.Config().FeatureFlags.PermalinkPreviews {
permalink = nil
}
if og != nil {
return &model.PostEmbed{
Type: model.PostEmbedOpengraph,
URL: firstLink,
Data: og,
}, nil
}
if image != nil {
// Note that we're not passing the image info here since it'll be part of the PostMetadata.Images field
return &model.PostEmbed{
Type: model.PostEmbedImage,
URL: firstLink,
}, nil
}
if permalink != nil {
return &model.PostEmbed{Type: model.PostEmbedPermalink, Data: permalink.PreviewPost}, nil
}
return &model.PostEmbed{
Type: model.PostEmbedLink,
URL: firstLink,
}, nil
}
func (a *App) getImagesForPost(c request.CTX, post *model.Post, imageURLs []string, isNewPost bool) map[string]*model.PostImage {
images := map[string]*model.PostImage{}
for _, embed := range post.Metadata.Embeds {
switch embed.Type {
case model.PostEmbedImage:
// These dimensions will generally be cached by a previous call to getEmbedForPost
imageURLs = append(imageURLs, embed.URL)
case model.PostEmbedMessageAttachment:
imageURLs = append(imageURLs, a.getImagesInMessageAttachments(post)...)
case model.PostEmbedOpengraph:
openGraph, ok := embed.Data.(*opengraph.OpenGraph)
if !ok {
mlog.Warn("Could not read the image data: the data could not be casted to OpenGraph",
mlog.String("post_id", post.Id), mlog.String("data type", fmt.Sprintf("%t", embed.Data)))
continue
}
for _, image := range openGraph.Images {
var imageURL string
if image.SecureURL != "" {
imageURL = image.SecureURL
} else if image.URL != "" {
imageURL = image.URL
}
if imageURL == "" {
continue
}
imageURLs = append(imageURLs, imageURL)
}
}
}
// Removing duplicates isn't strictly since images is a map, but it feels safer to do it beforehand
if len(imageURLs) > 1 {
imageURLs = model.RemoveDuplicateStrings(imageURLs)
}
for _, imageURL := range imageURLs {
if _, image, _, err := a.getLinkMetadata(c, imageURL, post.CreateAt, isNewPost, post.GetPreviewedPostProp()); err != nil {
appErr, ok := err.(*model.AppError)
isNotFound := ok && appErr.StatusCode == http.StatusNotFound
// Ignore NotFound errors.
if !isNotFound {
mlog.Debug("Failed to get dimensions of an image in a post",
mlog.String("post_id", post.Id), mlog.String("image_url", imageURL), mlog.Err(err))
}
} else if image != nil {
images[imageURL] = image
}
}
return images
}
func getEmojiNamesForString(s string) []string {
names := model.EmojiPattern.FindAllString(s, -1)
for i, name := range names {
names[i] = strings.Trim(name, ":")
}
return names
}
func getEmojiNamesForPost(post *model.Post, reactions []*model.Reaction) []string {
// Post message
names := getEmojiNamesForString(post.Message)
// Reactions
for _, reaction := range reactions {
names = append(names, reaction.EmojiName)
}
// Post attachments
for _, attachment := range post.Attachments() {
if attachment.Title != "" {
names = append(names, getEmojiNamesForString(attachment.Title)...)
}
if attachment.Text != "" {
names = append(names, getEmojiNamesForString(attachment.Text)...)
}
if attachment.Pretext != "" {
names = append(names, getEmojiNamesForString(attachment.Pretext)...)
}
for _, field := range attachment.Fields {
if field == nil {
continue
}
if value, ok := field.Value.(string); ok {
names = append(names, getEmojiNamesForString(value)...)
}
}
}
// Remove duplicates
names = model.RemoveDuplicateStrings(names)
return names
}
func (a *App) getCustomEmojisForPost(c request.CTX, post *model.Post, reactions []*model.Reaction) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
// Only custom emoji are returned
return []*model.Emoji{}, nil
}
names := getEmojiNamesForPost(post, reactions)
if len(names) == 0 {
return []*model.Emoji{}, nil
}
return a.GetMultipleEmojiByName(c, names)
}
func (a *App) isLinkAllowedForPreview(link string) bool {
domains := normalizeDomains(*a.Config().ServiceSettings.RestrictLinkPreviews)
for _, d := range domains {
parsed, err := url.Parse(link)
if err != nil {
a.Log().Warn("Unable to parse the link", mlog.String("link", link), mlog.Err(err))
// We disable link preview if link is badly formed
// to remain on the safe side
return false
}
// Conforming to IDNA2008 using the UTS-46 standard.
cleaned, err := idna.Lookup.ToASCII(parsed.Hostname())
if err != nil {
a.Log().Warn("Unable to lookup hostname to ASCII", mlog.String("hostname", parsed.Hostname()), mlog.Err(err))
// Same applies if compatibility processing fails.
return false
}
if strings.Contains(cleaned, d) {
return false
}
}
return true
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(
strings.TrimSpace(
strings.ToLower(
strings.ReplaceAll(
strings.ReplaceAll(domains, "@", " "),
",", " "),
),
),
)
}
// Given a string, returns the first autolinked URL in the string as well as an array of all Markdown
// images of the form . Note that this does not return Markdown links of the
// form [text](url).
func (a *App) getFirstLinkAndImages(str string) (string, []string) {
firstLink := ""
images := []string{}
markdown.Inspect(str, func(blockOrInline any) bool {
switch v := blockOrInline.(type) {
case *markdown.Autolink:
if link := v.Destination(); firstLink == "" && a.isLinkAllowedForPreview(link) {
firstLink = link
}
case *markdown.InlineImage:
if link := v.Destination(); a.isLinkAllowedForPreview(link) {
images = append(images, link)
}
case *markdown.ReferenceImage:
if link := v.ReferenceDefinition.Destination(); a.isLinkAllowedForPreview(link) {
images = append(images, link)
}
}
return true
})
return firstLink, images
}
func (a *App) getImagesInMessageAttachments(post *model.Post) []string {
var images []string
for _, attachment := range post.Attachments() {
_, imagesInText := a.getFirstLinkAndImages(attachment.Text)
images = append(images, imagesInText...)
_, imagesInPretext := a.getFirstLinkAndImages(attachment.Pretext)
images = append(images, imagesInPretext...)
for _, field := range attachment.Fields {
if field == nil {
continue
}
if value, ok := field.Value.(string); ok {
_, imagesInFieldValue := a.getFirstLinkAndImages(value)
images = append(images, imagesInFieldValue...)
}
}
if attachment.AuthorIcon != "" {
images = append(images, attachment.AuthorIcon)
}
if attachment.ImageURL != "" {
images = append(images, attachment.ImageURL)
}
if attachment.ThumbURL != "" {
images = append(images, attachment.ThumbURL)
}
if attachment.FooterIcon != "" {
images = append(images, attachment.FooterIcon)
}
}
return images
}
func looksLikeAPermalink(url, siteURL string) bool {
expression := fmt.Sprintf(`^(%s).*(/pl/)[a-z0-9]{26}$`, siteURL)
matched, err := regexp.MatchString(expression, strings.TrimSpace(url))
if err != nil {
mlog.Warn("error matching regex", mlog.Err(err))
}
return matched
}
func (a *App) containsPermalink(post *model.Post) bool {
link, _ := a.getFirstLinkAndImages(post.Message)
if link == "" {
return false
}
return looksLikeAPermalink(link, a.GetSiteURL())
}
func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64, isNewPost bool, previewedPostPropVal string) (*opengraph.OpenGraph, *model.PostImage, *model.Permalink, error) {
requestURL = resolveMetadataURL(requestURL, a.GetSiteURL())
timestamp = model.FloorToNearestHour(timestamp)
// Check cache
og, image, permalink, ok := getLinkMetadataFromCache(requestURL, timestamp)
if !*a.Config().ServiceSettings.EnablePermalinkPreviews || !a.Config().FeatureFlags.PermalinkPreviews {
permalink = nil
}
if ok && previewedPostPropVal == "" {
return og, image, permalink, nil
}
// Check the database if this isn't a new post. If it is a new post and the data is cached, it should be in memory.
if !isNewPost {
og, image, ok = a.getLinkMetadataFromDatabase(requestURL, timestamp)
if ok && previewedPostPropVal == "" {
cacheLinkMetadata(requestURL, timestamp, og, image, nil)
return og, image, nil, nil
}
}
var err error
if looksLikeAPermalink(requestURL, a.GetSiteURL()) && *a.Config().ServiceSettings.EnablePermalinkPreviews && a.Config().FeatureFlags.PermalinkPreviews {
referencedPostID := requestURL[len(requestURL)-26:]
referencedPost, appErr := a.GetSinglePost(referencedPostID, false)
// TODO: Look into saving a value in the LinkMetadata.Data field to prevent perpetually re-querying for the deleted post.
if appErr != nil {
return nil, nil, nil, appErr
}
referencedChannel, appErr := a.GetChannel(c, referencedPost.ChannelId)
if appErr != nil {
return nil, nil, nil, appErr
}
var referencedTeam *model.Team
if referencedChannel.Type == model.ChannelTypeDirect || referencedChannel.Type == model.ChannelTypeGroup {
referencedTeam = &model.Team{}
} else {
referencedTeam, appErr = a.GetTeam(referencedChannel.TeamId)
if appErr != nil {
return nil, nil, nil, appErr
}
}
// Get metadata for embedded post
if a.containsPermalink(referencedPost) {
// referencedPost contains a permalink: we don't get its metadata
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPost, referencedTeam, referencedChannel)}
} else {
// referencedPost does not contain a permalink: we get its metadata
referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false, false)
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPostWithMetadata, referencedTeam, referencedChannel)}
}
} else {
var request *http.Request
// Make request for a web page or an image
request, err = http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, nil, nil, err
}
var body io.ReadCloser
var contentType string
if (request.URL.Scheme+"://"+request.URL.Host) == a.GetSiteURL() && request.URL.Path == "/api/v4/image" {
// /api/v4/image requires authentication, so bypass the API by hitting the proxy directly
body, contentType, err = a.ImageProxy().GetImageDirect(a.ImageProxy().GetUnproxiedImageURL(request.URL.String()))
} else {
request.Header.Add("Accept", "image/*")
request.Header.Add("Accept", "text/html;q=0.8")
request.Header.Add("Accept-Language", *a.Config().LocalizationSettings.DefaultServerLocale)
client := a.HTTPService().MakeClient(false)
client.Timeout = time.Duration(*a.Config().ExperimentalSettings.LinkMetadataTimeoutMilliseconds) * time.Millisecond
var res *http.Response
res, err = client.Do(request)
if res != nil {
body = res.Body
contentType = res.Header.Get("Content-Type")
}
}
if body != nil {
defer func() {
io.Copy(io.Discard, body)
body.Close()
}()
}
if err == nil {
// Parse the data
og, image, err = a.parseLinkMetadata(requestURL, body, contentType)
}
og = model.TruncateOpenGraph(og) // remove unwanted length of texts
a.saveLinkMetadataToDatabase(requestURL, timestamp, og, image)
}
// Write back to cache and database, even if there was an error and the results are nil
cacheLinkMetadata(requestURL, timestamp, og, image, permalink)
return og, image, permalink, err
}
// resolveMetadataURL resolves a given URL relative to the server's site URL.
func resolveMetadataURL(requestURL string, siteURL string) string {
base, err := url.Parse(siteURL)
if err != nil {
return ""
}
resolved, err := base.Parse(requestURL)
if err != nil {
return ""
}
return resolved.String()
}
func getLinkMetadataFromCache(requestURL string, timestamp int64) (*opengraph.OpenGraph, *model.PostImage, *model.Permalink, bool) {
var cached linkMetadataCache
err := platform.LinkCache().Get(strconv.FormatInt(model.GenerateLinkMetadataHash(requestURL, timestamp), 16), &cached)
if err != nil {
return nil, nil, nil, false
}
return cached.OpenGraph, cached.PostImage, cached.Permalink, true
}
func (a *App) getLinkMetadataFromDatabase(requestURL string, timestamp int64) (*opengraph.OpenGraph, *model.PostImage, bool) {
linkMetadata, err := a.Srv().Store().LinkMetadata().Get(requestURL, timestamp)
if err != nil {
return nil, nil, false
}
data := linkMetadata.Data
switch v := data.(type) {
case *opengraph.OpenGraph:
return v, nil, true
case *model.PostImage:
return nil, v, true
default:
return nil, nil, true
}
}
func (a *App) saveLinkMetadataToDatabase(requestURL string, timestamp int64, og *opengraph.OpenGraph, image *model.PostImage) {
metadata := &model.LinkMetadata{
URL: requestURL,
Timestamp: timestamp,
}
if og != nil {
metadata.Type = model.LinkMetadataTypeOpengraph
metadata.Data = og
} else if image != nil {
metadata.Type = model.LinkMetadataTypeImage
metadata.Data = image
} else {
metadata.Type = model.LinkMetadataTypeNone
}
_, err := a.Srv().Store().LinkMetadata().Save(metadata)
if err != nil {
mlog.Warn("Failed to write link metadata", mlog.String("request_url", requestURL), mlog.Err(err))
}
}
func cacheLinkMetadata(requestURL string, timestamp int64, og *opengraph.OpenGraph, image *model.PostImage, permalink *model.Permalink) {
metadata := linkMetadataCache{
OpenGraph: og,
PostImage: image,
Permalink: permalink,
}
platform.LinkCache().SetWithExpiry(strconv.FormatInt(model.GenerateLinkMetadataHash(requestURL, timestamp), 16), metadata, platform.LinkCacheDuration)
}
func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
if contentType == "image/svg+xml" {
image := &model.PostImage{
Format: "svg",
}
return nil, image, nil
} else if strings.HasPrefix(contentType, "image") {
image, err := parseImages(io.LimitReader(body, MaxMetadataImageSize))
return nil, image, err
} else if strings.HasPrefix(contentType, "text/html") {
og := a.parseOpenGraphMetadata(requestURL, body, contentType)
// The OpenGraph library and Go HTML library don't error for malformed input, so check that at least
// one of these required fields exists before returning the OpenGraph data
if og.Title != "" || og.Type != "" || og.URL != "" {
return og, nil, nil
}
return nil, nil, nil
} else {
// Not an image or web page with OpenGraph information
return nil, nil, nil
}
}
func parseImages(body io.Reader) (*model.PostImage, error) {
// Store any data that is read for the config for any further processing
buf := &bytes.Buffer{}
t := io.TeeReader(body, buf)
// Read the image config to get the format and dimensions
config, format, err := image.DecodeConfig(t)
if err != nil {
return nil, err
}
image := &model.PostImage{
Width: config.Width,
Height: config.Height,
Format: format,
}
if format == "gif" {
// Decoding the config may have read some of the image data, so re-read the data that has already been read first
frameCount, err := imgutils.CountGIFFrames(io.MultiReader(buf, body))
if err != nil {
return nil, err
}
image.FrameCount = frameCount
}
// Make image information nil when the format is tiff
if format == "tiff" {
image = nil
}
return image, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
priority, err := a.Srv().Store().PostPriority().GetForPost(postId)
if err != nil && err != sql.ErrNoRows {
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return priority, nil
}
func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
priority, err := a.Srv().Store().PostPriority().GetForPosts(list.Order)
if err != nil {
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
priorityMap := make(map[string]*model.PostPriority)
for _, p := range priority {
priorityMap[p.PostId] = p
}
return priorityMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
// Ensure preferences service wrapper implements `product.PreferencesService`
var _ product.PreferencesService = (*preferencesServiceWrapper)(nil)
// preferencesServiceWrapper provides an implementation of `product.PreferencesService` for use by products.
type preferencesServiceWrapper struct {
app AppIface
}
func (w *preferencesServiceWrapper) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
return w.app.GetPreferencesForUser(userID)
}
func (w *preferencesServiceWrapper) UpdatePreferencesForUser(userID string, preferences model.Preferences) *model.AppError {
return w.app.UpdatePreferences(userID, preferences)
}
func (w *preferencesServiceWrapper) DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError {
return w.app.DeletePreferences(userID, preferences)
}
func (a *App) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
preferences, err := a.Srv().Store().Preference().GetAll(userID)
if err != nil {
return nil, model.NewAppError("GetPreferencesForUser", "app.preference.get_all.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return preferences, nil
}
func (a *App) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) {
preferences, err := a.Srv().Store().Preference().GetCategory(userID, category)
if err != nil {
return nil, model.NewAppError("GetPreferenceByCategoryForUser", "app.preference.get_category.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(preferences) == 0 {
err := model.NewAppError("GetPreferenceByCategoryForUser", "api.preference.preferences_category.get.app_error", nil, "", http.StatusNotFound)
return nil, err
}
return preferences, nil
}
func (a *App) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) {
res, err := a.Srv().Store().Preference().Get(userID, category, preferenceName)
if err != nil {
return nil, model.NewAppError("GetPreferenceByCategoryAndNameForUser", "app.preference.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return res, nil
}
func (a *App) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError {
for _, preference := range preferences {
if userID != preference.UserId {
return model.NewAppError("savePreferences", "api.preference.update_preferences.set.app_error", nil,
"userId="+userID+", preference.UserId="+preference.UserId, http.StatusForbidden)
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("UpdatePreferences", "app.preference.save.updating.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if err := a.Srv().Store().Channel().UpdateSidebarChannelsByPreferences(preferences); err != nil {
return model.NewAppError("UpdatePreferences", "api.preference.update_preferences.update_sidebar.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, "", "", userID, nil, "")
// TODO this needs to be updated to include information on which categories changed
a.Publish(message)
message = model.NewWebSocketEvent(model.WebsocketEventPreferencesChanged, "", "", userID, nil, "")
prefsJSON, jsonErr := json.Marshal(preferences)
if jsonErr != nil {
return model.NewAppError("UpdatePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("preferences", string(prefsJSON))
a.Publish(message)
return nil
}
func (a *App) DeletePreferences(userID string, preferences model.Preferences) *model.AppError {
for _, preference := range preferences {
if userID != preference.UserId {
err := model.NewAppError("DeletePreferences", "api.preference.delete_preferences.delete.app_error", nil,
"userId="+userID+", preference.UserId="+preference.UserId, http.StatusForbidden)
return err
}
}
for _, preference := range preferences {
if err := a.Srv().Store().Preference().Delete(userID, preference.Category, preference.Name); err != nil {
return model.NewAppError("DeletePreferences", "app.preference.delete.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if err := a.Srv().Store().Channel().DeleteSidebarChannelsByPreferences(preferences); err != nil {
return model.NewAppError("DeletePreferences", "api.preference.delete_preferences.update_sidebar.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, "", "", userID, nil, "")
// TODO this needs to be updated to include information on which categories changed
a.Publish(message)
message = model.NewWebSocketEvent(model.WebsocketEventPreferencesDeleted, "", "", userID, nil, "")
prefsJSON, jsonErr := json.Marshal(preferences)
if jsonErr != nil {
return model.NewAppError("DeletePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("preferences", string(prefsJSON))
a.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"os"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
func (s *Server) initializeProducts(
productMap map[string]product.Manifest,
serviceMap map[product.ServiceKey]any,
) error {
// create a product map to consume
pmap := make(map[string]struct{})
for name := range productMap {
if !s.shouldStart(name) {
continue
}
pmap[name] = struct{}{}
}
// We figure out the initialization order by trial and error fashion hence maxTry
// is the maximum possible trials of initialization attempts. The order is not
// determined elsewhere therefore we do a on the fly sorting here. Which means the
// initialization order will be resolved during the loop.
maxTry := len(pmap) * len(pmap)
for len(pmap) > 0 && maxTry != 0 {
initLoop:
for product := range pmap {
manifest := productMap[product]
// we have dependencies defined. Here we check if the serviceMap
// has all the dependencies registered. If not, we continue to the
// loop to let other products initialize and register their services
// if they have any.
for key := range manifest.Dependencies {
if _, ok := serviceMap[key]; !ok {
maxTry--
continue initLoop
}
}
// some products can register themselves/their services
initializer := manifest.Initializer
prod, err := initializer(serviceMap)
if err != nil {
return fmt.Errorf("error initializing product %q: %w", product, err)
}
s.products[product] = prod
// we remove this product from the map to not try to initialize it again
delete(pmap, product)
}
}
if maxTry == 0 && len(pmap) != 0 {
var products string
for p := range pmap {
products = strings.Join([]string{products, fmt.Sprintf("%q", p)}, " ")
}
return fmt.Errorf("could not initialize product(s) due to circular dependency: %s", products)
}
return nil
}
func (s *Server) shouldStart(product string) bool {
if product == "boards" {
if !s.Config().FeatureFlags.BoardsProduct {
s.Log().Warn("Skipping boards start: not enabled via feature flag")
return false
}
}
if product == "playbooks" {
if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" {
s.Log().Warn("Skipping playbooks start: disabled via env var")
return false
}
}
return true
}
func (s *Server) HasBoardProduct() (bool, error) {
prod, exists := s.services[product.BoardsKey]
if !exists {
return false, nil
}
if prod == nil {
return false, errors.New("board product is nil")
}
if _, ok := prod.(product.BoardsService); !ok {
return false, errors.New("board product key does not match its definition")
}
return true, nil
}
func (a *App) HasBoardProduct() (bool, error) {
return a.Srv().HasBoardProduct()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
date_constraints "github.com/reflog/dateconstraints"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const MaxRepeatViewings = 3
const MinSecondsBetweenRepeatViewings = 60 * 60
// http request cache
var noticesCache = utils.RequestCache{}
var rcStripRegexp = regexp.MustCompile(`(.*?)(-rc\d+)(.*?)`)
func cleanupVersion(originalVersion string) string {
// clean up BuildNumber to remove release- prefix, -rc suffix and a hash part of the version
version := strings.Replace(originalVersion, "release-", "", 1)
version = rcStripRegexp.ReplaceAllString(version, `$1$3`)
versionParts := strings.Split(version, ".")
var versionPartsOut []string
for _, part := range versionParts {
if _, err := strconv.ParseInt(part, 10, 16); err == nil {
versionPartsOut = append(versionPartsOut, part)
}
}
return strings.Join(versionPartsOut, ".")
}
func noticeMatchesConditions(config *model.Config, preferences store.PreferenceStore, userID string,
client model.NoticeClientType, clientVersion string, postCount int64, userCount int64, isSystemAdmin bool,
isTeamAdmin bool, isCloud bool, sku, dbName, dbVer, searchEngineName, searchEngineVer string,
notice *model.ProductNotice) (bool, error) {
cnd := notice.Conditions
// check client type
if cnd.ClientType != nil {
if !cnd.ClientType.Matches(client) {
return false, nil
}
}
// check if client version is in notice range
clientVersions := cnd.DesktopVersion
if client == model.NoticeClientTypeMobileAndroid || client == model.NoticeClientTypeMobileIos {
clientVersions = cnd.MobileVersion
}
clientVersionParsed, err := semver.NewVersion(clientVersion)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse version range %s", clientVersion)
}
for _, v := range clientVersions {
c, err2 := semver.NewConstraint(v)
if err2 != nil {
return false, errors.Wrapf(err2, "Cannot parse version range %s", v)
}
if !c.Check(clientVersionParsed) {
return false, nil
}
}
// check if notice date range matches current
if cnd.DisplayDate != nil {
y, m, d := time.Now().UTC().Date()
trunc := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
c, err2 := date_constraints.NewConstraint(*cnd.DisplayDate)
if err2 != nil {
return false, errors.Wrapf(err2, "Cannot parse date range %s", *cnd.DisplayDate)
}
if !c.Check(&trunc) {
return false, nil
}
}
// check if current server version is notice range
if !isCloud && cnd.ServerVersion != nil {
version := cleanupVersion(model.BuildNumber)
serverVersion, err := semver.NewVersion(version)
if err != nil {
mlog.Warn("Build number is not in semver format", mlog.String("build_number", version))
return false, nil
}
for _, v := range cnd.ServerVersion {
c, err := semver.NewConstraint(v)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
}
if !c.Check(serverVersion) {
return false, nil
}
}
}
// check if sku matches our license
if cnd.Sku != nil {
if !cnd.Sku.Matches(sku) {
return false, nil
}
}
// check the target audience
if cnd.Audience != nil {
if !cnd.Audience.Matches(isSystemAdmin, isTeamAdmin) {
return false, nil
}
}
// check user count condition against previously calculated total user count
if cnd.NumberOfUsers != nil && userCount > 0 {
if userCount < *cnd.NumberOfUsers {
return false, nil
}
}
// check post count condition against previously calculated total post count
if cnd.NumberOfPosts != nil && postCount > 0 {
if postCount < *cnd.NumberOfPosts {
return false, nil
}
}
if cnd.DeprecatingDependency != nil {
extDepVersion, err := semver.NewVersion(cnd.DeprecatingDependency.MinimumVersion)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse external dependency version %s", cnd.DeprecatingDependency.MinimumVersion)
}
switch cnd.DeprecatingDependency.Name {
case model.DatabaseDriverMysql, model.DatabaseDriverPostgres:
if dbName != cnd.DeprecatingDependency.Name {
return false, nil
}
serverDBMSVersion, err := semver.NewVersion(dbVer)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse DBMS version %s", dbVer)
}
return extDepVersion.GreaterThan(serverDBMSVersion), nil
case model.SearchengineElasticsearch:
if searchEngineName != model.SearchengineElasticsearch {
return false, nil
}
semverESVersion, err := semver.NewVersion(searchEngineVer)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse search engine version %s", searchEngineVer)
}
return extDepVersion.GreaterThan(semverESVersion), nil
default:
return false, nil
}
}
// check if our server config matches the notice
for k, v := range cnd.ServerConfig {
if !validateConfigEntry(config, k, v) {
return false, nil
}
}
// check if user's config matches the notice
for k, v := range cnd.UserConfig {
res, err := validateUserConfigEntry(preferences, userID, k, v)
if err != nil {
return false, err
}
if !res {
return false, nil
}
}
// check the type of installation
if cnd.InstanceType != nil {
if !cnd.InstanceType.Matches(isCloud) {
return false, nil
}
}
return true, nil
}
func validateUserConfigEntry(preferences store.PreferenceStore, userID string, key string, expectedValue any) (bool, error) {
parts := strings.Split(key, ".")
if len(parts) != 2 {
return false, errors.New("Invalid format of user config. Must be in form of Category.SettingName")
}
if _, ok := expectedValue.(string); !ok {
return false, errors.New("Invalid format of user config. Value should be string")
}
pref, err := preferences.Get(userID, parts[0], parts[1])
if err != nil {
return false, nil
}
return pref.Value == expectedValue, nil
}
func validateConfigEntry(conf *model.Config, path string, expectedValue any) bool {
value, found := config.GetValueByPath(strings.Split(path, "."), *conf)
if !found {
return false
}
vt := reflect.ValueOf(value)
if vt.IsNil() {
return expectedValue == nil
}
if vt.Kind() == reflect.Ptr {
vt = vt.Elem()
}
val := vt.Interface()
return val == expectedValue
}
// GetProductNotices is called from the frontend to fetch the product notices that are relevant to the caller
func (a *App) GetProductNotices(c *request.Context, userID, teamID string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
isSystemAdmin := a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem)
isTeamAdmin := a.SessionHasPermissionToTeam(*c.Session(), teamID, model.PermissionManageTeam)
// check if notices for regular users are disabled
if !*a.Config().AnnouncementSettings.UserNoticesEnabled && !isSystemAdmin {
return []model.NoticeMessage{}, nil
}
// check if notices for admins are disabled
if !*a.Config().AnnouncementSettings.AdminNoticesEnabled && (isTeamAdmin || isSystemAdmin) {
return []model.NoticeMessage{}, nil
}
views, err := a.Srv().Store().ProductNotices().GetViews(userID)
if err != nil {
return nil, model.NewAppError("GetProductNotices", "api.system.update_viewed_notices.failed", nil, "", http.StatusBadRequest).Wrap(err)
}
sku := a.Srv().ClientLicense()["SkuShortName"]
isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
dbName := *a.Config().SqlSettings.DriverName
var searchEngineName, searchEngineVersion string
if engine := a.Srv().Platform().SearchEngine; engine != nil && engine.ElasticsearchEngine != nil {
searchEngineName = engine.ElasticsearchEngine.GetName()
searchEngineVersion = engine.ElasticsearchEngine.GetFullVersion()
}
filteredNotices := make([]model.NoticeMessage, 0)
for noticeIndex, notice := range a.ch.cachedNotices {
// check if the notice has been viewed already
var view *model.ProductNoticeViewState
for viewIndex, v := range views {
if v.NoticeId == notice.ID {
view = &views[viewIndex]
break
}
}
if view != nil {
repeatable := notice.Repeatable != nil && *notice.Repeatable
if repeatable {
if view.Viewed > MaxRepeatViewings {
continue
}
if (time.Now().UTC().Unix() - view.Timestamp) < MinSecondsBetweenRepeatViewings {
continue
}
} else if view.Viewed > 0 {
continue
}
}
result, err := noticeMatchesConditions(
a.Config(),
a.Srv().Store().Preference(),
userID,
client,
clientVersion,
a.ch.cachedPostCount,
a.ch.cachedUserCount,
isSystemAdmin,
isTeamAdmin,
isCloud,
sku,
dbName,
a.ch.cachedDBMSVersion,
searchEngineName,
searchEngineVersion,
&a.ch.cachedNotices[noticeIndex])
if err != nil {
return nil, model.NewAppError("GetProductNotices", "api.system.update_notices.validating_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
if result {
selectedLocale := "en"
filteredNotices = append(filteredNotices, model.NoticeMessage{
NoticeMessageInternal: notice.LocalizedMessages[selectedLocale],
ID: notice.ID,
TeamAdminOnly: notice.TeamAdminOnly(),
SysAdminOnly: notice.SysAdminOnly(),
})
}
}
return filteredNotices, nil
}
// UpdateViewedProductNotices is called from the frontend to mark a set of notices as 'viewed' by user
func (a *App) UpdateViewedProductNotices(userID string, noticeIds []string) *model.AppError {
if err := a.Srv().Store().ProductNotices().View(userID, noticeIds); err != nil {
return model.NewAppError("UpdateViewedProductNotices", "api.system.update_viewed_notices.failed", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
// UpdateViewedProductNoticesForNewUser is called when new user is created to mark all current notices for this
// user as viewed in order to avoid showing them imminently on first login
func (a *App) UpdateViewedProductNoticesForNewUser(userID string) {
var noticeIds []string
for _, notice := range a.ch.cachedNotices {
noticeIds = append(noticeIds, notice.ID)
}
if err := a.Srv().Store().ProductNotices().View(userID, noticeIds); err != nil {
mlog.Error("Cannot update product notices viewed state for user", mlog.String("userId", userID))
}
}
// UpdateProductNotices is called periodically from a scheduled worker to fetch new notices and update the cache
func (a *App) UpdateProductNotices() *model.AppError {
url := *a.Config().AnnouncementSettings.NoticesURL
skip := *a.Config().AnnouncementSettings.NoticesSkipCache
mlog.Debug("Will fetch notices from", mlog.String("url", url), mlog.Bool("skip_cache", skip))
var err error
a.ch.cachedPostCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil {
mlog.Warn("Failed to fetch post count", mlog.String("error", err.Error()))
}
a.ch.cachedUserCount, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
mlog.Warn("Failed to fetch user count", mlog.String("error", err.Error()))
}
a.ch.cachedDBMSVersion, err = a.Srv().Store().GetDbVersion(false)
if err != nil {
mlog.Warn("Failed to get DBMS version", mlog.String("error", err.Error()))
}
a.ch.cachedDBMSVersion = strings.Split(a.ch.cachedDBMSVersion, " ")[0] // get rid of trailing strings attached to the version
data, err := utils.GetURLWithCache(url, ¬icesCache, skip)
if err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.fetch_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
a.ch.cachedNotices, err = model.UnmarshalProductNotices(data)
if err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.parse_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Srv().Store().ProductNotices().ClearOldNotices(a.ch.cachedNotices); err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.clear_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"math"
"net/http"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store/memstore"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RateLimiter struct {
throttledRateLimiter *throttled.GCRARateLimiter
useAuth bool
useIP bool
header string
trustedProxyIPHeader []string
}
func NewRateLimiter(settings *model.RateLimitSettings, trustedProxyIPHeader []string) (*RateLimiter, error) {
store, err := memstore.New(*settings.MemoryStoreSize)
if err != nil {
return nil, errors.Wrap(err, i18n.T("api.server.start_server.rate_limiting_memory_store"))
}
quota := throttled.RateQuota{
MaxRate: throttled.PerSec(*settings.PerSec),
MaxBurst: *settings.MaxBurst,
}
throttledRateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
if err != nil {
return nil, errors.Wrap(err, i18n.T("api.server.start_server.rate_limiting_rate_limiter"))
}
return &RateLimiter{
throttledRateLimiter: throttledRateLimiter,
useAuth: *settings.VaryByUser,
useIP: *settings.VaryByRemoteAddr,
header: settings.VaryByHeader,
trustedProxyIPHeader: trustedProxyIPHeader,
}, nil
}
func (rl *RateLimiter) GenerateKey(r *http.Request) string {
key := ""
if rl.useAuth {
token, tokenLocation := ParseAuthTokenFromRequest(r)
if tokenLocation != TokenLocationNotFound {
key += token
} else if rl.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
key += utils.GetIPAddress(r, rl.trustedProxyIPHeader)
}
} else if rl.useIP { // Only if Auth based is not enabed do we use a plain IP based
key += utils.GetIPAddress(r, rl.trustedProxyIPHeader)
}
// Note that most of the time the user won't have to set this because the utils.GetIPAddress above tries the
// most common headers anyway.
if rl.header != "" {
key += strings.ToLower(r.Header.Get(rl.header))
}
return key
}
func (rl *RateLimiter) RateLimitWriter(key string, w http.ResponseWriter) bool {
limited, context, err := rl.throttledRateLimiter.RateLimit(key, 1)
if err != nil {
mlog.Error("Internal server error when rate limiting. Rate Limiting broken.", mlog.Err(err))
return false
}
setRateLimitHeaders(w, context)
if limited {
mlog.Debug("Denied due to throttling settings code=429", mlog.String("key", key))
http.Error(w, "limit exceeded", http.StatusTooManyRequests)
}
return limited
}
func (rl *RateLimiter) UserIdRateLimit(userID string, w http.ResponseWriter) bool {
if rl.useAuth {
return rl.RateLimitWriter(userID, w)
}
return false
}
func (rl *RateLimiter) RateLimitHandler(wrappedHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := rl.GenerateKey(r)
if !rl.RateLimitWriter(key, w) {
wrappedHandler.ServeHTTP(w, r)
}
})
}
// Copied from https://github.com/throttled/throttled http.go
func setRateLimitHeaders(w http.ResponseWriter, context throttled.RateLimitResult) {
if v := context.Limit; v >= 0 {
w.Header().Add("X-RateLimit-Limit", strconv.Itoa(v))
}
if v := context.Remaining; v >= 0 {
w.Header().Add("X-RateLimit-Remaining", strconv.Itoa(v))
}
if v := context.ResetAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("X-RateLimit-Reset", strconv.Itoa(vi))
}
if v := context.RetryAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("Retry-After", strconv.Itoa(vi))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) {
post, err := a.GetSinglePost(reaction.PostId, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("deleteReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
reaction, nErr := a.Srv().Store().Reaction().Save(reaction)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveReactionForPost", "app.reaction.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ReactionHasBeenAdded(pluginContext, reaction)
return true
}, plugin.ReactionHasBeenAddedID)
})
a.Srv().Go(func() {
a.sendReactionEvent(model.WebsocketEventReactionAdded, reaction, post)
})
return reaction, nil
}
func (a *App) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) {
reactions, err := a.Srv().Store().Reaction().GetForPost(postID, true)
if err != nil {
return nil, model.NewAppError("GetReactionsForPost", "app.reaction.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return reactions, nil
}
func (a *App) GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError) {
reactions := make(map[string][]*model.Reaction)
allReactions, err := a.Srv().Store().Reaction().BulkGetForPosts(postIDs)
if err != nil {
return nil, model.NewAppError("GetBulkReactionsForPosts", "app.reaction.bulk_get_for_post_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, reaction := range allReactions {
reactionsForPost := reactions[reaction.PostId]
reactionsForPost = append(reactionsForPost, reaction)
reactions[reaction.PostId] = reactionsForPost
}
reactions = populateEmptyReactions(postIDs, reactions)
return reactions, nil
}
func populateEmptyReactions(postIDs []string, reactions map[string][]*model.Reaction) map[string][]*model.Reaction {
for _, postID := range postIDs {
if _, present := reactions[postID]; !present {
reactions[postID] = []*model.Reaction{}
}
}
return reactions
}
func (a *App) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError {
post, err := a.GetSinglePost(reaction.PostId, false)
if err != nil {
return err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return err
}
if channel.DeleteAt > 0 {
return model.NewAppError("DeleteReactionForPost", "api.reaction.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
}
if _, err := a.Srv().Store().Reaction().Delete(reaction); err != nil {
return model.NewAppError("DeleteReactionForPost", "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ReactionHasBeenRemoved(pluginContext, reaction)
return true
}, plugin.ReactionHasBeenRemovedID)
})
a.Srv().Go(func() {
a.sendReactionEvent(model.WebsocketEventReactionRemoved, reaction, post)
})
return nil
}
func (a *App) sendReactionEvent(event string, reaction *model.Reaction, post *model.Post) {
// send out that a reaction has been added/removed
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
reactionJSON, err := json.Marshal(reaction)
if err != nil {
a.Log().Warn("Failed to encode reaction to JSON", mlog.Err(err))
}
message.Add("reaction", string(reactionJSON))
a.Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Save(rc)
if err != nil {
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save_not_unique.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Update(rc)
if err != nil {
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update_not_unique.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
deleted, err := a.Srv().Store().RemoteCluster().Delete(remoteClusterId)
if err != nil {
return false, model.NewAppError("DeleteRemoteCluster", "api.remote_cluster.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return deleted, nil
}
func (a *App) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Get(remoteClusterId)
if err != nil {
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
list, err := a.Srv().Store().RemoteCluster().GetAll(filter)
if err != nil {
return nil, model.NewAppError("GetAllRemoteClusters", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().UpdateTopics(remoteClusterId, topics)
if err != nil {
return nil, model.NewAppError("UpdateRemoteClusterTopics", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
err := a.Srv().Store().RemoteCluster().SetLastPingAt(remoteClusterId)
if err != nil {
return model.NewAppError("SetRemoteClusterLastPingAt", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
service := a.Srv().GetRemoteClusterService()
if service == nil {
return nil, model.NewAppError("GetRemoteClusterService", "api.remote_cluster.service_not_enabled.app_error", nil, "", http.StatusNotImplemented)
}
return service, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
)
// MockOptionRemoteClusterService a mock of the remote cluster service
type MockOptionRemoteClusterService func(service *mockRemoteClusterService)
func MockOptionRemoteClusterServiceWithActive(active bool) MockOptionRemoteClusterService {
return func(mrcs *mockRemoteClusterService) {
mrcs.active = active
}
}
func NewMockRemoteClusterService(service remotecluster.RemoteClusterServiceIFace, options ...MockOptionRemoteClusterService) *mockRemoteClusterService {
mrcs := &mockRemoteClusterService{service, true}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockRemoteClusterService struct {
remotecluster.RemoteClusterServiceIFace
active bool
}
func (mrcs *mockRemoteClusterService) Shutdown() error {
return nil
}
func (mrcs *mockRemoteClusterService) Start() error {
return nil
}
func (mrcs *mockRemoteClusterService) Active() bool {
return mrcs.active
}
func (mrcs *mockRemoteClusterService) AddTopicListener(topic string, listener remotecluster.TopicListener) string {
return model.NewId()
}
func (mrcs *mockRemoteClusterService) RemoveTopicListener(listenerId string) {
}
func (mrcs *mockRemoteClusterService) AddConnectionStateListener(listener remotecluster.ConnectionStateListener) string {
return model.NewId()
}
func (mrcs *mockRemoteClusterService) RemoveConnectionStateListener(listenerId string) {
}
func (mrcs *mockRemoteClusterService) SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f remotecluster.SendMsgResultFunc) error {
return nil
}
func (mrcs *mockRemoteClusterService) SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp remotecluster.ReaderProvider, f remotecluster.SendFileResultFunc) error {
return nil
}
func (mrcs *mockRemoteClusterService) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
return nil, nil
}
func (mrcs *mockRemoteClusterService) ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) remotecluster.Response {
return remotecluster.Response{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package request
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Context struct {
t i18n.TranslateFunc
session model.Session
requestId string
ipAddress string
path string
userAgent string
acceptLanguage string
logger mlog.LoggerIFace
err *model.AppError
context context.Context
}
func NewContext(ctx context.Context, requestId, ipAddress, path, userAgent, acceptLanguage string, session model.Session, t i18n.TranslateFunc) *Context {
return &Context{
t: t,
session: session,
requestId: requestId,
ipAddress: ipAddress,
path: path,
userAgent: userAgent,
acceptLanguage: acceptLanguage,
context: ctx,
}
}
func EmptyContext(logger mlog.LoggerIFace) *Context {
return &Context{
t: i18n.T,
logger: logger,
context: context.Background(),
}
}
func (c *Context) T(translationID string, args ...any) string {
return c.t(translationID, args...)
}
func (c *Context) Session() *model.Session {
return &c.session
}
func (c *Context) RequestId() string {
return c.requestId
}
func (c *Context) IPAddress() string {
return c.ipAddress
}
func (c *Context) Path() string {
return c.path
}
func (c *Context) UserAgent() string {
return c.userAgent
}
func (c *Context) AcceptLanguage() string {
return c.acceptLanguage
}
func (c *Context) Context() context.Context {
return c.context
}
func (c *Context) SetSession(s *model.Session) {
c.session = *s
}
func (c *Context) SetT(t i18n.TranslateFunc) {
c.t = t
}
func (c *Context) SetRequestId(s string) {
c.requestId = s
}
func (c *Context) SetIPAddress(s string) {
c.ipAddress = s
}
func (c *Context) SetUserAgent(s string) {
c.userAgent = s
}
func (c *Context) SetAcceptLanguage(s string) {
c.acceptLanguage = s
}
func (c *Context) SetPath(s string) {
c.path = s
}
func (c *Context) SetContext(ctx context.Context) {
c.context = ctx
}
func (c *Context) GetT() i18n.TranslateFunc {
return c.t
}
func (c *Context) SetLogger(logger mlog.LoggerIFace) {
c.logger = logger
}
func (c *Context) Logger() mlog.LoggerIFace {
return c.logger
}
func (c *Context) SetAppError(err *model.AppError) {
c.err = err
}
func (c *Context) AppError() *model.AppError {
return c.err
}
type CTX interface {
T(string, ...interface{}) string
Session() *model.Session
RequestId() string
IPAddress() string
Path() string
UserAgent() string
AcceptLanguage() string
Context() context.Context
SetSession(s *model.Session)
SetT(i18n.TranslateFunc)
SetRequestId(string)
SetIPAddress(string)
SetUserAgent(string)
SetAcceptLanguage(string)
SetPath(string)
SetContext(ctx context.Context)
GetT() i18n.TranslateFunc
SetLogger(mlog.LoggerIFace)
Logger() mlog.LoggerIFace
SetAppError(*model.AppError)
AppError() *model.AppError
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
)
type PluginResponseWriter struct {
bytes.Buffer
headers http.Header
statusCode int
}
func (rt *PluginResponseWriter) Header() http.Header {
if rt.headers == nil {
rt.headers = make(http.Header)
}
return rt.headers
}
func (rt *PluginResponseWriter) WriteHeader(statusCode int) {
rt.statusCode = statusCode
}
// From net/http/httptest/recorder.go
func parseContentLength(cl string) int64 {
cl = strings.TrimSpace(cl)
if cl == "" {
return -1
}
n, err := strconv.ParseInt(cl, 10, 64)
if err != nil {
return -1
}
return n
}
func (rt *PluginResponseWriter) GenerateResponse() *http.Response {
res := &http.Response{
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
StatusCode: rt.statusCode,
Header: rt.headers.Clone(),
}
if res.StatusCode == 0 {
res.StatusCode = http.StatusOK
}
res.Status = fmt.Sprintf("%03d %s", res.StatusCode, http.StatusText(res.StatusCode))
if rt.Len() > 0 {
res.Body = io.NopCloser(rt)
} else {
res.Body = http.NoBody
}
res.ContentLength = parseContentLength(rt.headers.Get("Content-Length"))
return res
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func (a *App) GetRole(id string) (*model.Role, *model.AppError) {
role, err := a.Srv().Store().Role().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetRole", "app.role.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
appErr := a.Srv().mergeChannelHigherScopedPermissions([]*model.Role{role})
if appErr != nil {
return nil, appErr
}
return role, nil
}
func (a *App) GetAllRoles() ([]*model.Role, *model.AppError) {
roles, err := a.Srv().Store().Role().GetAll()
if err != nil {
return nil, model.NewAppError("GetAllRoles", "app.role.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
appErr := a.Srv().mergeChannelHigherScopedPermissions(roles)
if appErr != nil {
return nil, appErr
}
return roles, nil
}
func (s *Server) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
role, nErr := s.Store().Role().GetByName(ctx, name)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetRoleByName", "app.role.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetRoleByName", "app.role.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
err := s.mergeChannelHigherScopedPermissions([]*model.Role{role})
if err != nil {
return nil, err
}
return role, nil
}
func (a *App) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
return a.Srv().GetRoleByName(ctx, name)
}
func (a *App) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().GetByNames(names)
if nErr != nil {
return nil, model.NewAppError("GetRolesByNames", "app.role.get_by_names.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
err := a.mergeChannelHigherScopedPermissions(roles)
if err != nil {
return nil, err
}
return roles, nil
}
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
// moderated, and the value of the permission on the higher-scoped scheme.
func (s *Server) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {
var higherScopeNamesToQuery []string
for _, role := range roles {
if role.SchemeManaged {
higherScopeNamesToQuery = append(higherScopeNamesToQuery, role.Name)
}
}
if len(higherScopeNamesToQuery) == 0 {
return nil
}
higherScopedPermissionsMap, err := s.Store().Role().ChannelHigherScopedPermissions(higherScopeNamesToQuery)
if err != nil {
return model.NewAppError("mergeChannelHigherScopedPermissions", "app.role.get_by_names.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, role := range roles {
if role.SchemeManaged {
if higherScopedPermissions, ok := higherScopedPermissionsMap[role.Name]; ok {
role.MergeChannelHigherScopedPermissions(higherScopedPermissions)
}
}
}
return nil
}
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
// moderated, and the value of the permission on the higher-scoped scheme.
func (a *App) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {
return a.Srv().mergeChannelHigherScopedPermissions(roles)
}
func (a *App) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) {
// If patch is a no-op then short-circuit the store.
if patch.Permissions != nil && reflect.DeepEqual(*patch.Permissions, role.Permissions) {
return role, nil
}
role.Patch(patch)
role, err := a.UpdateRole(role)
if err != nil {
return nil, err
}
if appErr := a.sendUpdatedRoleEvent(role); appErr != nil {
return nil, appErr
}
return role, err
}
func (a *App) CreateRole(role *model.Role) (*model.Role, *model.AppError) {
role.Id = ""
role.CreateAt = 0
role.UpdateAt = 0
role.DeleteAt = 0
role.BuiltIn = false
role.SchemeManaged = false
var err error
role, err = a.Srv().Store().Role().Save(role)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateRole", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateRole", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return role, nil
}
func (a *App) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
savedRole, err := a.Srv().Store().Role().Save(role)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateRole", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateRole", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
builtInChannelRoles := []string{
model.ChannelGuestRoleId,
model.ChannelUserRoleId,
model.ChannelAdminRoleId,
}
builtInRolesMinusChannelRoles := append(utils.RemoveStringsFromSlice(model.BuiltInSchemeManagedRoleIDs, builtInChannelRoles...), model.NewSystemRoleIDs...)
if utils.StringInSlice(savedRole.Name, builtInRolesMinusChannelRoles) {
return savedRole, nil
}
var roleRetrievalFunc func() ([]*model.Role, *model.AppError)
if utils.StringInSlice(savedRole.Name, builtInChannelRoles) {
roleRetrievalFunc = func() ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().AllChannelSchemeRoles()
if nErr != nil {
return nil, model.NewAppError("UpdateRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return roles, nil
}
} else {
roleRetrievalFunc = func() ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().ChannelRolesUnderTeamRole(savedRole.Name)
if nErr != nil {
return nil, model.NewAppError("UpdateRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return roles, nil
}
}
impactedRoles, appErr := roleRetrievalFunc()
if appErr != nil {
return nil, appErr
}
impactedRoles = append(impactedRoles, role)
appErr = a.mergeChannelHigherScopedPermissions(impactedRoles)
if appErr != nil {
return nil, appErr
}
for _, ir := range impactedRoles {
if ir.Name != role.Name {
appErr = a.sendUpdatedRoleEvent(ir)
if appErr != nil {
return nil, appErr
}
}
}
return savedRole, nil
}
func (a *App) CheckRolesExist(roleNames []string) *model.AppError {
roles, err := a.GetRolesByNames(roleNames)
if err != nil {
return err
}
for _, name := range roleNames {
nameFound := false
for _, role := range roles {
if name == role.Name {
nameFound = true
break
}
}
if !nameFound {
return model.NewAppError("CheckRolesExist", "app.role.check_roles_exist.role_not_found", nil, "role="+name, http.StatusBadRequest)
}
}
return nil
}
func (a *App) sendUpdatedRoleEvent(role *model.Role) *model.AppError {
message := model.NewWebSocketEvent(model.WebsocketEventRoleUpdated, "", "", "", nil, "")
roleJSON, jsonErr := json.Marshal(role)
if jsonErr != nil {
return model.NewAppError("sendUpdatedRoleEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("role", string(roleJSON))
a.Publish(message)
return nil
}
func RemoveRoles(rolesToRemove []string, roles string) string {
roleList := strings.Fields(roles)
newRoles := make([]string, 0)
for _, role := range roleList {
shouldRemove := false
for _, roleToRemove := range rolesToRemove {
if role == roleToRemove {
shouldRemove = true
break
}
}
if !shouldRemove {
newRoles = append(newRoles, role)
}
}
return strings.Join(newRoles, " ")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/x509"
"encoding/pem"
"encoding/xml"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
SamlPublicCertificateName = "saml-public.crt"
SamlPrivateKeyName = "saml-private.key"
SamlIdpCertificateName = "saml-idp.crt"
)
func (a *App) GetSamlMetadata() (string, *model.AppError) {
if a.Saml() == nil {
err := model.NewAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return "", err
}
result, err := a.Saml().GetMetadata()
if err != nil {
return "", model.NewAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message, err.StatusCode)
}
return result, nil
}
func (a *App) writeSamlFile(filename string, fileData *multipart.FileHeader) *model.AppError {
file, err := fileData.Open()
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(filename, data)
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlPublicCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PublicCertificateFile = SamlPublicCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlPrivateKeyName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PrivateKeyFile = SamlPrivateKeyName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlIdpCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) removeSamlFile(filename string) *model.AppError {
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("RemoveSamlFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveSamlPublicCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.PublicCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PublicCertificateFile = ""
*cfg.SamlSettings.Encrypt = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveSamlPrivateCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.PrivateKeyFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PrivateKeyFile = ""
*cfg.SamlSettings.Encrypt = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveSamlIdpCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.IdpCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = ""
*cfg.SamlSettings.Enable = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus {
status := &model.SamlCertificateStatus{}
status.IdpCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.IdpCertificateFile)
status.PrivateKeyFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PrivateKeyFile)
status.PublicCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PublicCertificateFile)
return status
}
func (a *App) GetSamlMetadataFromIdp(idpMetadataURL string) (*model.SamlMetadataResponse, *model.AppError) {
if a.Saml() == nil {
err := model.NewAppError("GetSamlMetadataFromIdp", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return nil, err
}
if !strings.HasPrefix(idpMetadataURL, "http://") && !strings.HasPrefix(idpMetadataURL, "https://") {
idpMetadataURL = "https://" + idpMetadataURL
}
idpMetadataRaw, err := a.FetchSamlMetadataFromIdp(idpMetadataURL)
if err != nil {
return nil, err
}
data, err := a.BuildSamlMetadataObject(idpMetadataRaw)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) {
resp, err := a.HTTPService().MakeClient(false).Get(url)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if resp.StatusCode != http.StatusOK {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, fmt.Sprintf("status_code=%d", resp.StatusCode), http.StatusBadRequest)
}
defer resp.Body.Close()
bodyXML, err := io.ReadAll(resp.Body)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.failure_read_response_body_from_idp.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bodyXML, nil
}
func (a *App) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) {
entityDescriptor := model.EntityDescriptor{}
err := xml.Unmarshal(idpMetadata, &entityDescriptor)
if err != nil {
return nil, model.NewAppError("BuildSamlMetadataObject", "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
data := &model.SamlMetadataResponse{}
data.IdpDescriptorURL = entityDescriptor.EntityID
if entityDescriptor.IDPSSODescriptors == nil || len(entityDescriptor.IDPSSODescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
idpSSODescriptor := entityDescriptor.IDPSSODescriptors[0]
if idpSSODescriptor.SingleSignOnServices == nil || len(idpSSODescriptor.SingleSignOnServices) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_ssoservices.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
data.IdpURL = idpSSODescriptor.SingleSignOnServices[0].Location
if idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors == nil || len(idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
keyDescriptor := idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors[0]
data.IdpPublicCertificate = keyDescriptor.KeyInfo.X509Data.X509Certificate.Cert
return data, nil
}
func (a *App) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
const certPrefix = "-----BEGIN CERTIFICATE-----\n"
const certSuffix = "\n-----END CERTIFICATE-----"
fixedCertTxt := certPrefix + string(data) + certSuffix
block, _ := pem.Decode([]byte(fixedCertTxt))
if _, e := x509.ParseCertificate(block.Bytes); e != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_parse_idp_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(e)
}
data = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: block.Bytes,
})
if err := a.Srv().platform.SetConfigFile(SamlIdpCertificateName, data); err != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_save_idp_certificate_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (numAffected int, appErr *model.AppError) {
if a.Saml() == nil {
appErr = model.NewAppError("ResetAuthDataToEmail", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
numAffected, err := a.Srv().Store().User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, userIDs, includeDeleted, dryRun)
if err != nil {
appErr = model.NewAppError("ResetAuthDataToEmail", "api.admin.saml.failure_reset_authdata_to_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) {
if appErr := a.IsPhase2MigrationCompleted(); appErr != nil {
return nil, appErr
}
scheme, err := a.Srv().Store().Scheme().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetScheme", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetScheme", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetSchemeByName(name string) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().GetByName(name)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSchemeByName", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSchemeByName", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetSchemes(scope, page*perPage, perPage)
}
func (s *Server) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
if err := s.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := s.Store().Scheme().GetAllPage(scope, offset, limit)
if err != nil {
return nil, model.NewAppError("GetSchemes", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return scheme, nil
}
func (a *App) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
return a.Srv().GetSchemes(scope, offset, limit)
}
func (a *App) CreateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
// Clear any user-provided values for trusted properties.
scheme.DefaultTeamAdminRole = ""
scheme.DefaultTeamUserRole = ""
scheme.DefaultTeamGuestRole = ""
scheme.DefaultChannelAdminRole = ""
scheme.DefaultChannelUserRole = ""
scheme.DefaultChannelGuestRole = ""
scheme.DefaultPlaybookAdminRole = ""
scheme.DefaultPlaybookMemberRole = ""
scheme.DefaultRunAdminRole = ""
scheme.DefaultRunMemberRole = ""
scheme.CreateAt = 0
scheme.UpdateAt = 0
scheme.DeleteAt = 0
scheme, err := a.Srv().Store().Scheme().Save(scheme)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateScheme", "app.scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateScheme", "app.scheme.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme.Patch(patch)
scheme, err := a.UpdateScheme(scheme)
if err != nil {
return nil, err
}
return scheme, err
}
func (a *App) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().Save(scheme)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateScheme", "app.scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateScheme", "app.scheme.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().Delete(schemeId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteScheme", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteScheme", "app.scheme.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetTeamsForSchemePage(scheme *model.Scheme, page int, perPage int) ([]*model.Team, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetTeamsForScheme(scheme, page*perPage, perPage)
}
func (a *App) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
teams, err := a.Srv().Store().Team().GetTeamsByScheme(scheme.Id, offset, limit)
if err != nil {
return nil, model.NewAppError("GetTeamsForScheme", "app.team.get_by_scheme.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetChannelsForScheme(scheme, page*perPage, perPage)
}
func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
channelList, nErr := a.Srv().Store().Channel().GetChannelsByScheme(scheme.Id, offset, limit)
if nErr != nil {
return nil, model.NewAppError("GetChannelsForScheme", "app.channel.get_by_scheme.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return channelList, nil
}
func (s *Server) IsPhase2MigrationCompleted() *model.AppError {
if s.phase2PermissionsMigrationComplete {
return nil
}
if _, err := s.Store().System().GetByName(model.MigrationKeyAdvancedPermissionsPhase2); err != nil {
return model.NewAppError("App.IsPhase2MigrationCompleted", "app.schemes.is_phase_2_migration_completed.not_completed.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
s.phase2PermissionsMigrationComplete = true
return nil
}
func (a *App) IsPhase2MigrationCompleted() *model.AppError {
return a.Srv().IsPhase2MigrationCompleted()
}
func (a *App) SchemesIterator(scope string, batchSize int) func() []*model.Scheme {
offset := 0
return func() []*model.Scheme {
schemes, err := a.Srv().Store().Scheme().GetAllPage(scope, offset, batchSize)
if err != nil {
return []*model.Scheme{}
}
offset += batchSize
return schemes
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
func (a *App) TestElasticsearch(cfg *model.Config) *model.AppError {
if *cfg.ElasticsearchSettings.Password == model.FakeSetting {
if *cfg.ElasticsearchSettings.ConnectionURL == *a.Config().ElasticsearchSettings.ConnectionURL && *cfg.ElasticsearchSettings.Username == *a.Config().ElasticsearchSettings.Username {
*cfg.ElasticsearchSettings.Password = *a.Config().ElasticsearchSettings.Password
} else {
return model.NewAppError("TestElasticsearch", "ent.elasticsearch.test_config.reenter_password", nil, "", http.StatusBadRequest)
}
}
seI := a.SearchEngine().ElasticsearchEngine
if seI == nil {
err := model.NewAppError("TestElasticsearch", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
return err
}
if err := seI.TestConfig(cfg); err != nil {
return err
}
return nil
}
func (a *App) SetSearchEngine(se *searchengine.Broker) {
a.ch.srv.platform.SearchEngine = se
}
func (a *App) PurgeElasticsearchIndexes() *model.AppError {
engine := a.SearchEngine().ElasticsearchEngine
if engine == nil {
err := model.NewAppError("PurgeElasticsearchIndexes", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
return err
}
if err := engine.PurgeIndexes(); err != nil {
return err
}
return nil
}
func (a *App) PurgeBleveIndexes() *model.AppError {
engine := a.SearchEngine().BleveEngine
if engine == nil {
err := model.NewAppError("PurgeBleveIndexes", "searchengine.bleve.disabled.error", nil, "", http.StatusNotImplemented)
return err
}
if err := engine.PurgeIndexes(); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"io"
"net/http"
"net/url"
"runtime"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
PropSecurityURL = "https://securityupdatecheck.mattermost.com"
SecurityUpdatePeriod = 86400000 // 24 hours in milliseconds.
PropSecurityID = "id"
PropSecurityBuild = "b"
PropSecurityEnterpriseReady = "be"
PropSecurityDatabase = "db"
PropSecurityOS = "os"
PropSecurityUserCount = "uc"
PropSecurityTeamCount = "tc"
PropSecurityActiveUserCount = "auc"
PropSecurityUnitTests = "ut"
)
func (s *Server) DoSecurityUpdateCheck() {
if !*s.platform.Config().ServiceSettings.EnableSecurityFixAlert {
return
}
props, err := s.Store().System().Get()
if err != nil {
return
}
lastSecurityTime, _ := strconv.ParseInt(props[model.SystemLastSecurityTime], 10, 0)
currentTime := model.GetMillis()
if (currentTime - lastSecurityTime) > SecurityUpdatePeriod {
mlog.Debug("Checking for security update from Mattermost")
v := url.Values{}
v.Set(PropSecurityID, s.TelemetryId())
v.Set(PropSecurityBuild, model.CurrentVersion+"."+model.BuildNumber)
v.Set(PropSecurityEnterpriseReady, model.BuildEnterpriseReady)
v.Set(PropSecurityDatabase, *s.platform.Config().SqlSettings.DriverName)
v.Set(PropSecurityOS, runtime.GOOS)
if props[model.SystemRanUnitTests] != "" {
v.Set(PropSecurityUnitTests, "1")
} else {
v.Set(PropSecurityUnitTests, "0")
}
systemSecurityLastTime := &model.System{Name: model.SystemLastSecurityTime, Value: strconv.FormatInt(currentTime, 10)}
if lastSecurityTime == 0 {
s.Store().System().Save(systemSecurityLastTime)
} else {
s.Store().System().Update(systemSecurityLastTime)
}
if count, err := s.Store().User().Count(model.UserCountOptions{IncludeDeleted: true}); err == nil {
v.Set(PropSecurityUserCount, strconv.FormatInt(count, 10))
}
if ucr, err := s.Store().Status().GetTotalActiveUsersCount(); err == nil {
v.Set(PropSecurityActiveUserCount, strconv.FormatInt(ucr, 10))
}
if teamCount, err := s.Store().Team().AnalyticsTeamCount(nil); err == nil {
v.Set(PropSecurityTeamCount, strconv.FormatInt(teamCount, 10))
}
res, err := http.Get(PropSecurityURL + "/security?" + v.Encode())
if err != nil {
mlog.Error("Failed to get security update information from Mattermost.")
return
}
defer res.Body.Close()
var bulletins model.SecurityBulletins
if jsonErr := json.NewDecoder(res.Body).Decode(&bulletins); jsonErr != nil {
s.Log().Error("Failed to decode JSON", mlog.Err(jsonErr))
return
}
for _, bulletin := range bulletins {
if bulletin.AppliesToVersion == model.CurrentVersion {
if props["SecurityBulletin_"+bulletin.Id] == "" {
users, userErr := s.Store().User().GetSystemAdminProfiles()
if userErr != nil {
mlog.Error("Failed to get system admins for security update information from Mattermost.")
return
}
resBody, err := http.Get(PropSecurityURL + "/bulletins/" + bulletin.Id)
if err != nil {
mlog.Error("Failed to get security bulletin details")
return
}
body, err := io.ReadAll(resBody.Body)
resBody.Body.Close()
if err != nil || resBody.StatusCode != 200 {
mlog.Error("Failed to read security bulletin details")
return
}
for _, user := range users {
mlog.Info("Sending security bulletin", mlog.String("bulletin_id", bulletin.Id), mlog.String("user_email", user.Email))
license := s.License()
mailConfig := s.MailServiceConfig()
mail.SendMailUsingConfig(user.Email, i18n.T("mattermost.bulletin.subject"), string(body), mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", "SecurityUpdateCheck")
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
s.Store().System().Save(bulletinSeen)
}
}
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/rs/cors"
"golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/scheduler"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/active_users"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/expirynotify"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/export_delete"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/export_process"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/extract_content"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/hosted_purchase_screening"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/import_delete"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/import_process"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/last_accessible_file"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/last_accessible_post"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/migrations"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/notify_admin"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/product_notices"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/resend_invitation_email"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/awsmeter"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine/indexer"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/services/upgrader"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// declaring this as var to allow overriding in tests
var SentryDSN = "placeholder_sentry_dsn"
type Server struct {
// RootRouter is the starting point for all HTTP requests to the server.
RootRouter *mux.Router
// LocalRouter is the starting point for all the local UNIX socket
// requests to the server
LocalRouter *mux.Router
// Router is the starting point for all web, api4 and ws requests to the server. It differs
// from RootRouter only if the SiteURL contains a /subpath.
Router *mux.Router
Server *http.Server
ListenAddr *net.TCPAddr
RateLimiter *RateLimiter
localModeServer *http.Server
didFinishListen chan struct{}
EmailService email.ServiceInterface
httpService httpservice.HTTPService
PushNotificationsHub PushNotificationsHub
pushNotificationClient *http.Client // TODO: move this to it's own package
runEssentialJobs bool
Jobs *jobs.JobServer
licenseWrapper *licenseWrapper
timezones *timezones.Timezones
htmlTemplateWatcher *templates.Container
seenPendingPostIdsCache cache.Cache
openGraphDataCache cache.Cache
clusterLeaderListenerId string
loggerLicenseListenerId string
platform *platform.PlatformService
platformOptions []platform.Option
telemetryService *telemetry.TelemetryService
userService *users.UserService
teamService *teams.TeamService
serviceMux sync.RWMutex
remoteClusterService remotecluster.RemoteClusterServiceIFace
sharedChannelService SharedChannelServiceIFace // TODO: platform: move to platform package
phase2PermissionsMigrationComplete bool
Audit *audit.Audit
joinCluster bool
// startSearchEngine bool
skipPostInit bool
Cloud einterfaces.CloudInterface
tracer *tracing.Tracer
products map[string]product.Product
services map[product.ServiceKey]any
hooksManager *product.HooksManager
}
func (s *Server) Store() store.Store {
if s.platform != nil {
return s.platform.Store
}
return nil
}
func (s *Server) SetStore(st store.Store) {
if s.platform != nil {
s.platform.Store = st
}
}
func NewServer(options ...Option) (*Server, error) {
rootRouter := mux.NewRouter()
localRouter := mux.NewRouter()
s := &Server{
RootRouter: rootRouter,
LocalRouter: localRouter,
timezones: timezones.New(),
products: make(map[string]product.Product),
services: make(map[product.ServiceKey]any),
}
for _, option := range options {
if err := option(s); err != nil {
return nil, errors.Wrap(err, "failed to apply option")
}
}
// Following outlines the specific set of steps
// performed during server bootup. They are sensitive to order
// and has dependency requirements with the previous step.
//
// Step 1: Platform.
if s.platform == nil {
ps, sErr := platform.New(platform.ServiceConfig{}, s.platformOptions...)
if sErr != nil {
return nil, errors.Wrap(sErr, "failed to initialize platform")
}
s.platform = ps
}
subpath, err := utils.GetSubpathFromConfig(s.platform.Config())
if err != nil {
return nil, errors.Wrap(err, "failed to parse SiteURL subpath")
}
s.Router = s.RootRouter.PathPrefix(subpath).Subrouter()
s.httpService = httpservice.MakeHTTPService(s.platform)
// Step 2: Init Enterprise
// Depends on step 1 (s.Platform must be non-nil)
s.initEnterprise()
// Needed to run before loading license.
s.userService, err = users.New(users.ServiceConfig{
UserStore: s.Store().User(),
SessionStore: s.Store().Session(),
OAuthStore: s.Store().OAuth(),
ConfigFn: s.platform.Config,
Metrics: s.GetMetrics(),
Cluster: s.platform.Cluster(),
LicenseFn: s.License,
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create users service")
}
if model.BuildEnterpriseReady == "true" {
// Dependent on user service
s.LoadLicense()
}
s.licenseWrapper = &licenseWrapper{
srv: s,
}
s.teamService, err = teams.New(teams.ServiceConfig{
TeamStore: s.Store().Team(),
ChannelStore: s.Store().Channel(),
GroupStore: s.Store().Group(),
Users: s.userService,
WebHub: s.platform,
ConfigFn: s.platform.Config,
LicenseFn: s.License,
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create teams service")
}
s.hooksManager = product.NewHooksManager(s.GetMetrics())
// ensure app implements `product.UserService`
var _ product.UserService = (*App)(nil)
app := New(ServerConnector(s.Channels()))
serviceMap := map[product.ServiceKey]any{
ServerKey: s,
product.ConfigKey: s.platform,
product.LicenseKey: s.licenseWrapper,
product.FilestoreKey: s.platform.FileBackend(),
product.FileInfoStoreKey: &fileInfoWrapper{srv: s},
product.ClusterKey: s.platform,
product.UserKey: app,
product.LogKey: s.platform.Log(),
product.CloudKey: &cloudWrapper{cloud: s.Cloud},
product.KVStoreKey: s.platform,
product.StoreKey: store.NewStoreServiceAdapter(s.Store()),
product.SystemKey: &systemServiceAdapter{server: s},
product.SessionKey: app,
product.FrontendKey: app,
product.CommandKey: app,
}
// Step 4: Initialize products.
// Depends on s.httpService.
err = s.initializeProducts(product.GetProducts(), serviceMap)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize products")
}
s.services = serviceMap
// After channel is initialized set it to the App object
channelsWrapper, ok := serviceMap[product.ChannelKey].(*channelsWrapper)
if !ok {
return nil, errors.Wrap(err, "channels product is not initialized")
}
app.ch = channelsWrapper.app.ch
// It is important to initialize the hub only after the global logger is set
// to avoid race conditions while logging from inside the hub.
// Step 5: Start hub in platform which the hub depends on s.Channels() (step 4)
s.platform.Start()
// -------------------------------------------------------------------------
// Everything below this is not order sensitive and safe to be moved around.
// If you are adding a new field that is non-channels specific, please add
// below this. Otherwise, please add it to Channels struct in app/channels.go.
// -------------------------------------------------------------------------
if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry {
if strings.Contains(SentryDSN, "placeholder") {
mlog.Warn("Sentry reporting is enabled, but SENTRY_DSN is not set. Disabling reporting.")
} else {
if err2 := sentry.Init(sentry.ClientOptions{
Dsn: SentryDSN,
Release: model.BuildHash,
AttachStacktrace: true,
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
// sanitize data sent to sentry to reduce exposure of PII
if event.Request != nil {
event.Request.Cookies = ""
event.Request.QueryString = ""
event.Request.Headers = nil
event.Request.Data = ""
}
return event
},
EnableTracing: false,
TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 {
return 0.0
}),
}); err2 != nil {
mlog.Warn("Sentry could not be initiated, probably bad DSN?", mlog.Err(err2))
}
}
}
if *s.platform.Config().ServiceSettings.EnableOpenTracing {
tracer, err2 := tracing.New()
if err2 != nil {
return nil, err2
}
s.tracer = tracer
}
s.pushNotificationClient = s.httpService.MakeClient(true)
if err2 := utils.TranslationsPreInit(); err2 != nil {
return nil, errors.Wrapf(err2, "unable to load Mattermost translation files")
}
model.AppErrorInit(i18n.T)
if s.seenPendingPostIdsCache, err = s.platform.CacheProvider().NewCache(&cache.CacheOptions{
Size: PendingPostIDsCacheSize,
}); err != nil {
return nil, errors.Wrap(err, "Unable to create pending post ids cache")
}
if s.openGraphDataCache, err = s.platform.CacheProvider().NewCache(&cache.CacheOptions{
Size: openGraphMetadataCacheSize,
}); err != nil {
return nil, errors.Wrap(err, "Unable to create opengraphdata cache")
}
s.createPushNotificationsHub(request.EmptyContext(s.Log()))
if err2 := i18n.InitTranslations(*s.platform.Config().LocalizationSettings.DefaultServerLocale, *s.platform.Config().LocalizationSettings.DefaultClientLocale); err2 != nil {
return nil, errors.Wrapf(err2, "unable to load Mattermost translation files")
}
templatesDir, ok := templates.GetTemplateDirectory()
if !ok {
return nil, errors.New("Failed find server templates in \"templates\" directory or MM_SERVER_PATH")
}
htmlTemplateWatcher, errorsChan, err2 := templates.NewWithWatcher(templatesDir)
if err2 != nil {
return nil, errors.Wrap(err2, "cannot initialize server templates")
}
s.Go(func() {
for err2 := range errorsChan {
mlog.Warn("Server templates error", mlog.Err(err2))
}
})
s.htmlTemplateWatcher = htmlTemplateWatcher
s.telemetryService, err = telemetry.New(New(ServerConnector(s.Channels())), s.Store(), s.platform.SearchEngine, s.Log(), *s.Config().LogSettings.VerboseDiagnostics)
if err != nil {
return nil, errors.Wrapf(err, "unable to initialize telemetry service")
}
s.platform.SetTelemetryId(s.TelemetryId()) // TODO: move this into platform once telemetry service moved to platform.
emailService, err := email.NewService(email.ServiceConfig{
ConfigFn: s.platform.Config,
LicenseFn: s.License,
TemplatesContainer: s.TemplatesContainer(),
UserService: s.userService,
Store: s.GetStore(),
})
if err != nil {
return nil, errors.Wrapf(err, "unable to initialize email service")
}
s.EmailService = emailService
s.platform.SetupFeatureFlags()
s.initJobs()
s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader()))
if s.Jobs != nil {
s.Jobs.HandleClusterLeaderChange(s.IsLeader())
}
s.platform.SetupFeatureFlags()
})
// If configured with a subpath, redirect 404s at the root back into the subpath.
if subpath != "/" {
s.RootRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = path.Join(subpath, r.URL.Path)
http.Redirect(w, r, r.URL.String(), http.StatusFound)
})
}
if _, err = url.ParseRequestURI(*s.platform.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("SiteURL must be set. Some features will operate incorrectly if the SiteURL is not set. See documentation for details: https://docs.mattermost.com/configure/configuration-settings.html#site-url")
}
// Start email batching because it's not like the other jobs
s.platform.AddConfigListener(func(_, _ *model.Config) {
s.EmailService.InitEmailBatching()
})
logCurrentVersion := fmt.Sprintf("Current version is %v (%v/%v/%v/%v)", model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash, model.BuildHashEnterprise)
mlog.Info(
logCurrentVersion,
mlog.String("current_version", model.CurrentVersion),
mlog.String("build_number", model.BuildNumber),
mlog.String("build_date", model.BuildDate),
mlog.String("build_hash", model.BuildHash),
mlog.String("build_hash_enterprise", model.BuildHashEnterprise),
)
if model.BuildEnterpriseReady == "true" {
mlog.Info("Enterprise Build", mlog.Bool("enterprise_build", true))
} else {
mlog.Info("Team Edition Build", mlog.Bool("enterprise_build", false))
}
pwd, _ := os.Getwd()
mlog.Info("Printing current working", mlog.String("directory", pwd))
mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig()))
license := s.License()
allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging
if s.Audit == nil {
s.Audit = &audit.Audit{}
s.Audit.Init(audit.DefMaxQueueSize)
if err = s.configureAudit(s.Audit, allowAdvancedLogging); err != nil {
mlog.Error("Error configuring audit", mlog.Err(err))
}
}
s.platform.RemoveUnlicensedLogTargets(license)
s.platform.EnableLoggingMetrics()
s.loggerLicenseListenerId = s.AddLicenseListener(func(oldLicense, newLicense *model.License) {
s.platform.RemoveUnlicensedLogTargets(newLicense)
s.platform.EnableLoggingMetrics()
})
// if enabled - perform initial product notices fetch
if *s.platform.Config().AnnouncementSettings.AdminNoticesEnabled || *s.platform.Config().AnnouncementSettings.UserNoticesEnabled {
go func() {
appInstance := New(ServerConnector(s.Channels()))
if err := appInstance.UpdateProductNotices(); err != nil {
mlog.Warn("Failed to perform initial product notices fetch", mlog.Err(err))
}
}()
}
if s.skipPostInit {
return s, nil
}
s.platform.AddConfigListener(func(old, new *model.Config) {
appInstance := New(ServerConnector(s.Channels()))
if *old.GuestAccountsSettings.Enable && !*new.GuestAccountsSettings.Enable {
c := request.EmptyContext(s.Log())
if appErr := appInstance.DeactivateGuests(c); appErr != nil {
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
}
}
})
// Disable active guest accounts on first run if guest accounts are disabled
if !*s.platform.Config().GuestAccountsSettings.Enable {
appInstance := New(ServerConnector(s.Channels()))
c := request.EmptyContext(s.Log())
if appErr := appInstance.DeactivateGuests(c); appErr != nil {
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
}
}
if s.runEssentialJobs {
s.Go(func() {
appInstance := New(ServerConnector(s.Channels()))
s.runLicenseExpirationCheckJob()
runDNDStatusExpireJob(appInstance)
runPostReminderJob(appInstance)
})
s.runJobs()
}
s.doAppMigrations()
s.initPostMetadata()
// Dump the image cache if the proxy settings have changed. (need switch URLs to the correct proxy)
s.platform.AddConfigListener(func(oldCfg, newCfg *model.Config) {
if (oldCfg.ImageProxySettings.Enable != newCfg.ImageProxySettings.Enable) ||
(oldCfg.ImageProxySettings.ImageProxyType != newCfg.ImageProxySettings.ImageProxyType) ||
(oldCfg.ImageProxySettings.RemoteImageProxyURL != newCfg.ImageProxySettings.RemoteImageProxyURL) ||
(oldCfg.ImageProxySettings.RemoteImageProxyOptions != newCfg.ImageProxySettings.RemoteImageProxyOptions) {
s.openGraphDataCache.Purge()
}
})
return s, nil
}
func (s *Server) runJobs() {
s.Go(func() {
runSecurityJob(s)
})
s.Go(func() {
firstRun, err := s.getFirstServerRunTimestamp()
if err != nil {
mlog.Warn("Fetching time of first server run failed. Setting to 'now'.")
s.ensureFirstServerRunTimestamp()
firstRun = utils.MillisFromTime(time.Now())
}
s.telemetryService.RunTelemetryJob(firstRun)
})
s.Go(func() {
runSessionCleanupJob(s)
})
s.Go(func() {
runJobsCleanupJob(s)
})
s.Go(func() {
runTokenCleanupJob(s)
})
s.Go(func() {
runCommandWebhookCleanupJob(s)
})
s.Go(func() {
runConfigCleanupJob(s)
})
if complianceI := s.Channels().Compliance; complianceI != nil {
go complianceI.StartComplianceDailyJob()
}
if *s.platform.Config().JobSettings.RunJobs && s.Jobs != nil {
if err := s.Jobs.StartWorkers(); err != nil {
mlog.Error("Failed to start job server workers", mlog.Err(err))
}
}
if *s.platform.Config().JobSettings.RunScheduler && s.Jobs != nil {
if err := s.Jobs.StartSchedulers(); err != nil {
mlog.Error("Failed to start job server schedulers", mlog.Err(err))
}
}
if *s.platform.Config().ServiceSettings.EnableAWSMetering {
runReportToAWSMeterJob(s)
}
}
// Global app options that should be applied to apps created by this server
func (s *Server) AppOptions() []AppOption {
return []AppOption{
ServerConnector(s.Channels()),
}
}
func (s *Server) Channels() *Channels {
ch, _ := s.products["channels"].(*Channels)
return ch
}
// Return Database type (postgres or mysql) and current version of the schema
func (s *Server) DatabaseTypeAndSchemaVersion() (string, string) {
schemaVersion, _ := s.Store().GetDBSchemaVersion()
return *s.platform.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion)
}
func (s *Server) startInterClusterServices(license *model.License) error {
if license == nil {
mlog.Debug("No license provided; Remote Cluster services disabled")
return nil
}
// Remote Cluster service
// License check (assume enabled if shared channels enabled)
if !license.HasRemoteClusterService() && !license.HasSharedChannels() {
mlog.Debug("License does not have Remote Cluster services enabled")
return nil
}
// Config check
if !*s.platform.Config().ExperimentalSettings.EnableRemoteClusterService && !*s.platform.Config().ExperimentalSettings.EnableSharedChannels {
mlog.Debug("Remote Cluster Service disabled via config")
return nil
}
var err error
rcs, err := remotecluster.NewRemoteClusterService(s)
if err != nil {
return err
}
if err = rcs.Start(); err != nil {
return err
}
s.serviceMux.Lock()
s.remoteClusterService = rcs
s.serviceMux.Unlock()
// Shared Channels service (depends on remote cluster service)
// License check
if !license.HasSharedChannels() {
mlog.Debug("License does not have shared channels enabled")
return nil
}
// Config check
if !*s.platform.Config().ExperimentalSettings.EnableSharedChannels {
mlog.Debug("Shared Channels Service disabled via config")
return nil
}
appInstance := New(ServerConnector(s.Channels()))
scs, err := sharedchannel.NewSharedChannelService(s, appInstance)
if err != nil {
return err
}
s.platform.SetSharedChannelService(scs)
if err = scs.Start(); err != nil {
return err
}
s.serviceMux.Lock()
s.sharedChannelService = scs
s.serviceMux.Unlock()
return nil
}
const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
func (s *Server) StopHTTPServer() {
if s.Server != nil {
ctx, cancel := context.WithTimeout(context.Background(), TimeToWaitForConnectionsToCloseOnServerShutdown)
defer cancel()
didShutdown := false
for s.didFinishListen != nil && !didShutdown {
if err := s.Server.Shutdown(ctx); err != nil {
mlog.Warn("Unable to shutdown server", mlog.Err(err))
}
timer := time.NewTimer(time.Millisecond * 50)
select {
case <-s.didFinishListen:
didShutdown = true
case <-timer.C:
}
timer.Stop()
}
s.Server.Close()
s.Server = nil
}
}
func (s *Server) Shutdown() {
s.Log().Info("Stopping Server...")
defer sentry.Flush(2 * time.Second)
s.RemoveLicenseListener(s.loggerLicenseListenerId)
s.RemoveClusterLeaderChangedListener(s.clusterLeaderListenerId)
if s.tracer != nil {
if err := s.tracer.Close(); err != nil {
s.Log().Warn("Unable to cleanly shutdown opentracing client", mlog.Err(err))
}
}
err := s.telemetryService.Shutdown()
if err != nil {
s.Log().Warn("Unable to cleanly shutdown telemetry client", mlog.Err(err))
}
s.serviceMux.RLock()
if s.sharedChannelService != nil {
if err = s.sharedChannelService.Shutdown(); err != nil {
s.Log().Error("Error shutting down shared channel services", mlog.Err(err))
}
}
if s.remoteClusterService != nil {
if err = s.remoteClusterService.Shutdown(); err != nil {
s.Log().Error("Error shutting down intercluster services", mlog.Err(err))
}
}
s.serviceMux.RUnlock()
s.StopHTTPServer()
s.stopLocalModeServer()
// Push notification hub needs to be shutdown after HTTP server
// to prevent stray requests from generating a push notification after it's shut down.
s.StopPushNotificationsHubWorkers()
s.htmlTemplateWatcher.Close()
s.platform.StopSearchEngine()
s.Audit.Shutdown()
s.platform.StopFeatureFlagUpdateJob()
if err = s.platform.ShutdownConfig(); err != nil {
s.Log().Warn("Failed to shut down config store", mlog.Err(err))
}
if s.platform.Cluster() != nil {
s.platform.Cluster().StopInterNodeCommunication()
}
if err = s.platform.ShutdownMetrics(); err != nil {
s.Log().Warn("Failed to stop metrics server", mlog.Err(err))
}
// Stopping email service after HTTP server has stopped to prevent
// any stray notifications from being queued.
s.EmailService.Stop()
// This must be done after the cluster is stopped.
if s.Jobs != nil {
// For simplicity we don't check if workers and schedulers are active
// before stopping them as both calls essentially become no-ops
// if nothing is running.
if err = s.Jobs.StopWorkers(); err != nil && !errors.Is(err, jobs.ErrWorkersNotRunning) {
s.Log().Warn("Failed to stop job server workers", mlog.Err(err))
}
if err = s.Jobs.StopSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersNotRunning) {
s.Log().Warn("Failed to stop job server schedulers", mlog.Err(err))
}
}
// Stop products.
// This needs to happen last because products are dependent
// on parent services.
for name, product := range s.products {
if err2 := product.Stop(); err2 != nil {
s.Log().Warn("Unable to cleanly stop product", mlog.String("name", name), mlog.Err(err2))
}
}
if err = s.platform.Shutdown(); err != nil {
s.Log().Warn("Failed to stop platform", mlog.Err(err))
}
s.Log().Info("Server stopped")
// shutdown main and notification loggers which will flush any remaining log records.
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), time.Second*15)
defer timeoutCancel()
if err = s.NotificationsLog().ShutdownWithTimeout(timeoutCtx); err != nil {
fmt.Fprintf(os.Stderr, "Error shutting down notification logger: %v", err)
}
if err = s.Log().ShutdownWithTimeout(timeoutCtx); err != nil {
fmt.Fprintf(os.Stderr, "Error shutting down main logger: %v", err)
}
}
func (s *Server) Restart() error {
percentage, err := s.UpgradeToE0Status()
if err != nil || percentage != 100 {
return errors.Wrap(err, "unable to restart because the system has not been upgraded")
}
s.Shutdown()
argv0, err := exec.LookPath(os.Args[0])
if err != nil {
return err
}
if _, err = os.Stat(argv0); err != nil {
return err
}
mlog.Info("Restarting server")
return syscall.Exec(argv0, os.Args, os.Environ())
}
func (s *Server) CanIUpgradeToE0() error {
return upgrader.CanIUpgradeToE0()
}
func (s *Server) UpgradeToE0() error {
if err := upgrader.UpgradeToE0(); err != nil {
return err
}
upgradedFromTE := &model.System{Name: model.SystemUpgradedFromTeId, Value: "true"}
s.Store().System().Save(upgradedFromTE)
return nil
}
func (s *Server) UpgradeToE0Status() (int64, error) {
return upgrader.UpgradeToE0Status()
}
// Go creates a goroutine, but maintains a record of it to ensure that execution completes before
// the server is shutdown.
func (s *Server) Go(f func()) {
s.platform.Go(f)
}
// GoBuffered acts like a semaphore which creates a goroutine, but maintains a record of it
// to ensure that execution completes before the server is shutdown.
func (s *Server) GoBuffered(f func()) {
s.platform.GoBuffered(f)
}
var corsAllowedMethods = []string{
"POST",
"GET",
"OPTIONS",
"PUT",
"PATCH",
"DELETE",
}
// golang.org/x/crypto/acme/autocert/autocert.go
func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "Use HTTPS", http.StatusBadRequest)
return
}
target := "https://" + stripPort(r.Host) + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusFound)
}
// golang.org/x/crypto/acme/autocert/autocert.go
func stripPort(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport
}
return net.JoinHostPort(host, "443")
}
func (s *Server) Start() error {
// Start products.
// This needs to happen before because products are dependent on the HTTP server.
// make sure channels starts first
if err := s.products["channels"].Start(); err != nil {
return errors.Wrap(err, "Unable to start channels")
}
for name, product := range s.products {
if name == "channels" {
continue
}
if err := product.Start(); err != nil {
return errors.Wrapf(err, "Unable to start %s", name)
}
}
if s.joinCluster && s.platform.Cluster() != nil {
s.registerClusterHandlers()
s.platform.Cluster().StartInterNodeCommunication()
}
if err := s.ensureInstallationDate(); err != nil {
return errors.Wrapf(err, "unable to ensure installation date")
}
if err := s.ensureFirstServerRunTimestamp(); err != nil {
return errors.Wrapf(err, "unable to ensure first run timestamp")
}
if err := s.Store().Status().ResetAll(); err != nil {
mlog.Error("Error to reset the server status.", mlog.Err(err))
}
if s.MailServiceConfig().SendEmailNotifications {
if err := mail.TestConnection(s.MailServiceConfig()); err != nil {
mlog.Error("Mail server connection test failed", mlog.Err(err))
}
}
err := s.FileBackend().TestConnection()
if err != nil {
if _, ok := err.(*filestore.S3FileBackendNoBucketError); ok {
err = s.FileBackend().(*filestore.S3FileBackend).MakeBucket()
}
if err != nil {
mlog.Error("Problem with file storage settings", mlog.Err(err))
}
}
s.checkPushNotificationServerURL()
s.platform.ReloadConfig()
mlog.Info("Starting Server...")
var handler http.Handler = s.RootRouter
if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry && !strings.Contains(SentryDSN, "placeholder") {
sentryHandler := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
handler = sentryHandler.Handle(handler)
}
if allowedOrigins := *s.platform.Config().ServiceSettings.AllowCorsFrom; allowedOrigins != "" {
exposedCorsHeaders := *s.platform.Config().ServiceSettings.CorsExposedHeaders
allowCredentials := *s.platform.Config().ServiceSettings.CorsAllowCredentials
debug := *s.platform.Config().ServiceSettings.CorsDebug
corsWrapper := cors.New(cors.Options{
AllowedOrigins: strings.Fields(allowedOrigins),
AllowedMethods: corsAllowedMethods,
AllowedHeaders: []string{"*"},
ExposedHeaders: strings.Fields(exposedCorsHeaders),
MaxAge: 86400,
AllowCredentials: allowCredentials,
Debug: debug,
})
// If we have debugging of CORS turned on then forward messages to logs
if debug {
corsWrapper.Log = s.Log().With(mlog.String("source", "cors")).StdLogger(mlog.LvlDebug)
}
handler = corsWrapper.Handler(handler)
}
if *s.platform.Config().RateLimitSettings.Enable {
mlog.Info("RateLimiter is enabled")
rateLimiter, err2 := NewRateLimiter(&s.platform.Config().RateLimitSettings, s.platform.Config().ServiceSettings.TrustedProxyIPHeader)
if err2 != nil {
return err2
}
s.RateLimiter = rateLimiter
handler = rateLimiter.RateLimitHandler(handler)
}
// Creating a logger for logging errors from http.Server at error level
errStdLog := s.Log().With(mlog.String("source", "httpserver")).StdLogger(mlog.LvlError)
s.Server = &http.Server{
Handler: handler,
ReadTimeout: time.Duration(*s.platform.Config().ServiceSettings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(*s.platform.Config().ServiceSettings.WriteTimeout) * time.Second,
IdleTimeout: time.Duration(*s.platform.Config().ServiceSettings.IdleTimeout) * time.Second,
ErrorLog: errStdLog,
}
addr := *s.platform.Config().ServiceSettings.ListenAddress
if addr == "" {
if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS {
addr = ":https"
} else {
addr = ":http"
}
}
listener, err := net.Listen("tcp", addr)
if err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
s.ListenAddr = listener.Addr().(*net.TCPAddr)
logListeningPort := fmt.Sprintf("Server is listening on %v", listener.Addr().String())
mlog.Info(logListeningPort, mlog.String("address", listener.Addr().String()))
m := &autocert.Manager{
Cache: autocert.DirCache(*s.platform.Config().ServiceSettings.LetsEncryptCertificateCacheFile),
Prompt: autocert.AcceptTOS,
}
if *s.platform.Config().ServiceSettings.Forward80To443 {
if host, port, err := net.SplitHostPort(addr); err != nil {
mlog.Error("Unable to setup forwarding", mlog.Err(err))
} else if port != "443" {
return fmt.Errorf(i18n.T("api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port"), port)
} else {
httpListenAddress := net.JoinHostPort(host, "http")
if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
server := &http.Server{
Addr: httpListenAddress,
Handler: m.HTTPHandler(nil),
ErrorLog: s.Log().With(mlog.String("source", "le_forwarder_server")).StdLogger(mlog.LvlError),
}
go server.ListenAndServe()
} else {
go func() {
redirectListener, err := net.Listen("tcp", httpListenAddress)
if err != nil {
mlog.Error("Unable to setup forwarding", mlog.Err(err))
return
}
defer redirectListener.Close()
server := &http.Server{
Handler: http.HandlerFunc(handleHTTPRedirect),
ErrorLog: s.Log().With(mlog.String("source", "forwarder_server")).StdLogger(mlog.LvlError),
}
server.Serve(redirectListener)
}()
}
}
} else if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
return errors.New(i18n.T("api.server.start_server.forward80to443.disabled_while_using_lets_encrypt"))
}
s.didFinishListen = make(chan struct{})
go func() {
var err error
if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS {
tlsConfig := &tls.Config{
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
}
switch *s.platform.Config().ServiceSettings.TLSMinVer {
case "1.0":
tlsConfig.MinVersion = tls.VersionTLS10
case "1.1":
tlsConfig.MinVersion = tls.VersionTLS11
default:
tlsConfig.MinVersion = tls.VersionTLS12
}
defaultCiphers := []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
}
if len(s.platform.Config().ServiceSettings.TLSOverwriteCiphers) == 0 {
tlsConfig.CipherSuites = defaultCiphers
} else {
var cipherSuites []uint16
for _, cipher := range s.platform.Config().ServiceSettings.TLSOverwriteCiphers {
value, ok := model.ServerTLSSupportedCiphers[cipher]
if !ok {
mlog.Warn("Unsupported cipher passed", mlog.String("cipher", cipher))
continue
}
cipherSuites = append(cipherSuites, value)
}
if len(cipherSuites) == 0 {
mlog.Warn("No supported ciphers passed, fallback to default cipher suite")
cipherSuites = defaultCiphers
}
tlsConfig.CipherSuites = cipherSuites
}
certFile := ""
keyFile := ""
if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
tlsConfig.GetCertificate = m.GetCertificate
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
} else {
certFile = *s.platform.Config().ServiceSettings.TLSCertFile
keyFile = *s.platform.Config().ServiceSettings.TLSKeyFile
}
s.Server.TLSConfig = tlsConfig
err = s.Server.ServeTLS(listener, certFile, keyFile)
} else {
err = s.Server.Serve(listener)
}
if err != nil && err != http.ErrServerClosed {
mlog.Fatal("Error starting server", mlog.Err(err))
time.Sleep(time.Second)
}
close(s.didFinishListen)
}()
if *s.platform.Config().ServiceSettings.EnableLocalMode {
if err := s.startLocalModeServer(); err != nil {
mlog.Fatal(err.Error())
}
}
if err := s.startInterClusterServices(s.License()); err != nil {
mlog.Error("Error starting inter-cluster services", mlog.Err(err))
}
return nil
}
func (s *Server) startLocalModeServer() error {
s.localModeServer = &http.Server{
Handler: s.LocalRouter,
}
socket := *s.platform.Config().ServiceSettings.LocalModeSocketLocation
if err := os.RemoveAll(socket); err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
unixListener, err := net.Listen("unix", socket)
if err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
if err = os.Chmod(socket, 0600); err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
go func() {
err = s.localModeServer.Serve(unixListener)
if err != nil && err != http.ErrServerClosed {
mlog.Fatal("Error starting unix socket server", mlog.Err(err))
}
}()
return nil
}
func (s *Server) stopLocalModeServer() {
if s.localModeServer != nil {
s.localModeServer.Close()
}
}
func (a *App) OriginChecker() func(*http.Request) bool {
if allowed := *a.Config().ServiceSettings.AllowCorsFrom; allowed != "" {
if allowed != "*" {
siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL)
if err == nil {
siteURL.Path = ""
allowed += " " + siteURL.String()
}
}
return utils.OriginChecker(allowed)
}
return nil
}
func (s *Server) checkPushNotificationServerURL() {
notificationServer := *s.platform.Config().EmailSettings.PushNotificationServer
if strings.HasPrefix(notificationServer, "http://") {
mlog.Warn("Your push notification server is configured with HTTP. For improved security, update to HTTPS in your configuration.")
}
}
func runSecurityJob(s *Server) {
doSecurity(s)
model.CreateRecurringTask("Security", func() {
doSecurity(s)
}, time.Hour*4)
}
func runTokenCleanupJob(s *Server) {
doTokenCleanup(s)
model.CreateRecurringTask("Token Cleanup", func() {
doTokenCleanup(s)
}, time.Hour*1)
}
func runCommandWebhookCleanupJob(s *Server) {
doCommandWebhookCleanup(s)
model.CreateRecurringTask("Command Hook Cleanup", func() {
doCommandWebhookCleanup(s)
}, time.Hour*1)
}
func runSessionCleanupJob(s *Server) {
doSessionCleanup(s)
model.CreateRecurringTask("Session Cleanup", func() {
doSessionCleanup(s)
}, time.Hour*24)
}
func runJobsCleanupJob(s *Server) {
doJobsCleanup(s)
model.CreateRecurringTask("Job Cleanup", func() {
doJobsCleanup(s)
}, time.Hour*24)
}
func runConfigCleanupJob(s *Server) {
doConfigCleanup(s)
model.CreateRecurringTask("Configuration Cleanup", func() {
doConfigCleanup(s)
}, time.Hour*24)
}
func (s *Server) runLicenseExpirationCheckJob() {
s.doLicenseExpirationCheck()
model.CreateRecurringTask("License Expiration Check", func() {
s.doLicenseExpirationCheck()
}, time.Hour*24)
}
func runReportToAWSMeterJob(s *Server) {
model.CreateRecurringTask("Collect and send usage report to AWS Metering Service", func() {
doReportUsageToAWSMeteringService(s)
}, time.Hour*model.AwsMeteringReportInterval)
}
func doReportUsageToAWSMeteringService(s *Server) {
awsMeter := awsmeter.New(s.Store(), s.platform.Config())
if awsMeter == nil {
mlog.Error("Cannot obtain instance of AWS Metering Service.")
return
}
dimensions := []string{model.AwsMeteringDimensionUsageHrs}
reports := awsMeter.GetUserCategoryUsage(dimensions, time.Now().UTC(), time.Now().Add(-model.AwsMeteringReportInterval*time.Hour).UTC())
awsMeter.ReportUserCategoryUsage(reports)
}
func doSecurity(s *Server) {
s.DoSecurityUpdateCheck()
}
func doTokenCleanup(s *Server) {
expiry := model.GetMillis() - model.MaxTokenExipryTime
mlog.Debug("Cleaning up token store.")
s.Store().Token().Cleanup(expiry)
}
func doCommandWebhookCleanup(s *Server) {
s.Store().CommandWebhook().Cleanup()
}
const (
sessionsCleanupBatchSize = 1000
jobsCleanupBatchSize = 1000
)
func doSessionCleanup(s *Server) {
mlog.Debug("Cleaning up session store.")
err := s.Store().Session().Cleanup(model.GetMillis(), sessionsCleanupBatchSize)
if err != nil {
mlog.Warn("Error while cleaning up sessions", mlog.Err(err))
}
}
func doJobsCleanup(s *Server) {
if *s.platform.Config().JobSettings.CleanupJobsThresholdDays < 0 {
return
}
mlog.Debug("Cleaning up jobs store.")
dur := time.Duration(*s.platform.Config().JobSettings.CleanupJobsThresholdDays) * time.Hour * 24
expiry := model.GetMillisForTime(time.Now().Add(-dur))
err := s.Store().Job().Cleanup(expiry, jobsCleanupBatchSize)
if err != nil {
mlog.Warn("Error while cleaning up jobs", mlog.Err(err))
}
}
func doConfigCleanup(s *Server) {
if *s.platform.Config().JobSettings.CleanupConfigThresholdDays < 0 || !config.IsDatabaseDSN(s.platform.DescribeConfig()) {
return
}
mlog.Info("Cleaning up configuration store.")
if err := s.platform.CleanUpConfig(); err != nil {
mlog.Warn("Error while cleaning up configurations", mlog.Err(err))
}
}
func (s *Server) HandleMetrics(route string, h http.Handler) {
s.platform.HandleMetrics(route, h)
}
func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, license *model.License) *model.AppError {
key := model.LicenseUpForRenewalEmailSent + license.Id
if _, err := s.Store().System().GetByName(key); err == nil {
// return early because the key already exists and that means we already executed the code below to send email successfully
return nil
}
daysToExpiration := license.DaysToExpiration()
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// we want to at least have one email sent out to an admin
countNotOks := 0
for _, user := range users {
name := user.FirstName
if name == "" {
name = user.Username
}
T := i18n.GetUserTranslations(user.Locale)
ctaTitle := T("api.templates.license_up_for_renewal_subtitle_two")
ctaText := T("api.templates.license_up_for_renewal_renew_now")
if !status.IsRenewable {
ctaTitle = ""
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, ctaTitle, ctaLink, ctaText, daysToExpiration); err != nil {
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
countNotOks++
}
}
// if not even one admin got an email, we consider that this operation errored
if countNotOks == len(users) {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_sending_email", nil, "", http.StatusInternalServerError)
}
system := model.System{
Name: key,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Debug("Failed to mark license up for renewal email sending as completed.", mlog.Err(err))
}
return nil
}
func (s *Server) doLicenseExpirationCheck() {
s.LoadLicense()
// This takes care of a rare edge case reported here https://mattermost.atlassian.net/browse/MM-40962
// To reproduce that case locally, attach a license to a server that was started with enterprise enabled
// Then restart using BUILD_ENTERPRISE=false make restart-server to enter Team Edition
if model.BuildEnterpriseReady != "true" {
mlog.Debug("Skipping license expiration check because no license is expected on Team Edition")
return
}
license := s.License()
if license == nil {
mlog.Debug("License cannot be found.")
return
}
if license.IsCloud() {
mlog.Debug("Skipping license expiration check for Cloud")
return
}
users, err := s.Store().User().GetSystemAdminProfiles()
if err != nil {
mlog.Error("Failed to get system admins for license expired message from Mattermost.")
return
}
if license.IsWithinExpirationPeriod() {
appErr := s.sendLicenseUpForRenewalEmail(users, license)
if appErr != nil {
mlog.Debug(appErr.Error())
}
return
}
if !license.IsPastGracePeriod() {
mlog.Debug("License is not past the grace period.")
return
}
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr).Error())
return
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err).Error())
return
}
//send email to admin(s)
for _, user := range users {
user := user
if user.Email == "" {
mlog.Error("Invalid system admin email.", mlog.String("user_email", user.Email))
continue
}
T := i18n.GetUserTranslations(user.Locale)
ctaText := T("api.templates.remove_expired_license.body.renew_button")
if !status.IsRenewable {
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
s.Go(func() {
if err := s.SendRemoveExpiredLicenseEmail(user.Email, ctaText, ctaLink, user.Locale, *s.platform.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err))
}
})
}
//remove the license
s.RemoveLicense()
}
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (s *Server) SendRemoveExpiredLicenseEmail(email, ctaText, ctaLink, locale, siteURL string) *model.AppError {
if err := s.EmailService.SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL); err != nil {
return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (s *Server) FileBackend() filestore.FileBackend {
return s.platform.FileBackend()
}
func (s *Server) TotalWebsocketConnections() int {
return s.Platform().TotalWebsocketConnections()
}
func (s *Server) ClusterHealthScore() int {
return s.platform.Cluster().HealthScore()
}
func (ch *Channels) ClientConfigHash() string {
return ch.srv.Platform().ClientConfigHash()
}
func (s *Server) initJobs() {
s.Jobs = jobs.NewJobServer(s.platform, s.Store(), s.GetMetrics())
if jobsDataRetentionJobInterface != nil {
builder := jobsDataRetentionJobInterface(s)
s.Jobs.RegisterJobType(model.JobTypeDataRetention, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsMessageExportJobInterface != nil {
builder := jobsMessageExportJobInterface(s)
s.Jobs.RegisterJobType(model.JobTypeMessageExport, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsElasticsearchAggregatorInterface != nil {
builder := jobsElasticsearchAggregatorInterface(s)
s.Jobs.RegisterJobType(model.JobTypeElasticsearchPostAggregation, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsElasticsearchIndexerInterface != nil {
builder := jobsElasticsearchIndexerInterface(s)
s.Jobs.RegisterJobType(model.JobTypeElasticsearchPostIndexing, builder.MakeWorker(), nil)
}
if jobsLdapSyncInterface != nil {
builder := jobsLdapSyncInterface(New(ServerConnector(s.Channels())))
s.Jobs.RegisterJobType(model.JobTypeLdapSync, builder.MakeWorker(), builder.MakeScheduler())
}
s.Jobs.RegisterJobType(
model.JobTypeBlevePostIndexing,
indexer.MakeWorker(s.Jobs, s.platform.SearchEngine.BleveEngine.(*bleveengine.BleveEngine)),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeMigrations,
migrations.MakeWorker(s.Jobs, s.Store()),
migrations.MakeScheduler(s.Jobs, s.Store()),
)
s.Jobs.RegisterJobType(
model.JobTypePlugins,
scheduler.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
scheduler.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExpiryNotify,
expirynotify.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())).NotifySessionsExpired),
expirynotify.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeProductNotices,
product_notices.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
product_notices.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeImportProcess,
import_process.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeImportDelete,
import_delete.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store()),
import_delete.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExportDelete,
export_delete.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
export_delete.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExportProcess,
export_process.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeActiveUsers,
active_users.MakeWorker(s.Jobs, s.Store(), func() einterfaces.MetricsInterface { return s.GetMetrics() }),
active_users.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeResendInvitationEmail,
resend_invitation_email.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store(), s.telemetryService),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeExtractContent,
extract_content.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store()),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeLastAccessiblePost,
last_accessible_post.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
last_accessible_post.MakeScheduler(s.Jobs, s.License()),
)
s.Jobs.RegisterJobType(
model.JobTypeLastAccessibleFile,
last_accessible_file.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
last_accessible_file.MakeScheduler(s.Jobs, s.License()),
)
s.Jobs.RegisterJobType(
model.JobTypeUpgradeNotifyAdmin,
notify_admin.MakeUpgradeNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeUpgradeNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeTrialNotifyAdmin,
notify_admin.MakeTrialNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeInstallPluginNotifyAdmin,
notify_admin.MakeInstallPluginNotifyWorker(s.Jobs, New(ServerConnector(s.Channels()))),
notify_admin.MakeInstallPluginScheduler(s.Jobs, s.License(), model.JobTypeInstallPluginNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeHostedPurchaseScreening,
hosted_purchase_screening.MakeWorker(s.Jobs, s.License(), s.Store().System()),
hosted_purchase_screening.MakeScheduler(s.Jobs, s.License()),
)
s.platform.Jobs = s.Jobs
}
func (s *Server) TelemetryId() string {
if s.telemetryService == nil {
return ""
}
return s.telemetryService.TelemetryID
}
func (s *Server) HTTPService() httpservice.HTTPService {
return s.httpService
}
// GetStore returns the server's Store. Exposing via a method
// allows interfaces to be created with subsets of server APIs.
func (s *Server) GetStore() store.Store {
return s.Store()
}
// GetRemoteClusterService returns the `RemoteClusterService` instantiated by the server.
// May be nil if the service is not enabled via license.
func (s *Server) GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace {
s.serviceMux.RLock()
defer s.serviceMux.RUnlock()
return s.remoteClusterService
}
// GetSharedChannelSyncService returns the `SharedChannelSyncService` instantiated by the server.
// May be nil if the service is not enabled via license.
func (s *Server) GetSharedChannelSyncService() SharedChannelServiceIFace {
s.serviceMux.RLock()
defer s.serviceMux.RUnlock()
return s.sharedChannelService
}
// GetMetrics returns the server's Metrics interface. Exposing via a method
// allows interfaces to be created with subsets of server APIs.
func (s *Server) GetMetrics() einterfaces.MetricsInterface {
if s.platform == nil {
return nil
}
return s.platform.Metrics()
}
// SetRemoteClusterService sets the `RemoteClusterService` to be used by the server.
// For testing only.
func (s *Server) SetRemoteClusterService(remoteClusterService remotecluster.RemoteClusterServiceIFace) {
s.serviceMux.Lock()
defer s.serviceMux.Unlock()
s.remoteClusterService = remoteClusterService
}
// SetSharedChannelSyncService sets the `SharedChannelSyncService` to be used by the server.
// For testing only.
func (s *Server) SetSharedChannelSyncService(sharedChannelService SharedChannelServiceIFace) {
s.serviceMux.Lock()
defer s.serviceMux.Unlock()
s.sharedChannelService = sharedChannelService
s.platform.SetSharedChannelService(sharedChannelService)
}
func (s *Server) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
if *s.platform.Config().FileSettings.DriverName == "" {
img, appErr := s.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
return img, false, nil
}
path := "users/" + user.Id + "/profile.png"
data, err := s.ReadFile(path)
if err != nil {
img, appErr := s.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := s.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (s *Server) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
img, err := s.userService.GetDefaultProfileImage(user)
if err != nil {
switch {
case errors.Is(err, users.DefaultFontError):
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.default_font.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, users.UserInitialsError):
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.initial.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return img, nil
}
func (s *Server) ReadFile(path string) ([]byte, *model.AppError) {
result, nErr := s.FileBackend().ReadFile(path)
if nErr != nil {
return nil, model.NewAppError("ReadFile", "api.file.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func withMut(mut *sync.Mutex, f func()) {
mut.Lock()
defer mut.Unlock()
f()
}
func cancelTask(mut *sync.Mutex, taskPointer **model.ScheduledTask) {
mut.Lock()
defer mut.Unlock()
if *taskPointer != nil {
(*taskPointer).Cancel()
*taskPointer = nil
}
}
func runDNDStatusExpireJob(a *App) {
if a.IsLeader() {
withMut(&a.ch.dndTaskMut, func() {
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
})
}
a.ch.srv.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader()))
if a.IsLeader() {
withMut(&a.ch.dndTaskMut, func() {
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
})
} else {
cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask)
}
})
}
func runPostReminderJob(a *App) {
if a.IsLeader() {
withMut(&a.ch.postReminderMut, func() {
a.ch.postReminderTask = model.CreateRecurringTaskFromNextIntervalTime("Check Post reminders", a.CheckPostReminders, 5*time.Minute)
})
}
a.ch.srv.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if post reminder task should be running", mlog.Bool("isLeader", a.IsLeader()))
if a.IsLeader() {
withMut(&a.ch.postReminderMut, func() {
a.ch.postReminderTask = model.CreateRecurringTaskFromNextIntervalTime("Check Post reminders", a.CheckPostReminders, 5*time.Minute)
})
} else {
cancelTask(&a.ch.postReminderMut, &a.ch.postReminderTask)
}
})
}
func (a *App) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.AppError) {
table, err := a.Srv().Store().GetAppliedMigrations()
if err != nil {
return nil, model.NewAppError("GetDBSchemaTable", "api.file.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return table, nil
}
// Expose platform service from server, this should be replaced with server itself in time.
func (s *Server) Platform() *platform.PlatformService {
return s.platform
}
func (s *Server) Log() *mlog.Logger {
return s.platform.Logger()
}
func (s *Server) NotificationsLog() *mlog.Logger {
return s.platform.NotificationsLogger()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"math"
"net/http"
"os"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
session, err := a.ch.srv.platform.CreateSession(session)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateSession", "app.session.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateSession", "app.session.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return session, nil
}
func (a *App) GetCloudSession(token string) (*model.Session, *model.AppError) {
apiKey := os.Getenv("MM_CLOUD_API_KEY")
if apiKey != "" && apiKey == token {
// Need a bare-bones session object for later checks
session := &model.Session{
Token: token,
IsOAuth: false,
}
session.AddProp(model.SessionPropType, model.SessionTypeCloudKey)
return session, nil
}
return nil, model.NewAppError("GetCloudSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
}
func (a *App) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
rc, appErr := a.GetRemoteCluster(remoteId)
if appErr == nil && rc.Token == token {
// Need a bare-bones session object for later checks
session := &model.Session{
Token: token,
IsOAuth: false,
}
session.AddProp(model.SessionPropType, model.SessionTypeRemoteclusterToken)
return session, nil
}
return nil, model.NewAppError("GetRemoteClusterSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
}
func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
var session *model.Session
// We intentionally skip the error check here, we only want to check if the token is valid.
// If we don't have the session we are going to create one with the token eventually.
if session, _ = a.ch.srv.platform.GetSession(token); session != nil {
if session.Token != token {
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "session token is different from the one in DB", http.StatusUnauthorized)
}
if !session.IsExpired() {
a.ch.srv.platform.AddSessionToCache(session)
}
}
var appErr *model.AppError
if session == nil || session.Id == "" {
session, appErr = a.createSessionForUserAccessToken(token)
if appErr != nil {
detailedError := ""
statusCode := http.StatusUnauthorized
if appErr.Id != "app.user_access_token.invalid_or_missing" {
detailedError = appErr.Error()
statusCode = appErr.StatusCode
} else {
mlog.Warn("Error while creating session for user access token", mlog.Err(appErr))
}
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": detailedError}, "", statusCode)
}
}
if session.Id == "" || session.IsExpired() {
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "session is either nil or expired", http.StatusUnauthorized)
}
if *a.Config().ServiceSettings.SessionIdleTimeoutInMinutes > 0 &&
!session.IsOAuth && !session.IsMobileApp() &&
session.Props[model.SessionPropType] != model.SessionTypeUserAccessToken &&
!*a.Config().ServiceSettings.ExtendSessionLengthWithActivity {
timeout := int64(*a.Config().ServiceSettings.SessionIdleTimeoutInMinutes) * 1000 * 60
if (model.GetMillis() - session.LastActivityAt) > timeout {
// Revoking the session is an asynchronous task anyways since we are not checking
// for the return value of the call before returning the error.
// So moving this to a goroutine has 2 advantages:
// 1. We are treating this as a proper asynchronous task.
// 2. This also fixes a race condition in the web hub, where GetSession
// gets called from (*WebConn).isMemberOfTeam and revoking a session involves
// clearing the webconn cache, which needs the hub again.
a.Srv().Go(func() {
err := a.RevokeSessionById(session.Id)
if err != nil {
mlog.Warn("Error while revoking session", mlog.Err(err))
}
})
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "idle timeout", http.StatusUnauthorized)
}
}
return session, nil
}
func (a *App) GetSessions(userID string) ([]*model.Session, *model.AppError) {
sessions, err := a.ch.srv.platform.GetSessions(userID)
if err != nil {
return nil, model.NewAppError("GetSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return sessions, nil
}
func (a *App) RevokeAllSessions(userID string) *model.AppError {
if err := a.ch.srv.platform.RevokeAllSessions(userID); err != nil {
switch {
case errors.Is(err, platform.GetSessionError):
return model.NewAppError("RevokeAllSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) AddSessionToCache(session *model.Session) {
a.ch.srv.platform.AddSessionToCache(session)
}
// RevokeSessionsFromAllUsers will go through all the sessions active
// in the server and revoke them
func (a *App) RevokeSessionsFromAllUsers() *model.AppError {
if err := a.ch.srv.platform.RevokeSessionsFromAllUsers(); err != nil {
switch {
case errors.Is(err, users.DeleteAllAccessDataError):
return model.NewAppError("RevokeSessionsFromAllUsers", "app.oauth.remove_access_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeSessionsFromAllUsers", "app.session.remove_all_sessions_for_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) ReturnSessionToPool(session *model.Session) {
a.ch.srv.platform.ReturnSessionToPool(session)
}
func (a *App) ClearSessionCacheForUser(userID string) {
a.ch.srv.platform.ClearUserSessionCache(userID)
}
func (a *App) ClearSessionCacheForAllUsers() {
a.ch.srv.platform.ClearAllUsersSessionCache()
}
func (a *App) ClearSessionCacheForUserSkipClusterSend(userID string) {
a.Srv().Platform().ClearSessionCacheForUserSkipClusterSend(userID)
}
func (a *App) ClearSessionCacheForAllUsersSkipClusterSend() {
a.Srv().Platform().ClearSessionCacheForAllUsersSkipClusterSend()
}
func (a *App) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) *model.AppError {
if err := a.ch.srv.platform.RevokeSessionsForDeviceId(userID, deviceID, currentSessionId); err != nil {
return model.NewAppError("RevokeSessionsForDeviceId", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetSessionById(sessionID string) (*model.Session, *model.AppError) {
session, err := a.ch.srv.platform.GetSessionByID(sessionID)
if err != nil {
return nil, model.NewAppError("GetSessionById", "app.session.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return session, nil
}
func (a *App) RevokeSessionById(sessionID string) *model.AppError {
session, err := a.GetSessionById(sessionID)
if err != nil {
return model.NewAppError("RevokeSessionById", "app.session.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return a.RevokeSession(session)
}
func (a *App) RevokeSession(session *model.Session) *model.AppError {
if err := a.ch.srv.platform.RevokeSession(session); err != nil {
switch {
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) AttachDeviceId(sessionID string, deviceID string, expiresAt int64) *model.AppError {
_, err := a.Srv().Store().Session().UpdateDeviceId(sessionID, deviceID, expiresAt)
if err != nil {
return model.NewAppError("AttachDeviceId", "app.session.update_device_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config.
// A new ExpiresAt is only written if enough time has elapsed since last update.
// Returns true only if the session was extended.
func (a *App) ExtendSessionExpiryIfNeeded(session *model.Session) bool {
if !*a.Config().ServiceSettings.ExtendSessionLengthWithActivity {
return false
}
if session == nil || session.IsExpired() {
return false
}
sessionLength := a.GetSessionLengthInMillis(session)
// Only extend the expiry if the lessor of 1% or 1 day has elapsed within the
// current session duration.
threshold := int64(math.Min(float64(sessionLength)*0.01, float64(model.DayInMilliseconds)))
// Minimum session length is 1 day as of this writing, therefore a minimum ~14 minutes threshold.
// However we'll add a sanity check here in case that changes. Minimum 5 minute threshold,
// meaning we won't write a new expiry more than every 5 minutes.
if threshold < 5*60*1000 {
threshold = 5 * 60 * 1000
}
now := model.GetMillis()
elapsed := now - (session.ExpiresAt - sessionLength)
if elapsed < threshold {
return false
}
auditRec := a.MakeAuditRecord("extendSessionExpiry", audit.Fail)
defer a.LogAuditRec(auditRec, nil)
auditRec.AddEventPriorState(session)
newExpiry := now + sessionLength
if err := a.ch.srv.platform.ExtendSessionExpiry(session, newExpiry); err != nil {
mlog.Error("Failed to update ExpiresAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
auditRec.AddMeta("err", err.Error())
return false
}
mlog.Debug("Session extended", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id),
mlog.Int64("newExpiry", newExpiry), mlog.Int64("session_length", sessionLength))
auditRec.Success()
auditRec.AddEventResultState(session)
return true
}
// GetSessionLengthInMillis returns the session length, in milliseconds,
// based on the type of session (Mobile, SSO, Web/LDAP).
func (a *App) GetSessionLengthInMillis(session *model.Session) int64 {
if session == nil {
return 0
}
var hours int
if session.IsMobileApp() {
hours = *a.Config().ServiceSettings.SessionLengthMobileInHours
} else if session.IsSSOLogin() {
hours = *a.Config().ServiceSettings.SessionLengthSSOInHours
} else {
hours = *a.Config().ServiceSettings.SessionLengthWebInHours
}
return int64(hours * 60 * 60 * 1000)
}
// SetSessionExpireInHours sets the session's expiry the specified number of hours
// relative to either the session creation date or the current time, depending
// on the `ExtendSessionOnActivity` config setting.
func (a *App) SetSessionExpireInHours(session *model.Session, hours int) {
a.ch.srv.platform.SetSessionExpireInHours(session, hours)
}
func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
user, nErr := a.ch.srv.userService.GetUser(token.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserAccessToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserAccessToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !*a.Config().ServiceSettings.EnableUserAccessTokens && !user.IsBot {
return nil, model.NewAppError("CreateUserAccessToken", "app.user_access_token.disabled", nil, "", http.StatusNotImplemented)
}
token.Token = model.NewId()
token, nErr = a.Srv().Store().UserAccessToken().Save(token)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateUserAccessToken", "app.user_access_token.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Don't send emails to bot users.
if !user.IsBot {
if err := a.Srv().EmailService.SendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
a.Log().Error("Unable to send user access token added email", mlog.Err(err), mlog.String("user_id", user.Id))
}
}
return token, nil
}
func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Session, *model.AppError) {
token, nErr := a.Srv().Store().UserAccessToken().GetByToken(tokenString)
if nErr != nil {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "", http.StatusUnauthorized).Wrap(nErr)
}
if !token.IsActive {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_token", http.StatusUnauthorized)
}
user, nErr := a.Srv().Store().User().Get(context.Background(), token.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("createSessionForUserAccessToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !*a.Config().ServiceSettings.EnableUserAccessTokens && !user.IsBot {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "EnableUserAccessTokens=false", http.StatusUnauthorized)
}
if user.DeleteAt != 0 {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_user_id="+user.Id, http.StatusUnauthorized)
}
session := &model.Session{
Token: token.Token,
UserId: user.Id,
Roles: user.GetRawRoles(),
IsOAuth: false,
}
session.AddProp(model.SessionPropUserAccessTokenId, token.Id)
session.AddProp(model.SessionPropType, model.SessionTypeUserAccessToken)
if user.IsBot {
session.AddProp(model.SessionPropIsBot, model.SessionPropIsBotValue)
}
if user.IsGuest() {
session.AddProp(model.SessionPropIsGuest, "true")
} else {
session.AddProp(model.SessionPropIsGuest, "false")
}
a.ch.srv.platform.SetSessionExpireInHours(session, model.SessionUserAccessTokenExpiryHours)
session, nErr = a.Srv().Store().Session().Save(session)
if nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("CreateSession", "app.session.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreateSession", "app.session.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ch.srv.platform.AddSessionToCache(session)
return session, nil
}
func (a *App) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
if err := a.Srv().Store().UserAccessToken().Delete(token.Id); err != nil {
return model.NewAppError("RevokeUserAccessToken", "app.user_access_token.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return a.RevokeSession(session)
}
func (a *App) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
if err := a.Srv().Store().UserAccessToken().UpdateTokenDisable(token.Id); err != nil {
return model.NewAppError("DisableUserAccessToken", "app.user_access_token.update_token_disable.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return a.RevokeSession(session)
}
func (a *App) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
err := a.Srv().Store().UserAccessToken().UpdateTokenEnable(token.Id)
if err != nil {
return model.NewAppError("EnableUserAccessToken", "app.user_access_token.update_token_enable.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return nil
}
func (a *App) GetUserAccessTokens(page, perPage int) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().GetAll(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetUserAccessTokens", "app.user_access_token.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
func (a *App) GetUserAccessTokensForUser(userID string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().GetByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetUserAccessTokensForUser", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
func (a *App) GetUserAccessToken(tokenID string, sanitize bool) (*model.UserAccessToken, *model.AppError) {
token, err := a.Srv().Store().UserAccessToken().Get(tokenID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserAccessToken", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserAccessToken", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if sanitize {
token.Token = ""
}
return token, nil
}
func (a *App) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().Search(term)
if err != nil {
return nil, model.NewAppError("SearchUserAccessTokens", "app.user_access_token.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) checkChannelNotShared(c request.CTX, channelId string) error {
// check that channel exists.
if _, err := a.GetChannel(c, channelId); err != nil {
return fmt.Errorf("cannot share this channel: %w", err)
}
// Check channel is not already shared.
if _, err := a.GetSharedChannel(channelId); err == nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is already shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
return nil
}
func (a *App) checkChannelIsShared(channelId string) error {
if _, err := a.GetSharedChannel(channelId); err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is not shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
return nil
}
func (a *App) CheckCanInviteToSharedChannel(channelId string) error {
sc, err := a.GetSharedChannel(channelId)
if err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is not shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
if !sc.Home {
return errors.New("channel is homed on a remote cluster")
}
return nil
}
// SharedChannels
func (a *App) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
if err := a.checkChannelNotShared(c, sc.ChannelId); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().Save(sc)
}
func (a *App) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
return a.Srv().Store().SharedChannel().Get(channelID)
}
func (a *App) HasSharedChannel(channelID string) (bool, error) {
return a.Srv().Store().SharedChannel().HasChannel(channelID)
}
func (a *App) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
channels, err := a.Srv().Store().SharedChannel().GetAll(page*perPage, perPage, opts)
if err != nil {
return nil, model.NewAppError("GetSharedChannels", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
return a.Srv().Store().SharedChannel().GetAllCount(opts)
}
func (a *App) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return a.Srv().Store().SharedChannel().Update(sc)
}
func (a *App) DeleteSharedChannel(channelID string) (bool, error) {
return a.Srv().Store().SharedChannel().Delete(channelID)
}
// SharedChannelRemotes
func (a *App) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
if err := a.checkChannelIsShared(remote.ChannelId); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().SaveRemote(remote)
}
func (a *App) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemote(id)
}
func (a *App) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemoteByIds(channelID, remoteID)
}
func (a *App) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemotes(opts)
}
// HasRemote returns whether a given channelID is present in the channel remotes or not.
func (a *App) HasRemote(channelID string, remoteID string) (bool, error) {
return a.Srv().Store().SharedChannel().HasRemote(channelID, remoteID)
}
func (a *App) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().SharedChannel().GetRemoteForUser(remoteID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return rc, nil
}
func (a *App) UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
return a.Srv().Store().SharedChannel().UpdateRemoteCursor(id, cursor)
}
func (a *App) DeleteSharedChannelRemote(id string) (bool, error) {
return a.Srv().Store().SharedChannel().DeleteRemote(id)
}
func (a *App) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
if err := a.checkChannelIsShared(channelID); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().GetRemotesStatus(channelID)
}
// SharedChannelUsers
func (a *App) NotifySharedChannelUserUpdate(user *model.User) {
a.sendUpdatedUserEvent(*user)
}
// onUserProfileChange is called when a user's profile has changed
// (username, email, profile image, ...)
func (a *App) onUserProfileChange(userID string) {
syncService := a.Srv().GetSharedChannelSyncService()
if syncService == nil || !syncService.Active() {
return
}
syncService.NotifyUserProfileChanged(userID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
// TODO: platform: remove this and use from platform package
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
)
// SharedChannelServiceIFace is the interface to the shared channel service
type SharedChannelServiceIFace interface {
Shutdown() error
Start() error
NotifyChannelChanged(channelId string)
NotifyUserProfileChanged(userID string)
SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error
Active() bool
}
type MockOptionSharedChannelService func(service *mockSharedChannelService)
func MockOptionSharedChannelServiceWithActive(active bool) MockOptionSharedChannelService {
return func(mrcs *mockSharedChannelService) {
mrcs.active = active
}
}
func NewMockSharedChannelService(service SharedChannelServiceIFace, options ...MockOptionSharedChannelService) *mockSharedChannelService {
mrcs := &mockSharedChannelService{service, true, []string{}, []string{}, 0}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockSharedChannelService struct {
SharedChannelServiceIFace
active bool
channelNotifications []string
userProfileNotifications []string
numInvitations int
}
func (mrcs *mockSharedChannelService) NotifyChannelChanged(channelId string) {
mrcs.channelNotifications = append(mrcs.channelNotifications, channelId)
}
func (mrcs *mockSharedChannelService) NotifyUserProfileChanged(userId string) {
mrcs.userProfileNotifications = append(mrcs.userProfileNotifications, userId)
}
func (mrcs *mockSharedChannelService) Shutdown() error {
return nil
}
func (mrcs *mockSharedChannelService) Start() error {
return nil
}
func (mrcs *mockSharedChannelService) Active() bool {
return mrcs.active
}
func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error {
mrcs.numInvitations += 1
return nil
}
func (mrcs *mockSharedChannelService) NumInvitations() int {
return mrcs.numInvitations
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"fmt"
"image"
"mime/multipart"
"regexp"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/slackimport"
)
func (a *App) SlackImport(c *request.Context, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
actions := slackimport.Actions{
UpdateActive: func(user *model.User, active bool) (*model.User, *model.AppError) {
return a.UpdateActive(c, user, active)
},
AddUserToChannel: a.AddUserToChannel,
JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
return a.JoinUserToTeam(c, team, user, userRequestorId)
},
CreateDirectChannel: a.createDirectChannel,
CreateGroupChannel: a.createGroupChannel,
CreateChannel: func(channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
return a.CreateChannel(c, channel, addMember)
},
DoUploadFile: func(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
return a.DoUploadFile(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
},
GenerateThumbnailImage: a.generateThumbnailImage,
GeneratePreviewImage: a.generatePreviewImage,
InvalidateAllCaches: func() { a.ch.srv.InvalidateAllCaches() },
MaxPostSize: func() int { return a.ch.srv.platform.MaxPostSize() },
PrepareImage: func(fileData []byte) (image.Image, string, func(), error) {
img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData))
if err != nil {
return nil, "", nil, err
}
return img, imgType, release, err
},
}
importer := slackimport.New(a.Srv().Store(), actions, a.Config())
return importer.SlackImport(c, fileData, fileSize, teamID)
}
func (a *App) ProcessSlackText(text string) string {
text = expandAnnouncement(text)
text = replaceUserIds(a.Srv().Store().User(), text)
return text
}
// Expand announcements in incoming webhooks from Slack. Those announcements
// can be found in the text attribute, or in the pretext, text, title and value
// attributes of the attachment structure. The Slack attachment structure is
// documented here: https://api.slack.com/docs/attachments
func (a *App) ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment {
var nonNilAttachments = model.StringifySlackFieldValue(attachments)
for _, attachment := range attachments {
attachment.Pretext = a.ProcessSlackText(attachment.Pretext)
attachment.Text = a.ProcessSlackText(attachment.Text)
attachment.Title = a.ProcessSlackText(attachment.Title)
for _, field := range attachment.Fields {
if field != nil && field.Value != nil {
// Ensure the value is set to a string if it is set
field.Value = a.ProcessSlackText(fmt.Sprintf("%v", field.Value))
}
}
}
return nonNilAttachments
}
// To mention @channel or @here via a webhook in Slack, the message should contain
// <!channel> or <!here>, as explained at the bottom of this article:
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
func expandAnnouncement(text string) string {
a1 := [3]string{"<!channel>", "<!here>", "<!all>"}
a2 := [3]string{"@channel", "@here", "@all"}
for i, a := range a1 {
text = strings.Replace(text, a, a2[i], -1)
}
return text
}
// Replaces user IDs mentioned like this <@userID> to a normal username (eg. @bob)
// This is required so that Mattermost maintains Slack compatibility
// Refer to: https://api.slack.com/changelog/2017-09-the-one-about-usernames
func replaceUserIds(userStore store.UserStore, text string) string {
rgx, err := regexp.Compile("<@([a-zA-Z0-9]+)>")
if err == nil {
userIDs := make([]string, 0)
matches := rgx.FindAllStringSubmatch(text, -1)
for _, match := range matches {
userIDs = append(userIDs, match[1])
}
if users, err := userStore.GetProfileByIds(context.Background(), userIDs, nil, true); err == nil {
for _, user := range users {
text = strings.Replace(text, "<@"+user.Id+">", "@"+user.Username, -1)
}
}
}
return text
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoChannelCreator struct {
a *app.App
userID string
team *model.Team
Fuzzy bool
DisplayNameLen utils.Range
DisplayNameCharset string
NameLen utils.Range
NameCharset string
ChannelType model.ChannelType
CreateTime int64
}
func NewAutoChannelCreator(a *app.App, team *model.Team, userID string) *AutoChannelCreator {
return &AutoChannelCreator{
a: a,
team: team,
userID: userID,
Fuzzy: false,
DisplayNameLen: ChannelDisplayNameLen,
DisplayNameCharset: utils.ALPHANUMERIC,
NameLen: ChannelNameLen,
NameCharset: utils.LOWERCASE,
ChannelType: ChannelType,
CreateTime: 0,
}
}
func (cfg *AutoChannelCreator) createRandomChannel(c request.CTX) (*model.Channel, error) {
var displayName string
if cfg.Fuzzy {
displayName = utils.FuzzName()
} else {
displayName = utils.RandomName(cfg.NameLen, cfg.NameCharset)
}
name := utils.RandomName(cfg.NameLen, cfg.NameCharset)
channel := &model.Channel{
TeamId: cfg.team.Id,
DisplayName: displayName,
Name: name,
Type: cfg.ChannelType,
CreatorId: cfg.userID,
CreateAt: cfg.CreateTime,
}
channel, err := cfg.a.CreateChannel(c, channel, true)
if err != nil {
return nil, err
}
return channel, nil
}
func (cfg *AutoChannelCreator) CreateTestChannels(c request.CTX, num utils.Range) ([]*model.Channel, error) {
numChannels := utils.RandIntFromRange(num)
channels := make([]*model.Channel, numChannels)
for i := 0; i < numChannels; i++ {
var err error
channels[i], err = cfg.createRandomChannel(c)
if err != nil {
return nil, err
}
}
return channels, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"math/rand"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TestEnvironment struct {
Teams []*model.Team
Environments []TeamEnvironment
}
func CreateTestEnvironmentWithTeams(a *app.App, c request.CTX, client *model.Client4, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
teamCreator := NewAutoTeamCreator(client)
teamCreator.Fuzzy = fuzzy
teams, err := teamCreator.CreateTestTeams(rangeTeams)
if err != nil {
return TestEnvironment{}, err
}
environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))}
for i, team := range teams {
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
randomUser, err := userCreator.createRandomUser(c)
if err != nil {
return TestEnvironment{}, err
}
client.LoginById(randomUser.Id, UserPassword)
teamEnvironment, err := CreateTestEnvironmentInTeam(a, c, client, team, rangeChannels, rangeUsers, rangePosts, fuzzy)
if err != nil {
return TestEnvironment{}, err
}
environment.Environments[i] = teamEnvironment
}
return environment, nil
}
func CreateTestEnvironmentInTeam(a *app.App, c request.CTX, client *model.Client4, team *model.Team, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
// We need to create at least one user
if rangeUsers.Begin <= 0 {
rangeUsers.Begin = 1
}
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
users, err := userCreator.CreateTestUsers(c, rangeUsers)
if err != nil {
return TeamEnvironment{}, nil
}
usernames := make([]string, len(users))
for i, user := range users {
usernames[i] = user.Username
}
channelCreator := NewAutoChannelCreator(a, team, users[0].Id)
channelCreator.Fuzzy = fuzzy
channels, err := channelCreator.CreateTestChannels(c, rangeChannels)
if err != nil {
return TeamEnvironment{}, nil
}
// Have every user join every channel
for _, user := range users {
for _, channel := range channels {
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
_, _, err = client.AddChannelMember(channel.Id, user.Id)
if err != nil {
return TeamEnvironment{}, err
}
}
}
numPosts := utils.RandIntFromRange(rangePosts)
numImages := utils.RandIntFromRange(rangePosts) / 4
for j := 0; j < numPosts; j++ {
user := users[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(users) - 1})]
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
for i, channel := range channels {
postCreator := NewAutoPostCreator(a, channel.Id, user.Id)
postCreator.HasImage = i < numImages
postCreator.Users = usernames
postCreator.Fuzzy = fuzzy
_, err := postCreator.CreateRandomPost(c)
if err != nil {
return TeamEnvironment{}, err
}
}
}
return TeamEnvironment{users, channels}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"bytes"
"io"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
type AutoPostCreator struct {
a *app.App
channelid string
userid string
Fuzzy bool
TextLength utils.Range
HasImage bool
ImageFilenames []string
Users []string
UsersToPostFrom []string
Mentions utils.Range
Tags utils.Range
CreateTime int64
postsCreated int
}
// Automatic poster used for testing
func NewAutoPostCreator(a *app.App, channelid, userid string) *AutoPostCreator {
return &AutoPostCreator{
a: a,
channelid: channelid,
userid: userid,
Fuzzy: false,
TextLength: utils.Range{Begin: 100, End: 200},
HasImage: false,
ImageFilenames: TestImageFileNames,
Users: []string{},
UsersToPostFrom: []string{},
Mentions: utils.Range{Begin: 0, End: 5},
Tags: utils.Range{Begin: 0, End: 7},
CreateTime: 0,
postsCreated: 0,
}
}
func (cfg *AutoPostCreator) UploadTestFile(c request.CTX) ([]string, error) {
filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.ImageFilenames) - 1})]
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, filename))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
_, err = io.Copy(data, file)
if err != nil {
return nil, err
}
fileResp, err2 := cfg.a.UploadFile(c, data.Bytes(), cfg.channelid, filename)
if err2 != nil {
return nil, err2
}
return []string{fileResp.Id}, nil
}
func (cfg *AutoPostCreator) CreateRandomPost(c request.CTX) (*model.Post, error) {
return cfg.CreateRandomPostNested(c, "")
}
func (cfg *AutoPostCreator) CreateRandomPostNested(c request.CTX, rootId string) (*model.Post, error) {
var fileIDs []string
if cfg.HasImage {
var err error
fileIDs, err = cfg.UploadTestFile(c)
if err != nil {
return nil, err
}
}
var postText string
if cfg.Fuzzy {
postText = utils.FuzzPost()
} else {
postText = utils.RandomText(cfg.TextLength, cfg.Tags, cfg.Mentions, cfg.Users)
}
post := &model.Post{
ChannelId: cfg.channelid,
UserId: cfg.userid,
RootId: rootId,
Message: postText,
FileIds: fileIDs,
}
if cfg.CreateTime != 0 {
// Creating posts with the exact same timestamp results in some posts being skipped
// when they are retrieved by the API based on timestamp.
post.CreateAt = cfg.CreateTime + int64(cfg.postsCreated)
}
if len(cfg.UsersToPostFrom) != 0 {
i := utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.UsersToPostFrom)})
if i < len(cfg.UsersToPostFrom) {
post.UserId = cfg.UsersToPostFrom[i]
}
}
rpost, err := cfg.a.CreatePostMissingChannel(c, post, true, true)
if err != nil {
return nil, err
}
cfg.postsCreated += 1
return rpost, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TeamEnvironment struct {
Users []*model.User
Channels []*model.Channel
}
type AutoTeamCreator struct {
client *model.Client4
Fuzzy bool
NameLength utils.Range
NameCharset string
DomainLength utils.Range
DomainCharset string
EmailLength utils.Range
EmailCharset string
}
func NewAutoTeamCreator(client *model.Client4) *AutoTeamCreator {
return &AutoTeamCreator{
client: client,
Fuzzy: false,
NameLength: TeamNameLen,
NameCharset: utils.LOWERCASE,
DomainLength: TeamDomainNameLen,
DomainCharset: utils.LOWERCASE,
EmailLength: TeamEmailLen,
EmailCharset: utils.LOWERCASE,
}
}
func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, error) {
var teamEmail string
var teamDisplayName string
var teamName string
if cfg.Fuzzy {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.FuzzName()
teamName = model.NewRandomTeamName()
} else {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId()
}
team := &model.Team{
DisplayName: teamDisplayName,
Name: teamName,
Email: teamEmail,
Type: model.TeamOpen,
}
createdTeam, _, err := cfg.client.CreateTeam(team)
if err != nil {
return nil, err
}
return createdTeam, nil
}
func (cfg *AutoTeamCreator) CreateTestTeams(num utils.Range) ([]*model.Team, error) {
numTeams := utils.RandIntFromRange(num)
teams := make([]*model.Team, numTeams)
for i := 0; i < numTeams; i++ {
var err error
teams[i], err = cfg.createRandomTeam()
if err != nil {
return nil, err
}
}
return teams, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoUserCreator struct {
app *app.App
client *model.Client4
team *model.Team
EmailLength utils.Range
EmailCharset string
NameLength utils.Range
NameCharset string
Fuzzy bool
JoinTime int64
}
func NewAutoUserCreator(a *app.App, client *model.Client4, team *model.Team) *AutoUserCreator {
return &AutoUserCreator{
app: a,
client: client,
team: team,
EmailLength: UserEmailLen,
EmailCharset: utils.LOWERCASE,
NameLength: UserNameLen,
NameCharset: utils.LOWERCASE,
Fuzzy: false,
JoinTime: 0,
}
}
// Basic test team and user so you always know one
func CreateBasicUser(a *app.App, client *model.Client4) error {
found, _, _ := client.TeamExists(BTestTeamName, "")
if found {
return nil
}
newteam := &model.Team{DisplayName: BTestTeamDisplayName, Name: BTestTeamName, Email: BTestTeamEmail, Type: BTestTeamType}
basicteam, _, err := client.CreateTeam(newteam)
if err != nil {
return err
}
newuser := &model.User{Email: BTestUserEmail, Nickname: BTestUserName, Password: BTestUserPassword}
ruser, _, err := client.CreateUser(newuser)
if err != nil {
return err
}
_, err = a.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return model.NewAppError("CreateBasicUser", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if _, nErr := a.Srv().Store().Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id, CreateAt: model.GetMillis()}, *a.Config().TeamSettings.MaxUsersPerTeam); nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &limitExceededErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (cfg *AutoUserCreator) createRandomUser(c request.CTX) (*model.User, error) {
var userEmail string
var userName string
if cfg.Fuzzy {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.FuzzName()
} else {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
}
user := &model.User{
Email: userEmail,
Nickname: userName,
Password: UserPassword,
CreateAt: cfg.JoinTime,
}
ruser, appErr := cfg.app.CreateUserWithInviteId(c, user, cfg.team.InviteId, "")
if appErr != nil {
return nil, appErr
}
status := &model.Status{
UserId: ruser.Id,
Status: model.StatusOnline,
Manual: false,
LastActivityAt: ruser.CreateAt,
ActiveChannel: "",
}
if err := cfg.app.Srv().Store().Status().SaveOrUpdate(status); err != nil {
return nil, err
}
// We need to cheat to verify the user's email
_, err := cfg.app.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil, err
}
if cfg.JoinTime != 0 {
teamMember, appErr := cfg.app.GetTeamMember(cfg.team.Id, ruser.Id)
if appErr != nil {
return nil, appErr
}
teamMember.CreateAt = cfg.JoinTime
_, err := cfg.app.Srv().Store().Team().UpdateMember(teamMember)
if err != nil {
return nil, err
}
}
return ruser, nil
}
func (cfg *AutoUserCreator) CreateTestUsers(c request.CTX, num utils.Range) ([]*model.User, error) {
numUsers := utils.RandIntFromRange(num)
users := make([]*model.User, numUsers)
for i := 0; i < numUsers; i++ {
var err error
users[i], err = cfg.createRandomUser(c)
if err != nil {
return nil, err
}
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type AwayProvider struct {
}
const (
CmdAway = "away"
)
func init() {
app.RegisterCommandProvider(&AwayProvider{})
}
func (*AwayProvider) GetTrigger() string {
return CmdAway
}
func (*AwayProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdAway,
AutoComplete: true,
AutoCompleteDesc: T("api.command_away.desc"),
DisplayName: T("api.command_away.name"),
}
}
func (*AwayProvider) DoCommand(a *app.App, _ request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusAwayIfNeeded(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_away.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HeaderProvider struct {
}
const (
CmdHeader = "header"
)
func init() {
app.RegisterCommandProvider(&HeaderProvider{})
}
func (*HeaderProvider) GetTrigger() string {
return CmdHeader
}
func (*HeaderProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHeader,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_header.desc"),
AutoCompleteHint: T("api.command_channel_header.hint"),
DisplayName: T("api.command_channel_header.name"),
}
}
func (*HeaderProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
var channelMember *model.ChannelMember
channelMember, err = a.GetChannelMember(c, args.ChannelId, args.UserId)
if err != nil || channelMember == nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Header: new(string),
}
*patch.Header = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_header.update_channel.app_error")
if err.Id == "model.channel.is_valid.header.app_error" {
text = args.T("api.command_channel_header.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelHeaderMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type PurposeProvider struct {
}
const (
CmdPurpose = "purpose"
)
func init() {
app.RegisterCommandProvider(&PurposeProvider{})
}
func (*PurposeProvider) GetTrigger() string {
return CmdPurpose
}
func (*PurposeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdPurpose,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_purpose.desc"),
AutoCompleteHint: T("api.command_channel_purpose.hint"),
DisplayName: T("api.command_channel_purpose.name"),
}
}
func (*PurposeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Purpose: new(string),
}
*patch.Purpose = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_purpose.update_channel.app_error")
if err.Id == "model.channel.is_valid.purpose.app_error" {
text = args.T("api.command_channel_purpose.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelPurposeMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type RenameProvider struct {
}
const (
CmdRename = "rename"
)
func init() {
app.RegisterCommandProvider(&RenameProvider{})
}
func (*RenameProvider) GetTrigger() string {
return CmdRename
}
func (*RenameProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
renameAutocompleteData := model.NewAutocompleteData(CmdRename, T("api.command_channel_rename.hint"), T("api.command_channel_rename.desc"))
renameAutocompleteData.AddTextArgument(T("api.command_channel_rename.hint"), "[text]", "")
return &model.Command{
Trigger: CmdRename,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_rename.desc"),
AutoCompleteHint: T("api.command_channel_rename.hint"),
DisplayName: T("api.command_channel_rename.name"),
AutocompleteData: renameAutocompleteData,
}
}
func (*RenameProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{Text: args.T("api.command_channel_rename.direct_group.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) > model.ChannelNameMaxLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_long.app_error", map[string]any{
"Length": model.ChannelNameMaxLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) < model.ChannelNameMinLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_short.app_error", map[string]any{
"Length": model.ChannelNameMinLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
DisplayName: new(string),
}
*patch.DisplayName = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.update_channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type CodeProvider struct {
}
const (
CmdCode = "code"
)
func init() {
app.RegisterCommandProvider(&CodeProvider{})
}
func (*CodeProvider) GetTrigger() string {
return CmdCode
}
func (*CodeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCode,
AutoComplete: true,
AutoCompleteDesc: T("api.command_code.desc"),
AutoCompleteHint: T("api.command_code.hint"),
DisplayName: T("api.command_code.name"),
}
}
func (*CodeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_code.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
rmsg := " " + strings.Join(strings.Split(message, "\n"), "\n ")
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg, SkipSlackParsing: true}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"regexp"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type CustomStatusProvider struct {
}
const (
CmdCustomStatus = app.CmdCustomStatusTrigger
CmdCustomStatusClear = "clear"
)
func init() {
app.RegisterCommandProvider(&CustomStatusProvider{})
}
func (*CustomStatusProvider) GetTrigger() string {
return CmdCustomStatus
}
func (*CustomStatusProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCustomStatus,
AutoComplete: true,
AutoCompleteDesc: T("api.command_custom_status.desc"),
AutoCompleteHint: T("api.command_custom_status.hint"),
DisplayName: T("api.command_custom_status.name"),
}
}
func (*CustomStatusProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
return nil
}
message = strings.TrimSpace(message)
if message == CmdCustomStatusClear {
if err := a.RemoveCustomStatus(c, args.UserId); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.clear.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.clear.success"),
}
}
customStatus := GetCustomStatus(message)
customStatus.PreSave()
if err := a.SetCustomStatus(c, args.UserId, customStatus); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.success", map[string]any{
"EmojiName": ":" + customStatus.Emoji + ":",
"StatusMessage": customStatus.Text,
}),
}
}
func GetCustomStatus(message string) *model.CustomStatus {
customStatus := &model.CustomStatus{
Emoji: model.DefaultCustomStatusEmoji,
Text: message,
}
firstEmojiLocations := model.EmojiPattern.FindIndex([]byte(message))
if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 {
// emoji found at starting index
customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1]
customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:])
return customStatus
}
if message == "" {
return customStatus
}
spaceSeparatedMessage := strings.Fields(message)
if len(spaceSeparatedMessage) == 0 {
return customStatus
}
emojiString := spaceSeparatedMessage[0]
var unicode []string
for utf8.RuneCountInString(emojiString) >= 1 {
codepoint, size := utf8.DecodeRuneInString(emojiString)
code := model.RuneToHexadecimalString(codepoint)
unicode = append(unicode, code)
emojiString = emojiString[size:]
}
unicodeString := removeUnicodeSkinTone(strings.Join(unicode, "-"))
emoji, count := model.GetEmojiNameFromUnicode(unicodeString)
if count > 0 {
customStatus.Emoji = emoji
textString := strings.Join(spaceSeparatedMessage[1:], " ")
customStatus.Text = strings.TrimSpace(textString)
}
return customStatus
}
func removeUnicodeSkinTone(unicodeString string) string {
skinToneDetectorRegex := regexp.MustCompile("-(1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)")
skinToneLocations := skinToneDetectorRegex.FindIndex([]byte(unicodeString))
if len(skinToneLocations) == 0 {
return unicodeString
}
if _, count := model.GetEmojiNameFromUnicode(unicodeString); count > 0 {
return unicodeString
}
unicodeWithRemovedSkinTone := unicodeString[:skinToneLocations[0]] + unicodeString[skinToneLocations[1]:]
unicodeWithVariationSelector := unicodeString[:skinToneLocations[0]] + "-fe0f" + unicodeString[skinToneLocations[1]:]
if _, count := model.GetEmojiNameFromUnicode(unicodeWithRemovedSkinTone); count > 0 {
unicodeString = unicodeWithRemovedSkinTone
} else if _, count := model.GetEmojiNameFromUnicode(unicodeWithVariationSelector); count > 0 {
unicodeString = unicodeWithVariationSelector
}
return unicodeString
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type DndProvider struct {
}
const (
CmdDND = "dnd"
)
func init() {
app.RegisterCommandProvider(&DndProvider{})
}
func (*DndProvider) GetTrigger() string {
return CmdDND
}
func (*DndProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdDND,
AutoComplete: true,
AutoCompleteDesc: T("api.command_dnd.desc"),
DisplayName: T("api.command_dnd.name"),
}
}
func (*DndProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusDoNotDisturb(args.UserId)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_dnd.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var echoSem chan bool
type EchoProvider struct {
}
const (
CmdEcho = "echo"
)
func init() {
app.RegisterCommandProvider(&EchoProvider{})
}
func (*EchoProvider) GetTrigger() string {
return CmdEcho
}
func (*EchoProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdEcho,
AutoComplete: true,
AutoCompleteDesc: T("api.command_echo.desc"),
AutoCompleteHint: T("api.command_echo.hint"),
DisplayName: T("api.command_echo.name"),
}
}
func (*EchoProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_echo.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
maxThreads := 100
delay := 0
if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
delay = checkDelay
}
message = message[1:endMsg]
} else if strings.Contains(message, " ") {
delayIdx := strings.LastIndex(message, " ")
delayStr := strings.Trim(message[delayIdx:], " ")
if checkDelay, err := strconv.Atoi(delayStr); err == nil {
delay = checkDelay
message = message[:delayIdx]
}
}
if delay > 10000 {
return &model.CommandResponse{Text: args.T("api.command_echo.delay.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if echoSem == nil {
// We want one additional thread allowed so we never reach channel lockup
echoSem = make(chan bool, maxThreads+1)
}
if len(echoSem) >= maxThreads {
return &model.CommandResponse{Text: args.T("api.command_echo.high_volume.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
echoSem <- true
a.Srv().Go(func() {
defer func() { <-echoSem }()
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.Message = message
post.UserId = args.UserId
time.Sleep(time.Duration(delay) * time.Second)
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
mlog.Error("Unable to create /echo post.", mlog.Err(err))
}
})
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ExpandProvider struct {
}
type CollapseProvider struct {
}
const (
CmdExpand = "expand"
CmdCollapse = "collapse"
)
func init() {
app.RegisterCommandProvider(&ExpandProvider{})
app.RegisterCommandProvider(&CollapseProvider{})
}
func (*ExpandProvider) GetTrigger() string {
return CmdExpand
}
func (*CollapseProvider) GetTrigger() string {
return CmdCollapse
}
func (*ExpandProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdExpand,
AutoComplete: true,
AutoCompleteDesc: T("api.command_expand.desc"),
DisplayName: T("api.command_expand.name"),
}
}
func (*CollapseProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCollapse,
AutoComplete: true,
AutoCompleteDesc: T("api.command_collapse.desc"),
DisplayName: T("api.command_collapse.name"),
}
}
func (*ExpandProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, false)
}
func (*CollapseProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, true)
}
func setCollapsePreference(a *app.App, args *model.CommandArgs, isCollapse bool) *model.CommandResponse {
pref := model.Preference{
UserId: args.UserId,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
Value: strconv.FormatBool(isCollapse),
}
if err := a.Srv().Store().Preference().Save(model.Preferences{pref}); err != nil {
return &model.CommandResponse{Text: args.T("api.command_expand_collapse.fail.app_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage := model.NewWebSocketEvent(model.WebsocketEventPreferenceChanged, "", "", args.UserId, nil, "")
prefJSON, err := json.Marshal(pref)
if err != nil {
return &model.CommandResponse{Text: args.T("api.marshal_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage.Add("preference", string(prefJSON))
a.Publish(socketMessage)
var rmsg string
if isCollapse {
rmsg = args.T("api.command_collapse.success")
} else {
rmsg = args.T("api.command_expand.success")
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: rmsg}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type groupmsgProvider struct {
}
const (
CmdGroupMsg = "groupmsg"
)
func init() {
app.RegisterCommandProvider(&groupmsgProvider{})
}
func (*groupmsgProvider) GetTrigger() string {
return CmdGroupMsg
}
func (*groupmsgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdGroupMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_groupmsg.desc"),
AutoCompleteHint: T("api.command_groupmsg.hint"),
DisplayName: T("api.command_groupmsg.name"),
}
}
func (*groupmsgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
targetUsers := map[string]*model.User{}
targetUsersSlice := []string{args.UserId}
invalidUsernames := []string{}
users, parsedMessage := groupMsgUsernames(message)
for _, username := range users {
username = strings.TrimSpace(username)
username = strings.TrimPrefix(username, "@")
targetUser, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
invalidUsernames = append(invalidUsernames, username)
continue
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, targetUser.Id)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
invalidUsernames = append(invalidUsernames, username)
continue
}
_, exists := targetUsers[targetUser.Id]
if !exists && targetUser.Id != args.UserId {
targetUsers[targetUser.Id] = targetUser
targetUsersSlice = append(targetUsersSlice, targetUser.Id)
}
}
if len(invalidUsernames) > 0 {
invalidUsersString := map[string]any{
"Users": "@" + strings.Join(invalidUsernames, ", @"),
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.invalid_user.app_error", len(invalidUsernames), invalidUsersString),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) == 2 {
return app.GetCommandProvider("msg").DoCommand(a, c, args, fmt.Sprintf("%s %s", targetUsers[targetUsersSlice[1]].Username, parsedMessage))
}
if len(targetUsersSlice) < model.ChannelGroupMinUsers {
minUsers := map[string]any{
"MinUsers": model.ChannelGroupMinUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.min_users.app_error", minUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) > model.ChannelGroupMaxUsers {
maxUsers := map[string]any{
"MaxUsers": model.ChannelGroupMaxUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.max_users.app_error", maxUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
var groupChannel *model.Channel
var channelErr *model.AppError
if a.HasPermissionTo(args.UserId, model.PermissionCreateGroupChannel) {
groupChannel, channelErr = a.CreateGroupChannel(c, targetUsersSlice, args.UserId)
if channelErr != nil {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_groupmsg.group_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
groupChannel, channelErr = a.GetGroupChannel(c, targetUsersSlice)
if channelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = groupChannel.Id
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + groupChannel.Name, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}
func groupMsgUsernames(message string) ([]string, string) {
result := []string{}
resultMessage := ""
for idx, part := range strings.Split(message, ",") {
clean := strings.TrimPrefix(strings.TrimSpace(part), "@")
split := strings.Fields(clean)
if len(split) > 0 {
result = append(result, split[0])
}
if len(split) > 1 {
splitted := strings.SplitN(message, ",", idx+1)
resultMessage = strings.TrimPrefix(strings.TrimSpace(splitted[len(splitted)-1]), "@")
resultMessage = strings.TrimSpace(strings.TrimPrefix(resultMessage, split[0]))
break
}
}
return result, resultMessage
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HelpProvider struct {
}
const (
CmdHelp = "help"
)
func init() {
app.RegisterCommandProvider(&HelpProvider{})
}
func (h *HelpProvider) GetTrigger() string {
return CmdHelp
}
func (h *HelpProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHelp,
AutoComplete: true,
AutoCompleteDesc: T("api.command_help.desc"),
DisplayName: T("api.command_help.name"),
}
}
func (h *HelpProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
helpLink := *a.Config().SupportSettings.HelpLink
if helpLink == "" {
helpLink = model.SupportSettingsDefaultHelpLink
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_help.success", map[string]any{
"HelpLink": helpLink,
}),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type InviteProvider struct {
}
const (
CmdInvite = "invite"
)
func init() {
app.RegisterCommandProvider(&InviteProvider{})
}
func (*InviteProvider) GetTrigger() string {
return CmdInvite
}
func (*InviteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdInvite,
AutoComplete: true,
AutoCompleteDesc: T("api.command_invite.desc"),
AutoCompleteHint: T("api.command_invite.hint"),
DisplayName: T("api.command_invite.name"),
}
}
func (i *InviteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
Text: i.doCommand(a, c, args, message),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
func (i *InviteProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) string {
if message == "" {
return args.T("api.command_invite.missing_message.app_error")
}
resps := &[]string{}
targetUsers, targetChannels, resp := i.parseMessage(a, c, args, resps, message)
if resp != "" {
return resp
}
// Verify that the inviter has permissions to invite users to the every channel.
targetChannels = i.checkPermissions(a, c, args, resps, targetUsers[0], targetChannels)
for _, targetUser := range targetUsers {
for _, targetChannel := range targetChannels {
if resp = i.addUserToChannel(a, c, args, targetUser, targetChannel); resp != "" {
*resps = append(*resps, resp)
continue
}
if args.ChannelId != targetChannel.Id {
*resps = append(*resps, args.T("api.command_invite.success", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
}
}
}
if len(*resps) > 0 {
return strings.Join(*resps, "\n")
}
return ""
}
func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, message string) ([]*model.User, []*model.Channel, string) {
splitMessage := strings.Split(message, " ")
targetUsers := make([]*model.User, 0, 1)
targetChannels := make([]*model.Channel, 0)
for j, msg := range splitMessage {
if msg == "" {
continue
}
if msg[0] == '@' || (msg[0] != '~' && j == 0) {
targetUsername := strings.TrimPrefix(msg, "@")
userProfile := i.getUserProfile(a, targetUsername)
if userProfile == nil {
*resps = append(*resps, args.T("api.command_invite.missing_user.app_error", map[string]any{
"User": targetUsername,
}))
continue
}
targetUsers = append(targetUsers, userProfile)
} else {
targetChannelName := strings.TrimPrefix(msg, "~")
channelToJoin, err := a.GetChannelByName(c, targetChannelName, args.TeamId, false)
if err != nil {
*resps = append(*resps, args.T("api.command_invite.channel.error", map[string]any{
"Channel": targetChannelName,
}))
continue
}
targetChannels = append(targetChannels, channelToJoin)
}
}
if len(targetUsers) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
return nil, nil, args.T("api.command_invite.missing_message.app_error")
}
if len(targetChannels) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
channelToJoin, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return nil, nil, args.T("api.command_invite.channel.app_error")
}
targetChannels = append(targetChannels, channelToJoin)
}
return targetUsers, targetChannels, ""
}
func (i *InviteProvider) getUserProfile(a *app.App, username string) *model.User {
userProfile, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
return nil
}
if userProfile.DeleteAt != 0 {
return nil
}
return userProfile
}
func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, targetUser *model.User, targetChannels []*model.Channel) []*model.Channel {
var err *model.AppError
validChannels := make([]*model.Channel, 0, len(targetChannels))
for _, targetChannel := range targetChannels {
switch targetChannel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePublicChannelMembers) {
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePrivateChannelMembers) {
if _, err = a.GetChannelMember(c, targetChannel.Id, args.UserId); err == nil {
// User doing the inviting is a member of the channel.
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
// User doing the inviting is *not* a member of the channel.
*resps = append(*resps, args.T("api.command_invite.private_channel.app_error", map[string]any{
"Channel": targetChannel.Name,
}))
continue
}
default:
*resps = append(*resps, args.T("api.command_invite.directchannel.app_error"))
continue
}
validChannels = append(validChannels, targetChannel)
}
return validChannels
}
func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) string {
// Check if user is already in the channel
_, err := a.GetChannelMember(c, channelToJoin.Id, userProfile.Id)
if err == nil {
return args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{
"User": userProfile.Username,
})
}
if _, err = a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{UserRequestorID: args.UserId}); err != nil {
if err.Id == "api.channel.add_members.user_denied" {
return args.T("api.command_invite.group_constrained_user_denied")
} else if err.Id == "app.team.get_member.missing.app_error" ||
err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" {
return args.T("api.command_invite.user_not_in_team.app_error", map[string]any{
"Username": userProfile.Username,
})
}
return args.T("api.command_invite.fail.app_error")
}
return ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type InvitePeopleProvider struct {
}
const (
CmdInvite_PEOPLE = "invite_people"
)
func init() {
app.RegisterCommandProvider(&InvitePeopleProvider{})
}
func (*InvitePeopleProvider) GetTrigger() string {
return CmdInvite_PEOPLE
}
func (*InvitePeopleProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
autoComplete := true
if !*a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation || !*a.Config().ServiceSettings.EnableEmailInvitations {
autoComplete = false
}
return &model.Command{
Trigger: CmdInvite_PEOPLE,
AutoComplete: autoComplete,
AutoCompleteDesc: T("api.command.invite_people.desc"),
AutoCompleteHint: T("api.command.invite_people.hint"),
DisplayName: T("api.command.invite_people.name"),
}
}
func (*InvitePeopleProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionInviteUser) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionAddUserToTeam) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !*a.Config().EmailSettings.SendEmailNotifications {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_off")}
}
if !*a.Config().TeamSettings.EnableUserCreation {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.invite_off")}
}
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_invitations_off")}
}
emailList := strings.Fields(message)
for i := len(emailList) - 1; i >= 0; i-- {
emailList[i] = strings.Trim(emailList[i], ",")
if !strings.Contains(emailList[i], "@") {
emailList = append(emailList[:i], emailList[i+1:]...)
}
}
if len(emailList) == 0 {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.no_email")}
}
if err := a.InviteNewUsersToTeam(emailList, args.TeamId, args.UserId); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.fail")}
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.sent")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type JoinProvider struct {
}
const (
CmdJoin = "join"
)
func init() {
app.RegisterCommandProvider(&JoinProvider{})
}
func (*JoinProvider) GetTrigger() string {
return CmdJoin
}
func (*JoinProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdJoin,
AutoComplete: true,
AutoCompleteDesc: T("api.command_join.desc"),
AutoCompleteHint: T("api.command_join.hint"),
DisplayName: T("api.command_join.name"),
}
}
func (*JoinProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channelName := strings.ToLower(message)
if strings.HasPrefix(message, "~") {
channelName = message[1:]
}
channel, err := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channel.Name != channelName {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_join.missing.app_error")}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionJoinPublicChannels) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionReadChannel) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
default:
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if appErr := a.JoinChannel(c, channel, args.UserId); appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, appErr := a.GetTeam(channel.TeamId)
if appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LeaveProvider struct {
}
const (
CmdLeave = "leave"
)
func init() {
app.RegisterCommandProvider(&LeaveProvider{})
}
func (*LeaveProvider) GetTrigger() string {
return CmdLeave
}
func (*LeaveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLeave,
AutoComplete: true,
AutoCompleteDesc: T("api.command_leave.desc"),
DisplayName: T("api.command_leave.name"),
}
}
func (*LeaveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
err = a.LeaveChannel(c, args.ChannelId, args.UserId)
if err != nil {
if channel.Name == model.DefaultChannelName {
return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]any{"Channel": model.DefaultChannelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
member, err := a.GetTeamMember(team.Id, args.UserId)
if err != nil || member.DeleteAt != 0 {
return &model.CommandResponse{GotoLocation: args.SiteURL + "/"}
}
user, err := a.GetUser(args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if user.IsGuest() {
members, err := a.GetChannelMembersForUser(c, team.Id, args.UserId)
if err != nil || len(members) == 0 {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channel, err := a.GetChannel(c, members[0].ChannelId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DefaultChannelName}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"io"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var usage = `Mattermost testing commands to help configure the system
COMMANDS:
Setup - Creates a testing environment in current team.
/test setup [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>
Example:
/test setup teams fuzz 10 20 50
Users - Add a specified number of random users with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test users [fuzz] [range=min[,max]] [time=user_join_timestamp]
Default: range=2,5 time=
Examples:
/test users fuzz range=3,8 time=1565076128000
/test users range=1
Channels - Add a specified number of random public (o) or private (p) channels with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test channels [fuzz] [range=min[,max]] [type=(o|p)] [time=channel_create_timestamp]
Default: range=2,5 type=o time=
Examples:
/test channels fuzz range=5,10 type=p time=1565076128000
/test channels range=1
DMs - Add a specified number of random DM messages between the current user and a specified user, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test dms u=@username [range=min[,max]] [time=dm_create_timestamp]
Default: range=2,5 time=
Examples:
/test dms u=@user range=5,10 time=1565076128000
/test dms u=@user range=2
ThreadedPost - Create a threaded post with a specified number of replies at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test threaded_post [range=min[,max]] [time=post_timestamp]
Default: range=1000 time=
Examples:
/test threaded_post
/test threaded_post range=100,200 time=1565076128000
Posts - Add some random posts with fuzz text to current channel, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test posts [fuzz] [range=min[,max]] [images=max_images] [time=post_timestamp]
Default: range=2,5 images=0 time=
Example:
/test posts fuzz range=5,10 images=3 time=1565076128000
/test posts range=2
Post - Add post to a channel as another user.
/test post u=@username p=passwd c=~channelname t=teamname "message"
Example:
/test post u=@user-1 p=user-1 c=~town-square t=ad-1 "message"
Url - Add a post containing the text from a given url to current channel.
/test url
Example:
/test http://www.example.com/sample_file.md
Json - Add a post using the JSON file as payload to the current channel.
/test json url
Example:
/test json http://www.example.com/sample_body.json
`
const (
CmdTest = "test"
)
var (
userRE = regexp.MustCompile(`u=@?([^\s]+)`)
passwdRE = regexp.MustCompile(`p=([^\s]+)`)
teamRE = regexp.MustCompile(`t=([^\s]+)`)
channelRE = regexp.MustCompile(`c=~([^\s]+)`)
messageRE = regexp.MustCompile(`"(.*)"`)
fuzzRE = regexp.MustCompile(`fuzz`)
rangeRE = regexp.MustCompile(`range=([^\s]+)`)
timeRE = regexp.MustCompile(`time=([^\s]+)`)
imagesRE = regexp.MustCompile(`images=([^\s]+)`)
typeRE = regexp.MustCompile(`type=([^\s])+`)
)
type LoadTestProvider struct {
}
func init() {
app.RegisterCommandProvider(&LoadTestProvider{})
}
func (*LoadTestProvider) GetTrigger() string {
return CmdTest
}
func (*LoadTestProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
if !*a.Config().ServiceSettings.EnableTesting {
return nil
}
return &model.Command{
Trigger: CmdTest,
AutoComplete: false,
AutoCompleteDesc: "Debug Load Testing",
AutoCompleteHint: "help",
DisplayName: "test",
}
}
func (lt *LoadTestProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
commandResponse, err := lt.doCommand(a, c, args, message)
if err != nil {
c.Logger().Error("failed command /"+CmdTest, mlog.Err(err))
}
return commandResponse
}
func (lt *LoadTestProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
//This command is only available when EnableTesting is true
if !*a.Config().ServiceSettings.EnableTesting {
return &model.CommandResponse{}, nil
}
if strings.HasPrefix(message, "setup") {
return lt.SetupCommand(a, c, args, message)
}
if strings.HasPrefix(message, "users") {
return lt.UsersCommand(a, c, args, message)
}
if strings.HasPrefix(message, "activate_user") {
return lt.ActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "deactivate_user") {
return lt.DeActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "channels") {
return lt.ChannelsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "dms") {
return lt.DMsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "posts") {
return lt.PostsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "post") {
return lt.PostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "threaded_post") {
return lt.ThreadedPostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "url") {
return lt.URLCommand(a, c, args, message)
}
if strings.HasPrefix(message, "json") {
return lt.JsonCommand(a, c, args, message)
}
return lt.HelpCommand(args, message), nil
}
func (*LoadTestProvider) HelpCommand(args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{Text: usage, ResponseType: model.CommandResponseTypeEphemeral}
}
func (*LoadTestProvider) SetupCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
tokens := strings.Fields(strings.TrimPrefix(message, "setup"))
doTeams := contains(tokens, "teams")
doFuzz := contains(tokens, "fuzz")
numArgs := 0
if doTeams {
numArgs++
}
if doFuzz {
numArgs++
}
var numTeams int
var numChannels int
var numUsers int
var numPosts int
// Defaults
numTeams = 10
numChannels = 10
numUsers = 10
numPosts = 10
if doTeams {
if (len(tokens) - numArgs) >= 4 {
numTeams, _ = strconv.Atoi(tokens[numArgs+0])
numChannels, _ = strconv.Atoi(tokens[numArgs+1])
numUsers, _ = strconv.Atoi(tokens[numArgs+2])
numPosts, _ = strconv.Atoi(tokens[numArgs+3])
}
} else {
if (len(tokens) - numArgs) >= 3 {
numChannels, _ = strconv.Atoi(tokens[numArgs+0])
numUsers, _ = strconv.Atoi(tokens[numArgs+1])
numPosts, _ = strconv.Atoi(tokens[numArgs+2])
}
}
client := model.NewAPIv4Client(args.SiteURL)
if doTeams {
if err := CreateBasicUser(a, client); err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
_, _, err := client.Login(BTestUserEmail, BTestUserPassword)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
environment, err := CreateTestEnvironmentWithTeams(
a,
c,
client,
utils.Range{Begin: numTeams, End: numTeams},
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
c.Logger().Info("Testing environment created")
for i := 0; i < len(environment.Teams); i++ {
c.Logger().Info("Team Created: " + environment.Teams[i].Name)
c.Logger().Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + UserPassword)
}
} else {
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
CreateTestEnvironmentInTeam(
a,
c,
client,
team,
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
}
return &model.CommandResponse{Text: "Created environment", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "activate_user"))
if err := a.UpdateUserActive(c, user_id, true); err != nil {
return &model.CommandResponse{Text: "Failed to activate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Activated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DeActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "deactivate_user"))
if err := a.UpdateUserActive(c, user_id, false); err != nil {
return &model.CommandResponse{Text: "Failed to deactivate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "DeActivated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) UsersCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "users"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users", ResponseType: model.CommandResponseTypeEphemeral}, err
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add users: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
client := model.NewAPIv4Client(args.SiteURL)
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = doFuzz
userCreator.JoinTime = time
if _, err := userCreator.CreateTestUsers(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added users", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ChannelsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels", ResponseType: model.CommandResponseTypeEphemeral}, err
}
typ := model.ChannelTypeOpen
typeParam := getMatch(typeRE, cmd)
if typeParam != "" {
switch strings.ToUpper(typeParam) {
case "O":
case "P":
typ = model.ChannelTypePrivate
default:
return &model.CommandResponse{Text: "Failed to add channels: Invalid type parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid type parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add channels: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channelCreator := NewAutoChannelCreator(a, team, args.UserId)
channelCreator.Fuzzy = doFuzz
channelCreator.CreateTime = time
channelCreator.ChannelType = typ
if _, err := channelCreator.CreateTestChannels(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to create test channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added channels", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DMsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "dms"))
var err error
username := getMatch(userRE, message)
user, appErr := a.GetUserByUsername(username)
if appErr != nil {
return &model.CommandResponse{Text: "Failed to add DMS: Invalid username", ResponseType: model.CommandResponseTypeEphemeral}, appErr
}
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add DMs: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channel, err := a.GetOrCreateDirectChannel(c, args.UserId, user.Id)
postCreator := NewAutoPostCreator(a, channel.Id, args.UserId)
postCreator.CreateTime = time
postCreator.UsersToPostFrom = []string{user.Id}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
if _, err := postCreator.CreateRandomPost(c); err != nil {
return &model.CommandResponse{Text: "Failed to create test DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added DMs", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ThreadedPostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "threaded_post"))
var err error
rng := utils.Range{Begin: 1000, End: 1000}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to create post: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to create post: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = true
testPoster.Users = usernames
testPoster.CreateTime = time
rpost, err2 := testPoster.CreateRandomPost(c)
if err2 != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, err2
}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.CreateRandomPostNested(c, rpost.Id)
}
return &model.CommandResponse{Text: "Added threaded post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) PostsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
maxImages := 0
imagesParam := getMatch(imagesRE, cmd)
if imagesParam != "" {
maxImages, err = strconv.Atoi(imagesParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: Invalid images parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid images parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add posts: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = doFuzz
testPoster.Users = usernames
testPoster.CreateTime = time
numImages := utils.RandIntFromRange(utils.Range{Begin: 0, End: maxImages})
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.HasImage = (i < numImages)
_, err := testPoster.CreateRandomPost(c)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added posts", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func getMatch(re *regexp.Regexp, text string) string {
if match := re.FindStringSubmatch(text); match != nil {
return match[1]
}
return ""
}
func (*LoadTestProvider) PostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
textMessage := getMatch(messageRE, message)
if textMessage == "" {
return &model.CommandResponse{Text: "No message to post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
teamName := getMatch(teamRE, message)
team, err := a.GetTeamByName(teamName)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a team", ResponseType: model.CommandResponseTypeEphemeral}, err
}
channelName := getMatch(channelRE, message)
channel, err := a.GetChannelByName(c, channelName, team.Id, true)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a channel", ResponseType: model.CommandResponseTypeEphemeral}, err
}
passwd := getMatch(passwdRE, message)
username := getMatch(userRE, message)
user, err := a.GetUserByUsername(username)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
client := model.NewAPIv4Client(args.SiteURL)
_, _, nErr := client.LoginById(user.Id, passwd)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to login a user", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
post := &model.Post{
ChannelId: channel.Id,
Message: textMessage,
}
_, _, nErr = client.CreatePost(post)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
return &model.CommandResponse{Text: "Added a post to " + channel.DisplayName, ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) URLCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "url"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".md"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
bytes := make([]byte, 4000)
// break contents into 4000 byte posts
for {
length, err := r.Body.Read(bytes)
if err != nil && err != io.EOF {
return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if length == 0 {
break
}
post := &model.Post{}
post.Message = string(bytes[:length])
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) JsonCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "json"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".json"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
return &model.CommandResponse{Text: "Unable to decode post", ResponseType: model.CommandResponseTypeEphemeral}, errors.Wrapf(jsonErr, "could not decode post from json")
}
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if post.Message == "" {
post.Message = message
}
if _, err := a.CreatePostMissingChannel(c, &post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func parseRange(rng string) (utils.Range, error) {
tokens := strings.Split(rng, ",")
var begin int
var end int
var err1 error
var err2 error
switch {
case len(tokens) == 1:
begin, err1 = strconv.Atoi(tokens[0])
if err1 != nil || begin < 0 {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
end = begin
case len(tokens) == 2:
begin, err1 = strconv.Atoi(tokens[0])
end, err2 = strconv.Atoi(tokens[1])
if err1 != nil || err2 != nil || begin < 0 || end < begin {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
default:
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
return utils.Range{Begin: begin, End: end}, nil
}
func contains(items []string, token string) bool {
for _, elem := range items {
if elem == token {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LogoutProvider struct {
}
const (
CmdLogout = "logout"
)
func init() {
app.RegisterCommandProvider(&LogoutProvider{})
}
func (*LogoutProvider) GetTrigger() string {
return CmdLogout
}
func (*LogoutProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLogout,
AutoComplete: true,
AutoCompleteDesc: T("api.command_logout.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_logout.name"),
}
}
func (*LogoutProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// Actual logout is handled client side.
return &model.CommandResponse{GotoLocation: "/login"}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MarketplaceProvider struct {
}
const (
CmdMarketplace = "marketplace"
)
func init() {
app.RegisterCommandProvider(&MarketplaceProvider{})
}
func (h *MarketplaceProvider) GetTrigger() string {
return CmdMarketplace
}
func (h *MarketplaceProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
enabled := false
pluginSettings := a.Config().PluginSettings
if *pluginSettings.Enable && *pluginSettings.EnableMarketplace {
enabled = true
}
return &model.Command{
Trigger: CmdMarketplace,
AutoComplete: enabled,
AutoCompleteDesc: T("api.command_marketplace.desc"),
DisplayName: T("api.command_marketplace.name"),
}
}
func (h *MarketplaceProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_marketplace.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MeProvider struct {
}
const (
CmdMe = "me"
)
func init() {
app.RegisterCommandProvider(&MeProvider{})
}
func (*MeProvider) GetTrigger() string {
return CmdMe
}
func (*MeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMe,
AutoComplete: true,
AutoCompleteDesc: T("api.command_me.desc"),
AutoCompleteHint: T("api.command_me.hint"),
DisplayName: T("api.command_me.name"),
}
}
func (*MeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeInChannel,
Type: model.PostTypeMe,
Text: "*" + message + "*",
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type msgProvider struct {
}
const (
CmdMsg = "msg"
)
func init() {
app.RegisterCommandProvider(&msgProvider{})
}
func (*msgProvider) GetTrigger() string {
return CmdMsg
}
func (*msgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_msg.desc"),
AutoCompleteHint: T("api.command_msg.hint"),
DisplayName: T("api.command_msg.name"),
}
}
func (*msgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
splitMessage := strings.SplitN(message, " ", 2)
parsedMessage := ""
targetUsername := ""
if len(splitMessage) > 1 {
parsedMessage = strings.SplitN(message, " ", 2)[1]
}
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if userProfile.Id == args.UserId {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, userProfile.Id)
if err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
// Find the channel based on this user
channelName := model.GetDMNameFromIds(args.UserId, userProfile.Id)
targetChannelId := ""
if channel, channelErr := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true); channelErr != nil {
var nfErr *store.ErrNotFound
if errors.As(channelErr, &nfErr) {
if !a.HasPermissionTo(args.UserId, model.PermissionCreateDirectChannel) {
return &model.CommandResponse{Text: args.T("api.command_msg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
var directChannel *model.Channel
if directChannel, err = a.GetOrCreateDirectChannel(c, args.UserId, userProfile.Id); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T(err.Id), ResponseType: model.CommandResponseTypeEphemeral}
}
targetChannelId = directChannel.Id
} else {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.dm_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
targetChannelId = channel.Id
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = targetChannelId
post.UserId = args.UserId
if _, err = a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channelName, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MuteProvider struct {
}
const (
CmdMute = "mute"
)
func init() {
app.RegisterCommandProvider(&MuteProvider{})
}
func (*MuteProvider) GetTrigger() string {
return CmdMute
}
func (*MuteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMute,
AutoComplete: true,
AutoCompleteDesc: T("api.command_mute.desc"),
AutoCompleteHint: T("api.command_mute.hint"),
DisplayName: T("api.command_mute.name"),
}
}
func (*MuteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.no_channel.error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channelName := ""
splitMessage := strings.Split(message, " ")
// Overwrite channel with channel-handle if set
if strings.HasPrefix(message, "~") {
channelName = splitMessage[0][1:]
} else {
channelName = splitMessage[0]
}
if channelName != "" && message != "" {
channel, _ = a.Srv().Store().Channel().GetByName(channel.TeamId, channelName, true)
if channel == nil {
return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
}
channelMember, err := a.ToggleMuteChannel(c, channel.Id, args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
// Direct and Group messages won't have a nice channel title, omit it
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OfflineProvider struct {
}
const (
CmdOffline = "offline"
)
func init() {
app.RegisterCommandProvider(&OfflineProvider{})
}
func (*OfflineProvider) GetTrigger() string {
return CmdOffline
}
func (*OfflineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOffline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_offline.desc"),
DisplayName: T("api.command_offline.name"),
}
}
func (*OfflineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOffline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_offline.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OnlineProvider struct {
}
const (
CmdOnline = "online"
)
func init() {
app.RegisterCommandProvider(&OnlineProvider{})
}
func (*OnlineProvider) GetTrigger() string {
return CmdOnline
}
func (*OnlineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOnline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_online.desc"),
DisplayName: T("api.command_online.name"),
}
}
func (*OnlineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOnline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_online.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OpenProvider struct {
JoinProvider
}
const (
CmdOpen = "open"
)
func init() {
app.RegisterCommandProvider(&OpenProvider{})
}
func (open *OpenProvider) GetTrigger() string {
return CmdOpen
}
func (open *OpenProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
cmd := open.JoinProvider.GetCommand(a, T)
cmd.Trigger = CmdOpen
cmd.DisplayName = T("api.command_open.name")
return cmd
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
AvailableRemoteActions = "create, accept, remove, status"
)
type RemoteProvider struct {
}
const (
CommandTriggerRemote = "secure-connection"
)
func init() {
app.RegisterCommandProvider(&RemoteProvider{})
}
func (rp *RemoteProvider) GetTrigger() string {
return CommandTriggerRemote
}
func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]any{"Actions": AvailableRemoteActions}))
create := model.NewAutocompleteData("create", "", T("api.command_remote.invite.help"))
create.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
create.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
create.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
accept.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)
remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
remove.AddNamedDynamicListArgument("connectionID", T("api.command_remote.remove_remote_id.help"), "builtin:"+CommandTriggerRemote, true)
status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))
remote.AddCommand(create)
remote.AddCommand(accept)
remote.AddCommand(remove)
remote.AddCommand(status)
return &model.Command{
Trigger: rp.GetTrigger(),
AutoComplete: true,
AutoCompleteDesc: T("api.command_remote.desc"),
AutoCompleteHint: T("api.command_remote.hint"),
DisplayName: T("api.command_remote.name"),
AutocompleteData: remote,
}
}
func (rp *RemoteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSecureConnections) {
return responsef(args.T("api.command_remote.permission_required", map[string]any{"Permission": "manage_secure_connections"}))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return responsef(args.T("api.command_remote.missing_command", map[string]any{"Actions": AvailableRemoteActions}))
}
switch action {
case "create":
return rp.doCreate(a, args, margs)
case "accept":
return rp.doAccept(a, args, margs)
case "remove":
return rp.doRemove(a, args, margs)
case "status":
return rp.doStatus(a, args, margs)
}
return responsef(args.T("api.command_remote.unknown_action", map[string]any{"Action": action}))
}
func (rp *RemoteProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
if !a.HasPermissionTo(commandArgs.UserId, model.PermissionManageSecureConnections) {
return nil, errors.New("You require `manage_secure_connections` permission to manage secure connections.")
}
if arg.Name == "connectionID" && strings.Contains(parsed, " remove ") {
return getRemoteClusterAutocompleteListItems(a, true)
}
return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
}
// doCreate creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
url := a.GetSiteURL()
if url == "" {
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc := &model.RemoteCluster{
Name: name,
DisplayName: displayname,
Token: model.NewId(),
CreatorId: args.UserId,
}
rcSaved, appErr := a.AddRemoteCluster(rc)
if appErr != nil {
return responsef(args.T("api.command_remote.add_remote.error", map[string]any{"Error": appErr.Error()}))
}
// Display the encrypted invitation
invite := &model.RemoteClusterInvite{
RemoteId: rcSaved.RemoteId,
RemoteTeamId: args.TeamId,
SiteURL: url,
Token: rcSaved.Token,
}
encrypted, err := invite.Encrypt(password)
if err != nil {
return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
}
encoded := base64.URLEncoding.EncodeToString(encrypted)
return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
}
// doAccept accepts an invitation generated by a remote site.
func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
blob := margs["invite"]
if blob == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "invite"}))
}
// invite is encoded as base64 and encrypted
decoded, err := base64.URLEncoding.DecodeString(blob)
if err != nil {
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": err.Error()}))
}
invite := &model.RemoteClusterInvite{}
err = invite.Decrypt(decoded, password)
if err != nil {
return responsef(args.T("api.command_remote.incorrect_password.error", map[string]any{"Error": err.Error()}))
}
rcs, _ := a.GetRemoteClusterService()
if rcs == nil {
return responsef(args.T("api.command_remote.service_not_enabled"))
}
url := a.GetSiteURL()
if url == "" {
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
if err != nil {
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]any{"SiteURL": rc.SiteURL}))
}
// doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
id, ok := margs["connectionID"]
if !ok {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "remoteId"}))
}
deleted, err := a.DeleteRemoteCluster(id)
if err != nil {
responsef(args.T("api.command_remote.remove_remote.error", map[string]any{"Error": err.Error()}))
}
result := "removed"
if !deleted {
result = "**NOT FOUND**"
}
return responsef("##### " + args.T("api.command_remote.cluster_removed", map[string]any{"RemoteId": id, "Result": result}))
}
// doStatus displays connection status for all remote clusters.
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
if err != nil {
responsef(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
}
if len(list) == 0 {
return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+" \n")
// | Secure Connection | Display name | ConnectionID | Site URL | Invite accepted | Online | Last ping |
fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | \n")
for _, rc := range list {
accepted := formatBool(args.T, rc.SiteURL != "")
online := formatBool(args.T, isOnline(rc.LastPingAt))
lastPing := formatTimestamp(rc.LastPingAt)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.SiteURL, accepted, online, lastPing)
}
return responsef(sb.String())
}
func isOnline(lastPing int64) bool {
return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
}
func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
}
clusters, err := a.GetAllRemoteClusters(filter)
if err != nil || len(clusters) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(clusters))
for _, rc := range clusters {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
list = append(list, item)
}
return list, nil
}
func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelId string, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
NotInChannel: channelId,
}
all, err := a.GetAllRemoteClusters(filter)
if err != nil || len(all) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(all))
for _, rc := range all {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
list = append(list, item)
}
return list, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RemoveProvider struct {
}
type KickProvider struct {
}
const (
CmdRemove = "remove"
CmdKick = "kick"
)
func init() {
app.RegisterCommandProvider(&RemoveProvider{})
app.RegisterCommandProvider(&KickProvider{})
}
func (*RemoveProvider) GetTrigger() string {
return CmdRemove
}
func (*KickProvider) GetTrigger() string {
return CmdKick
}
func (*RemoveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdRemove,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_remove.name"),
}
}
func (*KickProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdKick,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_kick.name"),
}
}
func (*RemoveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func (*KickProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_remove.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_remove.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_remove.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
targetUsername := ""
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if userProfile.DeleteAt != 0 {
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
_, err = a.GetChannelMember(c, args.ChannelId, userProfile.Id)
if err != nil {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
return &model.CommandResponse{
Text: args.T("api.command_remove.user_not_in_channel", map[string]any{
"Username": userProfile.GetDisplayName(nameFormat),
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if err = a.RemoveUserFromChannel(c, userProfile.Id, args.UserId, channel); err != nil {
var text string
if err.Id == "api.channel.remove_members.denied" {
text = args.T("api.command_remove.group_constrained_user_denied")
} else {
text = args.T(err.Id, map[string]any{
"Channel": model.DefaultChannelName,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SearchProvider struct {
}
const (
CmdSearch = "search"
)
func init() {
app.RegisterCommandProvider(&SearchProvider{})
}
func (search *SearchProvider) GetTrigger() string {
return CmdSearch
}
func (search *SearchProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSearch,
AutoComplete: true,
AutoCompleteDesc: T("api.command_search.desc"),
AutoCompleteHint: T("api.command_search.hint"),
DisplayName: T("api.command_search.name"),
}
}
func (search *SearchProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_search.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SettingsProvider struct {
}
const (
CmdSettings = "settings"
)
func init() {
app.RegisterCommandProvider(&SettingsProvider{})
}
func (settings *SettingsProvider) GetTrigger() string {
return CmdSettings
}
func (settings *SettingsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSettings,
AutoComplete: true,
AutoCompleteDesc: T("api.command_settings.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_settings.name"),
}
}
func (settings *SettingsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_settings.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShareProvider struct {
}
const (
CommandTriggerShare = "share-channel"
AvailableShareActions = "invite, uninvite, unshare, status"
)
func init() {
app.RegisterCommandProvider(&ShareProvider{})
}
func (sp *ShareProvider) GetTrigger() string {
return CommandTriggerShare
}
func (sp *ShareProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
share := model.NewAutocompleteData(CommandTriggerShare, "[action]", T("api.command_share.available_actions", map[string]any{"Actions": AvailableShareActions}))
inviteRemote := model.NewAutocompleteData("invite", "", T("api.command_share.invite_remote.help"))
inviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.remote_id.help"), "builtin:"+CommandTriggerShare, true)
inviteRemote.AddNamedTextArgument("readonly", T("api.command_share.share_read_only.help"), T("api.command_share.share_read_only.hint"), "Y|N|y|n", false)
unInviteRemote := model.NewAutocompleteData("uninvite", "", T("api.command_share.uninvite_remote.help"))
unInviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.uninvite_remote_id.help"), "builtin:"+CommandTriggerShare, true)
unshareChannel := model.NewAutocompleteData("unshare", "", T("api.command_share.unshare_channel.help"))
status := model.NewAutocompleteData("status", "", T("api.command_share.channel_status.help"))
share.AddCommand(inviteRemote)
share.AddCommand(unInviteRemote)
share.AddCommand(unshareChannel)
share.AddCommand(status)
return &model.Command{
Trigger: CommandTriggerShare,
AutoComplete: true,
AutoCompleteDesc: T("api.command_share.desc"),
AutoCompleteHint: T("api.command_share.hint"),
DisplayName: T("api.command_share.name"),
AutocompleteData: share,
}
}
func (sp *ShareProvider) GetAutoCompleteListItems(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
switch {
case strings.Contains(parsed, " share "):
return sp.getAutoCompleteShareChannel(c, a, commandArgs, arg)
case strings.Contains(parsed, " invite "):
return sp.getAutoCompleteInviteRemote(a, commandArgs, arg)
case strings.Contains(parsed, " uninvite "):
return sp.getAutoCompleteUnInviteRemote(a, commandArgs, arg)
}
return nil, errors.New("invalid action")
}
func (sp *ShareProvider) getAutoCompleteShareChannel(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
channel, err := a.GetChannel(c, commandArgs.ChannelId)
if err != nil {
return nil, err
}
var item model.AutocompleteListItem
switch arg.Name {
case "name":
item = model.AutocompleteListItem{
Item: channel.Name,
HelpText: channel.DisplayName,
}
case "displayname":
item = model.AutocompleteListItem{
Item: channel.DisplayName,
HelpText: channel.Name,
}
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
return []model.AutocompleteListItem{item}, nil
}
func (sp *ShareProvider) getAutoCompleteInviteRemote(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
switch arg.Name {
case "connectionID":
return getRemoteClusterAutocompleteListItemsNotInChannel(a, commandArgs.ChannelId, true)
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
}
func (sp *ShareProvider) getAutoCompleteUnInviteRemote(a *app.App, _ *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
switch arg.Name {
case "connectionID":
return getRemoteClusterAutocompleteListItems(a, true)
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
}
func (sp *ShareProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSharedChannels) {
return responsef(args.T("api.command_share.permission_required", map[string]any{"Permission": "manage_shared_channels"}))
}
if a.Srv().GetSharedChannelSyncService() == nil {
return responsef(args.T("api.command_share.service_disabled"))
}
if a.Srv().GetRemoteClusterService() == nil {
return responsef(args.T("api.command_remote.service_disabled"))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return responsef(args.T("api.command_share.missing_action", map[string]any{"Actions": AvailableShareActions}))
}
switch action {
case "share":
return sp.doShareChannel(a, c, args, margs)
case "unshare":
return sp.doUnshareChannel(a, args, margs)
case "invite":
return sp.doInviteRemote(a, c, args, margs)
case "uninvite":
return sp.doUninviteRemote(a, args, margs)
case "status":
return sp.doStatus(a, args, margs)
}
return responsef(args.T("api.command_share.unknown_action", map[string]any{"Action": action, "Actions": AvailableShareActions}))
}
func (sp *ShareProvider) doShareChannel(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
// check that channel exists.
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": errApp.Error()}))
}
if name := margs["name"]; name == "" {
margs["name"] = channel.Name
}
if name := margs["displayname"]; name == "" {
margs["displayname"] = channel.DisplayName
}
if name := margs["purpose"]; name == "" {
margs["purpose"] = channel.Purpose
}
if name := margs["header"]; name == "" {
margs["header"] = channel.Header
}
if _, ok := margs["readonly"]; !ok {
margs["readonly"] = "N"
}
readonly, err := parseBool(margs["readonly"])
if err != nil {
return responsef(args.T("api.command_share.invalid_value.error", map[string]any{"Arg": "readonly", "Error": err.Error()}))
}
sc := &model.SharedChannel{
ChannelId: args.ChannelId,
TeamId: args.TeamId,
Home: true,
ReadOnly: readonly,
ShareName: margs["name"],
ShareDisplayName: margs["displayname"],
SharePurpose: margs["purpose"],
ShareHeader: margs["header"],
CreatorId: args.UserId,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": err.Error()}))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.channel_shared"))
}
func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
sc, appErr := a.GetSharedChannel(args.ChannelId)
if appErr != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": appErr.Error()}))
}
deleted, err := a.DeleteSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": err.Error()}))
}
if !deleted {
return responsef(args.T("api.command_share.not_shared_channel_unshare"))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.shared_channel_unavailable"))
}
func (sp *ShareProvider) doInviteRemote(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) (resp *model.CommandResponse) {
remoteId, ok := margs["connectionID"]
if !ok || remoteId == "" {
return responsef(args.T("api.command_share.must_specify_valid_remote"))
}
hasRemote, err := a.HasRemote(args.ChannelId, remoteId)
if err != nil {
return responsef(args.T("api.command_share.fetch_remote.error", map[string]any{"Error": err.Error()}))
}
if hasRemote {
return responsef(args.T("api.command_share.remote_already_invited"))
}
// Check if channel is shared or not.
hasChan, err := a.HasSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.check_channel_exist.error", map[string]any{"Error": err.Error()}))
}
if !hasChan {
// If it doesn't exist, then create it.
resp2 := sp.doShareChannel(a, c, args, margs)
// We modify the outgoing response by prepending the text
// from the shareChannel response.
defer func() {
resp.Text = resp2.Text + "\n" + resp.Text
}()
}
// don't allow invitation to shared channel originating from remote.
// (also blocks cyclic invitations)
if err := a.CheckCanInviteToSharedChannel(args.ChannelId); err != nil {
return responsef(args.T("api.command_share.channel_invite_not_home.error"))
}
rc, appErr := a.GetRemoteCluster(remoteId)
if appErr != nil {
return responsef(args.T("api.command_share.remote_id_invalid.error", map[string]any{"Error": appErr.Error()}))
}
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": errApp.Error()}))
}
// send channel invite to remote cluster
if err := a.Srv().GetSharedChannelSyncService().SendChannelInvite(channel, args.UserId, rc); err != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_share.invitation_sent", map[string]any{"Name": rc.DisplayName, "SiteURL": rc.SiteURL}))
}
func (sp *ShareProvider) doUninviteRemote(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
remoteId, ok := margs["connectionID"]
if !ok || remoteId == "" {
return responsef(args.T("api.command_share.remote_not_valid"))
}
scr, err := a.GetSharedChannelRemoteByIds(args.ChannelId, remoteId)
if err != nil || scr.ChannelId != args.ChannelId {
return responsef(args.T("api.command_share.channel_remote_id_not_exists", map[string]any{"RemoteId": remoteId}))
}
deleted, err := a.DeleteSharedChannelRemote(scr.Id)
if err != nil || !deleted {
return responsef(args.T("api.command_share.could_not_uninvite.error", map[string]any{"RemoteId": remoteId, "Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_share.remote_uninvited", map[string]any{"RemoteId": remoteId}))
}
func (sp *ShareProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
statuses, err := a.GetSharedChannelRemotesStatus(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.fetch_remote_status.error", map[string]any{"Error": err.Error()}))
}
if len(statuses) == 0 {
return responsef(args.T("api.command_share.no_remote_invited"))
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_share.channel_status_id", map[string]any{"ChannelId": statuses[0].ChannelId})+"\n\n")
fmt.Fprintf(&sb, args.T("api.command_share.remote_table_header")+" \n")
// "| Secure Connection | SiteURL | ReadOnly | InviteAccepted | Online | Last Sync |"
fmt.Fprintf(&sb, "| ---- | ---- | ---- | ---- | ---- | ---- | \n")
for _, status := range statuses {
readonly := formatBool(args.T, status.ReadOnly)
accepted := formatBool(args.T, status.IsInviteAccepted)
online := formatBool(args.T, isOnline(status.LastPingAt))
lastSync := formatTimestamp(status.NextSyncAt)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s |\n",
status.DisplayName, status.SiteURL, readonly, accepted, online, lastSync)
}
return responsef(sb.String())
}
func notifyClientsForChannelUpdate(a *app.App, sharedChannel *model.SharedChannel) {
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, sharedChannel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", sharedChannel.ChannelId)
a.Publish(messageWs)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShortcutsProvider struct {
}
const (
CmdShortcuts = "shortcuts"
)
func init() {
app.RegisterCommandProvider(&ShortcutsProvider{})
}
func (*ShortcutsProvider) GetTrigger() string {
return CmdShortcuts
}
func (*ShortcutsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShortcuts,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shortcuts.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_shortcuts.name"),
}
}
func (*ShortcutsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_shortcuts.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShrugProvider struct {
}
const (
CmdShrug = "shrug"
)
func init() {
app.RegisterCommandProvider(&ShrugProvider{})
}
func (*ShrugProvider) GetTrigger() string {
return CmdShrug
}
func (*ShrugProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShrug,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shrug.desc"),
AutoCompleteHint: T("api.command_shrug.hint"),
DisplayName: T("api.command_shrug.name"),
}
}
func (*ShrugProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
rmsg := `¯\\\_(ツ)\_/¯`
if message != "" {
rmsg = message + " " + rmsg
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type TemplatesProvider struct {
}
const (
CmdTemplates = "templates"
)
func init() {
app.RegisterCommandProvider(&TemplatesProvider{})
}
func (h *TemplatesProvider) GetTrigger() string {
return CmdTemplates
}
func (h *TemplatesProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
workTemplateEnabled := a.Config().FeatureFlags.WorkTemplate
pbActive, err := a.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
pbActive = false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
hasBoard = false
}
return &model.Command{
Trigger: CmdTemplates,
AutoComplete: hasBoard && pbActive && workTemplateEnabled,
AutoCompleteDesc: T("api.command_templates.desc"),
DisplayName: T("api.command_templates.name"),
}
}
func (h *TemplatesProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_templates.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
ActionKey = "-action"
)
// responsef creates an ephemeral command response using printf syntax.
func responsef(format string, args ...any) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: fmt.Sprintf(format, args...),
Type: model.PostTypeDefault,
}
}
// parseNamedArgs parses a command string into a map of arguments. It is assumed the
// command string is of the form `<action> --arg1 value1 ...` Supports empty values.
// Arg names are limited to [0-9a-zA-Z_].
func parseNamedArgs(cmd string) map[string]string {
m := make(map[string]string)
split := strings.Fields(cmd)
// check for optional action
if len(split) >= 2 && !strings.HasPrefix(split[1], "--") {
m[ActionKey] = split[1] // prefix with hyphen to avoid collision with arg named "action"
}
for i := 0; i < len(split); i++ {
if !strings.HasPrefix(split[i], "--") {
continue
}
var val string
arg := trimSpaceAndQuotes(strings.Trim(split[i], "-"))
if i < len(split)-1 && !strings.HasPrefix(split[i+1], "--") {
val = trimSpaceAndQuotes(split[i+1])
}
if arg != "" {
m[arg] = val
}
}
return m
}
func trimSpaceAndQuotes(s string) string {
trimmed := strings.TrimSpace(s)
trimmed = strings.TrimPrefix(trimmed, "\"")
trimmed = strings.TrimPrefix(trimmed, "'")
trimmed = strings.TrimSuffix(trimmed, "\"")
trimmed = strings.TrimSuffix(trimmed, "'")
return trimmed
}
func parseBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "1", "t", "true", "yes", "y":
return true, nil
case "0", "f", "false", "no", "n":
return false, nil
}
return false, fmt.Errorf("cannot parse '%s' as a boolean", s)
}
func formatBool(fn i18n.TranslateFunc, b bool) string {
if b {
return fn("True")
}
return fn("False")
}
func formatTimestamp(timestamp int64) string {
if timestamp == 0 {
return "--"
}
ts := model.GetTimeForMillis(timestamp)
if !isToday(ts) {
return ts.Format("Jan 2 15:04:05 MST 2006")
}
date := ts.Format("15:04:05 MST 2006")
return fmt.Sprintf("Today %s", date)
}
func isToday(ts time.Time) bool {
now := time.Now()
year, month, day := ts.Date()
nowYear, nowMonth, nowDay := now.Date()
return year == nowYear && month == nowMonth && day == nowDay
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GetUserStatusesByIds used by apiV4
func (a *App) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
return a.Srv().Platform().GetUserStatusesByIds(userIDs)
}
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
// while an 'away' device is still connected
func (a *App) SetStatusLastActivityAt(userID string, activityAt int64) {
a.Srv().Platform().SetStatusLastActivityAt(userID, activityAt)
}
func (a *App) SetStatusOnline(userID string, manual bool) {
a.Srv().Platform().SetStatusOnline(userID, manual)
}
func (a *App) SetStatusOffline(userID string, manual bool) {
a.Srv().Platform().SetStatusOffline(userID, manual)
}
func (a *App) SetStatusAwayIfNeeded(userID string, manual bool) {
a.Srv().Platform().SetStatusAwayIfNeeded(userID, manual)
}
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
// and sets status of given userId to dnd which will be restored back after endtime
func (a *App) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
a.Srv().Platform().SetStatusDoNotDisturbTimed(userId, endtime)
}
func (a *App) SetStatusDoNotDisturb(userID string) {
a.Srv().Platform().SetStatusDoNotDisturb(userID)
}
func (a *App) SetStatusOutOfOffice(userID string) {
a.Srv().Platform().SetStatusOutOfOffice(userID)
}
func (a *App) GetStatusFromCache(userID string) *model.Status {
return a.Srv().Platform().GetStatusFromCache(userID)
}
func (a *App) GetStatus(userID string) (*model.Status, *model.AppError) {
return a.Srv().Platform().GetStatus(userID)
}
// UpdateDNDStatusOfUsers is a recurring task which is started when server starts
// which unsets dnd status of users if needed and saves and broadcasts it
func (a *App) UpdateDNDStatusOfUsers() {
statuses, err := a.UpdateExpiredDNDStatuses()
if err != nil {
mlog.Warn("Failed to fetch dnd statues from store", mlog.String("err", err.Error()))
return
}
for i := range statuses {
a.Srv().Platform().AddStatusCache(statuses[i])
a.Srv().Platform().BroadcastStatus(statuses[i])
}
}
func (a *App) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError {
if cs == nil || (cs.Emoji == "" && cs.Text == "") {
return model.NewAppError("SetCustomStatus", "api.custom_status.set_custom_statuses.update.app_error", nil, "", http.StatusBadRequest)
}
user, err := a.GetUser(userID)
if err != nil {
return err
}
user.SetCustomStatus(cs)
_, updateErr := a.UpdateUser(c, user, true)
if updateErr != nil {
return updateErr
}
if err := a.addRecentCustomStatus(userID, cs); err != nil {
c.Logger().Error("Can't add recent custom status for", mlog.String("userID", userID), mlog.Err(err))
}
return nil
}
func (a *App) RemoveCustomStatus(c request.CTX, userID string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
user.ClearCustomStatus()
_, updateErr := a.UpdateUser(c, user, true)
if updateErr != nil {
return updateErr
}
return nil
}
func (a *App) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return &model.CustomStatus{}, err
}
return user.GetCustomStatus(), nil
}
func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
var newRCS model.RecentCustomStatuses
pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses)
if appErr != nil || pref.Value == "" {
newRCS = model.RecentCustomStatuses{*status}
} else {
var existingRCS model.RecentCustomStatuses
if err := json.Unmarshal([]byte(pref.Value), &existingRCS); err != nil {
return model.NewAppError("addRecentCustomStatus", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
newRCS = existingRCS.Add(status)
}
newRCSJSON, err := json.Marshal(newRCS)
if err != nil {
return model.NewAppError("addRecentCustomStatus", "api.marshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
pref = &model.Preference{
UserId: userID,
Category: model.PreferenceCategoryCustomStatus,
Name: model.PreferenceNameRecentCustomStatuses,
Value: string(newRCSJSON),
}
if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil {
return appErr
}
return nil
}
func (a *App) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses)
if appErr != nil {
return appErr
}
if pref.Value == "" {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest)
}
var existingRCS model.RecentCustomStatuses
if err := json.Unmarshal([]byte(pref.Value), &existingRCS); err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if ok, err := existingRCS.Contains(status); !ok || err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest)
}
newRCS, err := existingRCS.Remove(status)
if err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
newRCSJSON, err := json.Marshal(newRCS)
if err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.marshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
pref.Value = string(newRCSJSON)
if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil {
return appErr
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"os"
"runtime"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/config"
)
func (a *App) GenerateSupportPacket() []model.FileData {
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
var warnings []string
// Creating an array of files that we are going to be adding to our zip file
fileDatas := []model.FileData{}
// A array of the functions that we can iterate through since they all have the same return value
functions := []func() (*model.FileData, string){
a.generateSupportPacketYaml,
a.createPluginsFile,
a.createSanitizedConfigFile,
a.getMattermostLog,
a.getNotificationsLog,
}
for _, fn := range functions {
fileData, warning := fn()
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
} else {
warnings = append(warnings, warning)
}
}
// Adding a warning.txt file to the fileDatas if any warning
if len(warnings) > 0 {
finalWarning := strings.Join(warnings, "\n")
fileDatas = append(fileDatas, model.FileData{
Filename: "warning.txt",
Body: []byte(finalWarning),
})
}
return fileDatas
}
func (a *App) generateSupportPacketYaml() (*model.FileData, string) {
// Here we are getting information regarding Elastic Search
var elasticServerVersion string
var elasticServerPlugins []string
if a.Srv().Platform().SearchEngine.ElasticsearchEngine != nil {
elasticServerVersion = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetFullVersion()
elasticServerPlugins = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetPlugins()
}
// Here we are getting information regarding LDAP
ldapInterface := a.ch.Ldap
var vendorName, vendorVersion string
if ldapInterface != nil {
vendorName, vendorVersion = ldapInterface.GetVendorNameAndVendorVersion()
}
// Here we are getting information regarding the database (mysql/postgres + current schema version)
databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion()
databaseVersion, _ := a.Srv().Store().GetDbVersion(false)
uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, errors.Wrap(err, "error while getting user count").Error()
}
analytics, err := a.GetAnalytics("standard", "")
if analytics == nil {
return nil, errors.Wrap(err, "error while getting analytics").Error()
}
elasticPostIndexing, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_indexing", 0, 2)
elasticPostAggregation, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_aggregation", 0, 2)
ldapSyncJobs, _ := a.Srv().Store().Job().GetAllByTypePage("ldap_sync", 0, 2)
messageExport, _ := a.Srv().Store().Job().GetAllByTypePage("message_export", 0, 2)
dataRetentionJobs, _ := a.Srv().Store().Job().GetAllByTypePage("data_retention", 0, 2)
complianceJobs, _ := a.Srv().Store().Job().GetAllByTypePage("compliance", 0, 2)
migrationJobs, _ := a.Srv().Store().Job().GetAllByTypePage("migrations", 0, 2)
licenseTo := ""
supportedUsers := 0
if license := a.Srv().License(); license != nil {
supportedUsers = *license.Features.Users
licenseTo = license.Customer.Company
}
// Creating the struct for support packet yaml file
supportPacket := model.SupportPacket{
LicenseTo: licenseTo,
ServerOS: runtime.GOOS,
ServerArchitecture: runtime.GOARCH,
ServerVersion: model.CurrentVersion,
BuildHash: model.BuildHash,
DatabaseType: databaseType,
DatabaseVersion: databaseVersion,
DatabaseSchemaVersion: databaseSchemaVersion,
LdapVendorName: vendorName,
LdapVendorVersion: vendorVersion,
ElasticServerVersion: elasticServerVersion,
ElasticServerPlugins: elasticServerPlugins,
ActiveUsers: int(uniqueUserCount),
LicenseSupportedUsers: supportedUsers,
TotalChannels: int(analytics[0].Value) + int(analytics[1].Value),
TotalPosts: int(analytics[2].Value),
TotalTeams: int(analytics[4].Value),
WebsocketConnections: int(analytics[5].Value),
MasterDbConnections: int(analytics[6].Value),
ReplicaDbConnections: int(analytics[7].Value),
DailyActiveUsers: int(analytics[8].Value),
MonthlyActiveUsers: int(analytics[9].Value),
InactiveUserCount: int(analytics[10].Value),
ElasticPostIndexingJobs: elasticPostIndexing,
ElasticPostAggregationJobs: elasticPostAggregation,
LdapSyncJobs: ldapSyncJobs,
MessageExportJobs: messageExport,
DataRetentionJobs: dataRetentionJobs,
ComplianceJobs: complianceJobs,
MigrationJobs: migrationJobs,
}
// Marshal to a Yaml File
supportPacketYaml, err := yaml.Marshal(&supportPacket)
if err == nil {
fileData := model.FileData{
Filename: "support_packet.yaml",
Body: supportPacketYaml,
}
return &fileData, ""
}
warning := fmt.Sprintf("yaml.Marshal(&supportPacket) Error: %s", err.Error())
return nil, warning
}
func (a *App) createPluginsFile() (*model.FileData, string) {
var warning string
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
pluginsResponse, appErr := a.GetPlugins()
if appErr == nil {
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
if err == nil {
fileData := model.FileData{
Filename: "plugins.json",
Body: pluginsPrettyJSON,
}
return &fileData, ""
}
warning = fmt.Sprintf("json.MarshalIndent(pluginsResponse) Error: %s", err.Error())
} else {
warning = fmt.Sprintf("c.App.GetPlugins() Error: %s", appErr.Error())
}
return nil, warning
}
func (a *App) getNotificationsLog() (*model.FileData, string) {
var warning string
// Getting notifications.log
if *a.Config().NotificationLogSettings.EnableFile {
// notifications.log
notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation)
notificationsLogFileData, notificationsLogFileDataErr := os.ReadFile(notificationsLog)
if notificationsLogFileDataErr == nil {
fileData := model.FileData{
Filename: "notifications.log",
Body: notificationsLogFileData,
}
return &fileData, ""
}
warning = fmt.Sprintf("os.ReadFile(notificationsLog) Error: %s", notificationsLogFileDataErr.Error())
} else {
warning = "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json"
}
return nil, warning
}
func (a *App) getMattermostLog() (*model.FileData, string) {
var warning string
// Getting mattermost.log
if *a.Config().LogSettings.EnableFile {
// mattermost.log
mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation)
mattermostLogFileData, mattermostLogFileDataErr := os.ReadFile(mattermostLog)
if mattermostLogFileDataErr == nil {
fileData := model.FileData{
Filename: "mattermost.log",
Body: mattermostLogFileData,
}
return &fileData, ""
}
warning = fmt.Sprintf("os.ReadFile(mattermostLog) Error: %s", mattermostLogFileDataErr.Error())
} else {
warning = "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json"
}
return nil, warning
}
func (a *App) createSanitizedConfigFile() (*model.FileData, string) {
// Getting sanitized config, prettifying it, and then adding it to our file data array
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
if err == nil {
fileData := model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
}
return &fileData, ""
}
warning := fmt.Sprintf("json.MarshalIndent(c.App.GetSanitizedConfig()) Error: %s", err.Error())
return nil, warning
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// createDefaultChannelMemberships adds users to channels based on their group memberships and how those groups are
// configured to sync with channels for group members on or after the given timestamp. If a channelID is given
// only that channel's members are created. If channelID is nil all channel memberships are created.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultChannelMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
channelMembers, appErr := a.ChannelMembersToAdd(params.Since, params.ScopedChannelID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userChannel := range channelMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userChannel.UserID {
continue
}
channel, err := a.GetChannel(c, userChannel.ChannelID)
if err != nil {
return err
}
tmem, err := a.GetTeamMember(channel.TeamId, userChannel.UserID)
if err != nil && err.Id != "app.team.get_member.missing.app_error" {
return err
}
// First add user to team
if tmem == nil {
_, err = a.AddTeamMember(c, channel.TeamId, userChannel.UserID)
if err != nil {
if err.Id == "api.team.join_user_to_team.allowed_domains.app_error" {
c.Logger().Info("User not added to channel - the domain associated with the user is not in the list of allowed team domains",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
mlog.String("team_id", channel.TeamId),
)
continue
}
return err
}
c.Logger().Info("added teammember",
mlog.String("user_id", userChannel.UserID),
mlog.String("team_id", channel.TeamId),
)
}
_, err = a.AddChannelMember(c, userChannel.UserID, channel, ChannelMemberOpts{
SkipTeamMemberIntegrityCheck: true,
})
if err != nil {
if err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" {
c.Logger().Info("Not adding user to channel because they have already left the team",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
)
} else {
return err
}
}
c.Logger().Info("added channelmember",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
)
}
return nil
}
// createDefaultTeamMemberships adds users to teams based on their group memberships and how those groups are
// configured to sync with teams for group members on or after the given timestamp. If a teamID is given
// only that team's members are created. If teamID is nil all team memberships are created.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultTeamMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
teamMembers, appErr := a.TeamMembersToAdd(params.Since, params.ScopedTeamID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userTeam := range teamMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userTeam.UserID {
continue
}
_, err := a.AddTeamMember(c, userTeam.TeamID, userTeam.UserID)
if err != nil {
if err.Id == "api.team.join_user_to_team.allowed_domains.app_error" {
c.Logger().Info("User not added to team - the domain associated with the user is not in the list of allowed team domains",
mlog.String("user_id", userTeam.UserID),
mlog.String("team_id", userTeam.TeamID),
)
continue
}
return err
}
c.Logger().Info("added teammember",
mlog.String("user_id", userTeam.UserID),
mlog.String("team_id", userTeam.TeamID),
)
}
return nil
}
// CreateDefaultMemberships adds users to teams and channels based on their group memberships and how those groups
// are configured to sync with teams and channels for group members on or after the given timestamp.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
err := a.createDefaultTeamMemberships(c, params)
if err != nil {
return err
}
err = a.createDefaultChannelMemberships(c, params)
if err != nil {
return err
}
return nil
}
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
// groups of all group-constrained teams and channels.
func (a *App) DeleteGroupConstrainedMemberships(c *request.Context) error {
err := a.deleteGroupConstrainedChannelMemberships(c, nil)
if err != nil {
return err
}
err = a.deleteGroupConstrainedTeamMemberships(c, nil)
if err != nil {
return err
}
return nil
}
// deleteGroupConstrainedTeamMemberships deletes team memberships of users who aren't members of the allowed
// groups of the given group-constrained team. If a teamID is given then the procedure is scoped to the given team,
// if teamID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedTeamMemberships(c request.CTX, teamID *string) error {
teamMembers, appErr := a.TeamMembersToRemove(teamID)
if appErr != nil {
return appErr
}
for _, userTeam := range teamMembers {
err := a.RemoveUserFromTeam(c, userTeam.TeamId, userTeam.UserId, "")
if err != nil {
return err
}
c.Logger().Info("removed teammember",
mlog.String("user_id", userTeam.UserId),
mlog.String("team_id", userTeam.TeamId),
)
}
return nil
}
// deleteGroupConstrainedChannelMemberships deletes channel memberships of users who aren't members of the allowed
// groups of the given group-constrained channel. If a channelID is given then the procedure is scoped to the given team,
// if channelID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedChannelMemberships(c request.CTX, channelID *string) error {
channelMembers, appErr := a.ChannelMembersToRemove(channelID)
if appErr != nil {
return appErr
}
for _, userChannel := range channelMembers {
channel, err := a.GetChannel(c, userChannel.ChannelId)
if err != nil {
return err
}
err = a.RemoveUserFromChannel(c, userChannel.UserId, "", channel)
if err != nil {
return err
}
a.Log().Info("removed channelmember",
mlog.String("user_id", userChannel.UserId),
mlog.String("channel_id", channel.Id),
)
}
return nil
}
// SyncSyncableRoles updates the SchemeAdmin field value of the given syncable's members based on the configuration of
// the member's group memberships and the configuration of those groups to the syncable. This method should only
// be invoked on group-synced (aka group-constrained) syncables.
func (a *App) SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError {
permittedAdmins, err := a.Srv().Store().Group().PermittedSyncableAdmins(syncableID, syncableType)
if err != nil {
return model.NewAppError("SyncSyncableRoles", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Log().Info(
fmt.Sprintf("Permitted admins for %s", syncableType),
mlog.String(strings.ToLower(fmt.Sprintf("%s_id", syncableType)), syncableID),
mlog.Any("permitted_admins", permittedAdmins),
)
switch syncableType {
case model.GroupSyncableTypeTeam:
nErr := a.Srv().Store().Team().UpdateMembersRole(syncableID, permittedAdmins)
if nErr != nil {
return model.NewAppError("App.SyncSyncableRoles", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
case model.GroupSyncableTypeChannel:
nErr := a.Srv().Store().Channel().UpdateMembersRole(syncableID, permittedAdmins)
if nErr != nil {
return model.NewAppError("App.SyncSyncableRoles", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
default:
return model.NewAppError("App.SyncSyncableRoles", "groups.unsupported_syncable_type", map[string]any{"Value": syncableType}, "", http.StatusInternalServerError)
}
}
// SyncRolesAndMembership updates the SchemeAdmin status and membership of all of the members of the given
// syncable.
func (a *App) SyncRolesAndMembership(c request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool) {
a.SyncSyncableRoles(syncableID, syncableType)
lastJob, _ := a.Srv().Store().Job().GetNewestJobByStatusAndType(model.JobStatusSuccess, model.JobTypeLdapSync)
var since int64
if lastJob != nil {
since = lastJob.StartAt
}
params := model.CreateDefaultMembershipParams{Since: since, ReAddRemovedMembers: includeRemovedMembers}
switch syncableType {
case model.GroupSyncableTypeTeam:
params.ScopedTeamID = &syncableID
a.createDefaultTeamMemberships(c, params)
a.deleteGroupConstrainedTeamMemberships(c, &syncableID)
if err := a.ClearTeamMembersCache(syncableID); err != nil {
c.Logger().Warn("Error clearing team members cache", mlog.Err(err))
}
case model.GroupSyncableTypeChannel:
params.ScopedChannelID = &syncableID
a.createDefaultChannelMemberships(c, params)
a.deleteGroupConstrainedChannelMemberships(c, &syncableID)
if err := a.ClearChannelMembersCache(c, syncableID); err != nil {
c.Logger().Warn("Error clearing channel members cache", mlog.Err(err))
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"image"
"io"
"mime/multipart"
"net/http"
"net/url"
"sort"
"strings"
fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// teamServiceWrapper provides an implementation of `product.TeamService` to be used by products.
type teamServiceWrapper struct {
app AppIface
}
func (w *teamServiceWrapper) GetMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.GetTeamMember(teamID, userID)
}
func (w *teamServiceWrapper) CreateMember(ctx *request.Context, teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.AddTeamMember(ctx, teamID, userID)
}
func (w *teamServiceWrapper) GetGroup(groupID string) (*model.Group, *model.AppError) {
return w.app.GetGroup(groupID, nil, nil)
}
func (w *teamServiceWrapper) GetTeam(teamID string) (*model.Team, *model.AppError) {
return w.app.GetTeam(teamID)
}
func (w *teamServiceWrapper) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := w.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
// Ensure the wrapper implements the product service.
var _ product.TeamService = (*teamServiceWrapper)(nil)
func (a *App) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
maxActiveTeams := *teamLimits.Active
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
// Sort the list of teams based on their creation date
sort.Slice(teams, func(i, j int) bool {
return teams[i].CreateAt < teams[j].CreateAt
})
var activeTeams []*model.Team
var cloudArchivedTeams []*model.Team
for _, team := range teams {
if team.DeleteAt == 0 {
activeTeams = append(activeTeams, team)
}
if team.DeleteAt > 0 && team.CloudLimitsArchived {
cloudArchivedTeams = append(cloudArchivedTeams, team)
}
}
if len(activeTeams) > maxActiveTeams {
// If there are more active teams than allowed, we must archive them
// Remove the first n elements (where n is the allowed number of teams) so they aren't archived
teamsToArchive := activeTeams[maxActiveTeams:]
for _, team := range teamsToArchive {
cloudLimitsArchived := true
// Archive the remainder
patch := model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
_, err := a.PatchTeam(team.Id, &patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
} else if len(activeTeams) < maxActiveTeams && len(cloudArchivedTeams) > 0 {
// If the number of activeTeams is less than the allowed limit, and there are some cloudArchivedTeams, we can restore these cloudArchivedTeams
activeTeamsBeforeLimit := maxActiveTeams - len(activeTeams)
teamsToRestore := cloudArchivedTeams
// If the number of active teams remaining before the limit is hit is fewer than the number of cloudArchivedTeams, trim the list (still according to CreateAt)
// Otherwise, we can restore all of the cloudArchivedTeams without hitting the limit, so don't filter the list
if activeTeamsBeforeLimit < len(cloudArchivedTeams) {
teamsToRestore = cloudArchivedTeams[:(activeTeamsBeforeLimit)]
}
cloudLimitsArchived := false
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teamsToRestore {
err := a.RestoreTeam(team.Id)
if err != nil {
return err
}
_, err = a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
}
}
return nil
}
func (a *App) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
cloudLimitsArchived := true
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teams {
if team.Id != teamID {
_, err := a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
}
return nil
}
// MM-48246 A/B test show linked boards
const preferenceName = "linked_board_created"
func (a *App) shouldCreateOnboardingLinkedBoard(c request.CTX, teamId string) bool {
ffEnabled := a.Config().FeatureFlags.OnboardingAutoShowLinkedBoard
if !ffEnabled {
return false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
a.Log().Error("error checking the existence of boards product: ", mlog.Err(err))
return false
}
if !hasBoard {
a.Log().Warn("board product not found")
return false
}
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
var nfErr *store.ErrNotFound
if errors.As(sysValErr, &nfErr) { // if no board has been registered, it can create one for this team
return true
}
a.Log().Error("cannot get the system values", mlog.Err(sysValErr))
return false
}
// get the team list and check if the team value has been already stored, if so, no need to create a board in town square in that team
teamsList := strings.Split(data.Value, ",")
for _, team := range teamsList {
if team == teamId {
return false
}
}
return true
}
func (a *App) createOnboardingLinkedBoard(c request.CTX, teamId string) (*fb_model.Board, *model.AppError) {
const defaultTemplatesTeam = "0"
// see https://github.com/mattermost/mattermost-server/v6/server/boards/blob/main/server/services/store/sqlstore/board.go#L302
// and https://github.com/mattermost/mattermost-server/pull/22201#discussion_r1099536430
const defaultTemplateTitle = "Welcome to Boards!"
welcomeToBoardsTemplateId := fmt.Sprintf("%x", md5.Sum([]byte(defaultTemplateTitle)))
userId := c.Session().UserId
boardServiceItf, ok := a.Srv().services[product.BoardsKey]
if !ok {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.product_key_not_found", nil, "", http.StatusBadRequest)
}
boardService, typeOk := boardServiceItf.(product.BoardsService)
if !typeOk {
// boardServiceItf is NOT of type product.BoardsService
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.itf_not_of_type", nil, "", http.StatusBadRequest)
}
templates, err := boardService.GetTemplates(defaultTemplatesTeam, userId)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_getting_templates", nil, "", http.StatusBadRequest).Wrap(err)
}
channel, appErr := a.GetChannelByName(c, model.DefaultChannelName, teamId, false)
if appErr != nil {
return nil, appErr
}
var template *fb_model.Board = nil
for _, t := range templates {
v := t.Properties["trackingTemplateId"]
if v == welcomeToBoardsTemplateId {
template = t
break
}
}
if template == nil && len(templates) > 0 {
template = templates[0]
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, userId, teamId, false)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_duplicating_board", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(boardsAndBlocks.Boards) != 1 {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_no_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// link the board with the channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
ChannelID: &channel.Id,
}, boardsAndBlocks.Boards[0].ID, userId)
if err != nil && patchedBoard == nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_patching_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// Save in the system preferences that the board was already created once per team
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
c.Logger().Error("cannot get the system preferences", mlog.Err(sysValErr))
}
teamsList := teamId
// if data is not nil, data.Value contains the list of teams where the A/B test has alredy created a channel for town square
if data != nil {
teamsList = data.Value + "," + teamId
}
if err := a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.PreferenceOnboarding + "_" + preferenceName,
Value: teamsList,
}); err != nil {
c.Logger().Warn("encountered error saving user preferences", mlog.Err(err))
}
return patchedBoard, nil
}
func (a *App) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.AppError) {
rteam, err := a.ch.srv.teamService.CreateTeam(team)
if err != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateTeam", "app.team.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
case errors.As(err, &cErr):
return nil, model.NewAppError("CreateTeam", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, <Err):
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateTeam", "app.team.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// MM-48246 A/B test show linked boards. Create a welcome to boards linked board per user
if a.shouldCreateOnboardingLinkedBoard(c, team.Id) {
board, aErr := a.createOnboardingLinkedBoard(c, team.Id)
if aErr != nil || board == nil {
a.Log().Warn("Error creating the linked board, only team created", mlog.Err(err))
return rteam, nil
}
if board.ID != "" {
logInfo := fmt.Sprintf("Board created with id %s and associated to channel %s in team %s", board.ID, board.ChannelID, team.Id)
a.Log().Info(logInfo, mlog.Err(err))
}
}
return rteam, nil
}
func (a *App) CreateTeamWithUser(c *request.Context, team *model.Team, userID string) (*model.Team, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
team.Email = user.Email
if !a.ch.srv.teamService.IsTeamEmailAllowed(user, team) {
return nil, model.NewAppError("CreateTeamWithUser", "api.team.is_team_creation_allowed.domain.app_error", nil, "", http.StatusBadRequest)
}
rteam, err := a.CreateTeam(c, team)
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, rteam, user, ""); err != nil {
return nil, err
}
return rteam, nil
}
func (a *App) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
oldTeam, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{Sanitized: true})
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("UpdateTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return oldTeam, nil
}
// RenameTeam is used to rename the team Name and the DisplayName fields
func (a *App) RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError) {
// check if name is occupied
_, errnf := a.GetTeamByName(newTeamName)
// "-" can be used as a newTeamName if only DisplayName change is wanted
if errnf == nil && newTeamName != "-" {
errbody := fmt.Sprintf("team with name %s already exists", newTeamName)
return nil, model.NewAppError("RenameTeam", "app.team.rename_team.name_occupied", nil, errbody, http.StatusBadRequest)
}
if newTeamName != "-" {
team.Name = newTeamName
}
if newDisplayName != "" {
team.DisplayName = newDisplayName
}
newTeam, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{})
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("RenameTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("RenameTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("RenameTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RenameTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return newTeam, nil
}
func (a *App) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) {
oldTeam, err := a.GetTeam(team.Id)
if err != nil {
return nil, err
}
oldTeam.SchemeId = team.SchemeId
oldTeam, nErr := a.Srv().Store().Team().Update(oldTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("UpdateTeamScheme", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamScheme", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
nErr = a.ClearTeamMembersCache(team.Id)
if nErr != nil {
return nil, model.NewAppError("UpdateTeamScheme", "app.team.clear_cache.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.Srv().Store().Channel().ClearMembersForUserCache()
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeamScheme); appErr != nil {
return nil, appErr
}
return oldTeam, nil
}
func (a *App) UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError {
oldTeam, err := a.GetTeam(teamID)
if err != nil {
return err
}
// Force a regeneration of the invite token if changing a team to restricted.
if (allowOpenInvite != oldTeam.AllowOpenInvite || teamType != oldTeam.Type) && (!allowOpenInvite || teamType == model.TeamInvite) {
oldTeam.InviteId = model.NewId()
}
oldTeam.Type = teamType
oldTeam.AllowOpenInvite = allowOpenInvite
oldTeam, nErr := a.Srv().Store().Team().Update(oldTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("UpdateTeamPrivacy", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("UpdateTeamPrivacy", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, *model.AppError) {
team, err := a.ch.srv.teamService.PatchTeam(teamID, patch)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("PatchTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("PatchTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("PatchTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("PatchTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return team, nil
}
func (a *App) RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return nil, err
}
team.InviteId = model.NewId()
updatedTeam, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("RegenerateTeamInviteId", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("RegenerateTeamInviteId", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(updatedTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return updatedTeam, nil
}
func (a *App) sendTeamEvent(team *model.Team, event string) *model.AppError {
sanitizedTeam := &model.Team{}
*sanitizedTeam = *team
sanitizedTeam.Sanitize()
message := model.NewWebSocketEvent(event, sanitizedTeam.Id, "", "", nil, "")
teamJSON, jsonErr := json.Marshal(sanitizedTeam)
if jsonErr != nil {
return model.NewAppError("sendTeamEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("team", string(teamJSON))
a.Publish(message)
return nil
}
func (a *App) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return "", "", "", err
}
if team.SchemeId != nil && *team.SchemeId != "" {
scheme, err := a.GetScheme(*team.SchemeId)
if err != nil {
return "", "", "", err
}
return scheme.DefaultTeamGuestRole, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole, nil
}
return model.TeamGuestRoleId, model.TeamUserRoleId, model.TeamAdminRoleId, nil
}
func (a *App) UpdateTeamMemberRoles(teamID string, userID string, newRoles string) (*model.TeamMember, *model.AppError) {
member, nErr := a.Srv().Store().Team().GetMember(context.Background(), teamID, userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if member == nil {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.team.update_member_roles.not_a_member", nil, "userId="+userID+" teamId="+teamID, http.StatusBadRequest)
}
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForTeam(teamID)
if err != nil {
return nil, err
}
prevSchemeGuestValue := member.SchemeGuest
var newExplicitRoles []string
member.SchemeGuest = false
member.SchemeUser = false
member.SchemeAdmin = false
for _, roleName := range strings.Fields(newRoles) {
var role *model.Role
role, err = a.GetRoleByName(context.Background(), roleName)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
if !role.SchemeManaged {
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
newExplicitRoles = append(newExplicitRoles, roleName)
} else {
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
switch roleName {
case schemeAdminRole:
member.SchemeAdmin = true
case schemeUserRole:
member.SchemeUser = true
case schemeGuestRole:
member.SchemeGuest = true
default:
// If not part of the scheme for this team, then it is not allowed to apply it as an explicit role.
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.channel.update_team_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
}
}
}
if member.SchemeGuest && member.SchemeUser {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.team.update_team_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
if prevSchemeGuestValue != member.SchemeGuest {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.channel.update_team_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
}
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
member, nErr = a.Srv().Store().Team().UpdateMember(member)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ClearSessionCacheForUser(userID)
if appErr := a.sendUpdatedMemberRoleEvent(userID, member); appErr != nil {
return nil, appErr
}
return member, nil
}
func (a *App) UpdateTeamMemberSchemeRoles(teamID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.TeamMember, *model.AppError) {
member, err := a.GetTeamMember(teamID, userID)
if err != nil {
return nil, err
}
member.SchemeAdmin = isSchemeAdmin
member.SchemeUser = isSchemeUser
member.SchemeGuest = isSchemeGuest
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateTeamMemberSchemeRoles", "api.team.update_team_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
// If the migration is not completed, we also need to check the default team_admin/team_user roles are not present in the roles field.
if err = a.IsPhase2MigrationCompleted(); err != nil {
member.ExplicitRoles = RemoveRoles([]string{model.TeamGuestRoleId, model.TeamUserRoleId, model.TeamAdminRoleId}, member.ExplicitRoles)
}
member, nErr := a.Srv().Store().Team().UpdateMember(member)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamMemberSchemeRoles", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ClearSessionCacheForUser(userID)
if appErr := a.sendUpdatedMemberRoleEvent(userID, member); appErr != nil {
return nil, appErr
}
return member, nil
}
func (a *App) sendUpdatedMemberRoleEvent(userID string, member *model.TeamMember) *model.AppError {
message := model.NewWebSocketEvent(model.WebsocketEventMemberroleUpdated, "", "", userID, nil, "")
tmJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("sendUpdatedMemberRoleEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("member", string(tmJSON))
a.Publish(message)
return nil
}
func (a *App) AddUserToTeam(c request.CTX, teamID string, userID string, userRequestorId string) (*model.Team, *model.TeamMember, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
teamMember, err := a.JoinUserToTeam(c, team, user, userRequestorId)
if err != nil {
return nil, nil, err
}
return team, teamMember, nil
}
func (a *App) AddUserToTeamByTeamId(c *request.Context, teamID string, user *model.User) *model.AppError {
team, err := a.Srv().Store().Team().Get(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("AddUserToTeamByTeamId", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("AddUserToTeamByTeamId", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, err := a.JoinUserToTeam(c, team, user, ""); err != nil {
return err
}
return nil
}
func (a *App) AddUserToTeamByToken(c *request.Context, userID string, tokenID string) (*model.Team, *model.TeamMember, *model.AppError) {
token, err := a.Srv().Store().Token().GetByToken(tokenID)
if err != nil {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(tokenData["teamId"])
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
if team.IsGroupConstrained() {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.invite_token.group_constrained.error", nil, "", http.StatusForbidden)
}
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
if user.IsGuest() && token.Type == TokenTypeTeamInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest)
}
if !user.IsGuest() && token.Type == TokenTypeGuestInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest)
}
teamMember, appErr := a.JoinUserToTeam(c, team, user, "")
if appErr != nil {
return nil, nil, appErr
}
if token.Type == TokenTypeGuestInvitation {
channels, err := a.Srv().Store().Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " "), false)
if err != nil {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, channel := range channels {
_, err := a.AddUserToChannel(c, user, channel, false)
if err != nil {
mlog.Warn("Error adding user to channel", mlog.Err(err))
}
}
}
if err := a.DeleteToken(token); err != nil {
mlog.Warn("Error while deleting token", mlog.Err(err))
}
return team, teamMember, nil
}
func (a *App) AddUserToTeamByInviteId(c *request.Context, inviteId string, userID string) (*model.Team, *model.TeamMember, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
teamMember, err := a.JoinUserToTeam(c, team, user, "")
if err != nil {
return nil, nil, err
}
return team, teamMember, nil
}
func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
teamMember, alreadyAdded, err := a.ch.srv.teamService.JoinUserToTeam(team, user)
if err != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.Is(err, teams.AcceptedDomainError):
return nil, model.NewAppError("JoinUserToTeam", "api.team.join_user_to_team.allowed_domains.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, teams.MemberCountError):
return nil, model.NewAppError("JoinUserToTeam", "app.team.get_active_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, teams.MaxMemberCountError):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.max_accounts.app_error", nil, "teamId="+team.Id, http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
case errors.As(err, &conflictErr):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &limitExceededErr):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if alreadyAdded {
return teamMember, nil
}
if _, err := a.Srv().Store().User().UpdateUpdateAt(user.Id); err != nil {
return nil, model.NewAppError("JoinUserToTeam", "app.user.update_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
if _, err := a.createInitialSidebarCategories(user.Id, opts); err != nil {
mlog.Warn(
"Encountered an issue creating default sidebar categories.",
mlog.String("user_id", user.Id),
mlog.String("team_id", team.Id),
mlog.Err(err),
)
}
shouldBeAdmin := team.Email == user.Email
if !user.IsGuest() {
// Soft error if there is an issue joining the default channels
if err := a.JoinDefaultChannels(c, team.Id, user, shouldBeAdmin, userRequestorId); err != nil {
mlog.Warn(
"Encountered an issue joining default channels.",
mlog.String("user_id", user.Id),
mlog.String("team_id", team.Id),
mlog.Err(err),
)
}
}
a.ClearSessionCacheForUser(user.Id)
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForUserTeams(user.Id)
var actor *model.User
if userRequestorId != "" {
actor, _ = a.GetUser(userRequestorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedTeam(pluginContext, teamMember, actor)
return true
}, plugin.UserHasJoinedTeamID)
})
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", user.Id, nil, "")
message.Add("team_id", team.Id)
message.Add("user_id", user.Id)
a.Publish(message)
return teamMember, nil
}
func (a *App) GetTeam(teamID string) (*model.Team, *model.AppError) {
team, err := a.ch.srv.teamService.GetTeam(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return team, nil
}
func (a *App) GetTeams(teamIDs []string) ([]*model.Team, *model.AppError) {
teams, err := a.ch.srv.teamService.GetTeams(teamIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return teams, nil
}
func (a *App) GetTeamByName(name string) (*model.Team, *model.AppError) {
team, err := a.Srv().Store().Team().GetByName(name)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamByName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamByName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
}
return team, nil
}
func (a *App) GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return team, nil
}
func (a *App) GetAllTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAll()
if err != nil {
return nil, model.NewAppError("GetAllTeams", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllTeamsPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllPage(offset, limit, opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPage", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllTeamsPageWithCount(offset int, limit int, opts *model.TeamSearch) (*model.TeamsWithCount, *model.AppError) {
totalCount, err := a.Srv().Store().Team().AnalyticsTeamCount(opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPageWithCount", "app.team.analytics_team_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
teams, err := a.Srv().Store().Team().GetAllPage(offset, limit, opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPageWithCount", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &model.TeamsWithCount{Teams: teams, TotalCount: totalCount}, nil
}
func (a *App) GetAllPrivateTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllPrivateTeamListing()
if err != nil {
return nil, model.NewAppError("GetAllPrivateTeams", "app.team.get_all_private_team_listing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllPublicTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllTeamListing()
if err != nil {
return nil, model.NewAppError("GetAllPublicTeams", "app.team.get_all_team_listing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
// SearchAllTeams returns a team list and the total count of the results
func (a *App) SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) {
if searchOpts.IsPaginated() {
teams, count, err := a.Srv().Store().Team().SearchAllPaged(searchOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllTeams", "app.team.search_all_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, count, nil
}
results, err := a.Srv().Store().Team().SearchAll(searchOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllTeams", "app.team.search_all_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return results, int64(len(results)), nil
}
func (a *App) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().SearchOpen(searchOpts)
if err != nil {
return nil, model.NewAppError("SearchPublicTeams", "app.team.search_open_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().SearchPrivate(searchOpts)
if err != nil {
return nil, model.NewAppError("SearchPrivateTeams", "app.team.search_private_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetTeamsByUserId(userID)
if err != nil {
return nil, model.NewAppError("GetTeamsForUser", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
teamMember, err := a.Srv().Store().Team().GetMember(sqlstore.WithMaster(context.Background()), teamID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamMember", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamMember", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return teamMember, nil
}
func (a *App) GetTeamMembersForUser(userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetTeamsForUser(context.Background(), userID, excludeTeamID, includeDeleted)
if err != nil {
return nil, model.NewAppError("GetTeamMembersForUser", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembersForUserWithPagination(userID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetTeamsForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetTeamMembersForUserWithPagination", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err != nil {
return nil, model.NewAppError("GetTeamMembers", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetMembersByIds(teamID, userIDs, restrictions)
if err != nil {
return nil, model.NewAppError("GetTeamMembersByIds", "app.team.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, *model.AppError) {
teamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
return nil, model.NewAppError("GetCommonTeamIDsForUsers", "app.team.get_common_team_ids_for_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamIDs, nil
}
func (a *App) AddTeamMember(c request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError) {
_, teamMember, err := a.AddUserToTeam(c, teamID, userID, "")
if err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", userID, nil, "")
message.Add("team_id", teamID)
message.Add("user_id", userID)
a.Publish(message)
return teamMember, nil
}
func (a *App) AddTeamMembers(c *request.Context, teamID string, userIDs []string, userRequestorId string, graceful bool) ([]*model.TeamMemberWithError, *model.AppError) {
var membersWithErrors []*model.TeamMemberWithError
for _, userID := range userIDs {
_, teamMember, err := a.AddUserToTeam(c, teamID, userID, userRequestorId)
if err != nil {
if graceful {
membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{
UserId: userID,
Error: err,
})
continue
}
return nil, err
}
membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{
UserId: userID,
Member: teamMember,
})
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", userID, nil, "")
message.Add("team_id", teamID)
message.Add("user_id", userID)
a.Publish(message)
}
return membersWithErrors, nil
}
func (a *App) AddTeamMemberByToken(c *request.Context, userID, tokenID string) (*model.TeamMember, *model.AppError) {
_, teamMember, err := a.AddUserToTeamByToken(c, userID, tokenID)
if err != nil {
return nil, err
}
return teamMember, nil
}
func (a *App) AddTeamMemberByInviteId(c *request.Context, inviteId, userID string) (*model.TeamMember, *model.AppError) {
team, teamMember, err := a.AddUserToTeamByInviteId(c, inviteId, userID)
if err != nil {
return nil, err
}
if team.IsGroupConstrained() {
return nil, model.NewAppError("AddTeamMemberByInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
}
return teamMember, nil
}
func (a *App) GetTeamUnread(teamID, userID string) (*model.TeamUnread, *model.AppError) {
channelUnreads, err := a.Srv().Store().Team().GetChannelUnreadsForTeam(teamID, userID)
if err != nil {
return nil, model.NewAppError("GetTeamUnread", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var teamUnread = &model.TeamUnread{
MsgCount: 0,
MentionCount: 0,
MentionCountRoot: 0,
MsgCountRoot: 0,
TeamId: teamID,
}
for _, cu := range channelUnreads {
teamUnread.MentionCount += cu.MentionCount
teamUnread.MentionCountRoot += cu.MentionCountRoot
if cu.NotifyProps[model.MarkUnreadNotifyProp] != model.ChannelMarkUnreadMention {
teamUnread.MsgCount += cu.MsgCount
teamUnread.MsgCountRoot += cu.MsgCountRoot
}
}
return teamUnread, nil
}
func (a *App) RemoveUserFromTeam(c request.CTX, teamID string, userID string, requestorId string) *model.AppError {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return model.NewAppError("RemoveUserFromTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return model.NewAppError("RemoveUserFromTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return model.NewAppError("RemoveUserFromTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return model.NewAppError("RemoveUserFromTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
if err := a.LeaveTeam(c, team, user, requestorId); err != nil {
return err
}
return nil
}
func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMember, requestorId string) *model.AppError {
var actor *model.User
if requestorId != "" {
actor, _ = a.GetUser(requestorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
return true
}, plugin.UserHasLeftTeamID)
})
user, nErr := a.Srv().Store().User().Get(context.Background(), teamMember.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("postProcessTeamMemberLeave", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("postProcessTeamMemberLeave", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if _, err := a.Srv().Store().User().UpdateUpdateAt(user.Id); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.user.update_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().ClearSidebarOnTeamLeave(user.Id, teamMember.TeamId); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// delete the preferences that set the last channel used in the team and other team specific preferences
if err := a.Srv().Store().Preference().DeleteCategory(user.Id, teamMember.TeamId); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.preference.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.ClearSessionCacheForUser(user.Id)
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForUserTeams(user.Id)
return nil
}
func (a *App) LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError {
teamMember, err := a.GetTeamMember(team.Id, user.Id)
if err != nil {
return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
var channelList model.ChannelList
var nErr error
if channelList, nErr = a.Srv().Store().Channel().GetChannels(team.Id, user.Id, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
}); nErr != nil {
var nfErr *store.ErrNotFound
if errors.As(nErr, &nfErr) {
channelList = model.ChannelList{}
} else {
return model.NewAppError("LeaveTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
for _, channel := range channelList {
if !channel.IsGroupOrDirect() {
a.invalidateCacheForChannelMembers(channel.Id)
if nErr = a.Srv().Store().Channel().RemoveMember(channel.Id, user.Id); nErr != nil {
return model.NewAppError("LeaveTeam", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
channel, cErr := a.Srv().Store().Channel().GetByName(team.Id, model.DefaultChannelName, false)
if cErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cErr, &nfErr):
return model.NewAppError("LeaveTeam", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(cErr)
default:
return model.NewAppError("LeaveTeam", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(cErr)
}
}
if requestorId == user.Id {
if err = a.postLeaveTeamMessage(c, user, channel); err != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(err))
}
} else {
if err = a.postRemoveFromTeamMessage(c, user, channel); err != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(err))
}
}
}
if err := a.ch.srv.teamService.RemoveTeamMember(teamMember); err != nil {
return model.NewAppError("RemoveTeamMemberFromTeam", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.postProcessTeamMemberLeave(c, teamMember, requestorId); err != nil {
return err
}
return nil
}
func (a *App) postLeaveTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.leave.left"), user.Username),
Type: model.PostTypeLeaveTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postRemoveFromTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.remove_user_from_team.removed"), user.Username),
Type: model.PostTypeRemoveFromTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string, channelIds []string) (*model.User, *model.Team, []*model.Channel, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), senderId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
var channels []*model.Channel
var err error
if len(channelIds) > 0 {
channels, err = a.Srv().Store().Channel().GetChannelsByIds(channelIds, false)
if err != nil {
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
for _, channel := range channels {
if channel.TeamId != teamID {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest)
}
}
return user, team, channels, nil
}
func (a *App) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
return nil, err
}
user, team, channels, err := a.prepareInviteNewUsersToTeam(teamID, senderId, memberInvite.ChannelIds)
if err != nil {
return nil, err
}
allowedDomains := a.ch.srv.teamService.GetAllowedDomains(user, team)
var inviteListWithErrors []*model.EmailInviteWithError
var goodEmails []string
for _, email := range emailList {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !teams.IsEmailAddressAllowed(email, allowedDomains) {
invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else {
goodEmails = append(goodEmails, email)
}
inviteListWithErrors = append(inviteListWithErrors, invite)
}
var reminderData *model.TeamInviteReminderData
if reminderInterval != "" {
reminderData = &model.TeamInviteReminderData{Interval: reminderInterval}
}
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
userIsFirstAdmin := a.UserIsFirstAdmin(user)
var eErr error
var invitesWithErrors2 []*model.EmailInviteWithError
if len(channels) > 0 {
invitesWithErrors2, eErr = a.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), reminderData, memberInvite.Message, true, user.IsSystemAdmin(), userIsFirstAdmin)
inviteListWithErrors = append(inviteListWithErrors, invitesWithErrors2...)
} else {
eErr = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true, user.IsSystemAdmin(), userIsFirstAdmin)
}
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
return inviteListWithErrors, nil
}
func (a *App) prepareInviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) (*model.User, *model.Team, []*model.Channel, *model.AppError) {
if err := guestsInvite.IsValid(); err != nil {
return nil, nil, nil, err
}
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
cchan := make(chan store.StoreResult, 1)
go func() {
channels, err := a.Srv().Store().Channel().GetChannelsByIds(guestsInvite.Channels, false)
cchan <- store.StoreResult{Data: channels, NErr: err}
close(cchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), senderId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-cchan
if result.NErr != nil {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
channels := result.Data.([]*model.Channel)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
result = <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
for _, channel := range channels {
if channel.TeamId != teamID {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest)
}
}
return user, team, channels, nil
}
func (a *App) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError) {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return nil, model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, team, channels, err := a.prepareInviteGuestsToChannels(teamID, guestsInvite, senderId)
if err != nil {
return nil, err
}
var inviteListWithErrors []*model.EmailInviteWithError
var goodEmails []string
for _, email := range guestsInvite.Emails {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !users.CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
invite.Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else {
goodEmails = append(goodEmails, email)
}
inviteListWithErrors = append(inviteListWithErrors, invite)
}
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message, true, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
return inviteListWithErrors, nil
}
func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if len(emailList) == 0 {
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
return err
}
user, team, _, err := a.prepareInviteNewUsersToTeam(teamID, senderId, []string{})
if err != nil {
return err
}
allowedDomains := a.ch.srv.teamService.GetAllowedDomains(user, team)
var invalidEmailList []string
for _, email := range emailList {
if !teams.IsEmailAddressAllowed(email, allowedDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
return nil
}
func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, team, channels, err := a.prepareInviteGuestsToChannels(teamID, guestsInvite, senderId)
if err != nil {
return err
}
var invalidEmailList []string
for _, email := range guestsInvite.Emails {
if !users.CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
return model.NewAppError("InviteGuestsToChannels", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusRequestEntityTooLarge)
}
}
return nil
}
func (a *App) FindTeamByName(name string) bool {
if _, err := a.Srv().Store().Team().GetByName(name); err != nil {
return false
}
return true
}
func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, includeCollapsedThreads bool) ([]*model.TeamUnread, *model.AppError) {
data, err := a.Srv().Store().Team().GetChannelUnreadsForAllTeams(excludeTeamId, userID)
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
members := []*model.TeamUnread{}
membersMap := make(map[string]*model.TeamUnread)
unreads := func(cu *model.ChannelUnread, tu *model.TeamUnread) *model.TeamUnread {
tu.MentionCount += cu.MentionCount
tu.MentionCountRoot += cu.MentionCountRoot
if cu.NotifyProps[model.MarkUnreadNotifyProp] != model.ChannelMarkUnreadMention {
tu.MsgCount += cu.MsgCount
tu.MsgCountRoot += cu.MsgCountRoot
}
return tu
}
teamIDs := make([]string, 0, len(data))
for i := range data {
id := data[i].TeamId
if mu, ok := membersMap[id]; ok {
membersMap[id] = unreads(data[i], mu)
} else {
teamIDs = append(teamIDs, id)
membersMap[id] = unreads(data[i], &model.TeamUnread{
MsgCount: 0,
MentionCount: 0,
MentionCountRoot: 0,
MsgCountRoot: 0,
ThreadCount: 0,
ThreadMentionCount: 0,
ThreadUrgentMentionCount: 0,
TeamId: id,
})
}
}
includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
if includeCollapsedThreads {
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled())
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for teamID, member := range membersMap {
if _, ok := teamUnreads[teamID]; ok {
member.ThreadCount = teamUnreads[teamID].ThreadCount
member.ThreadMentionCount = teamUnreads[teamID].ThreadMentionCount
member.ThreadUrgentMentionCount = teamUnreads[teamID].ThreadUrgentMentionCount
}
}
}
for _, member := range membersMap {
members = append(members, member)
}
return members, nil
}
func (a *App) PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
return a.PermanentDeleteTeam(c, team)
}
func (a *App) PermanentDeleteTeam(c request.CTX, team *model.Team) *model.AppError {
team.DeleteAt = model.GetMillis()
if _, err := a.Srv().Store().Team().Update(team); err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("PermanentDeleteTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if channels, err := a.Srv().Store().Channel().GetTeamChannels(team.Id); err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("PermanentDeleteTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
} else {
for _, ch := range channels {
a.PermanentDeleteChannel(c, ch)
}
}
if err := a.Srv().Store().Team().RemoveAllMembersByTeam(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Command().PermanentDeleteByTeam(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.permanentdeleteteam.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Team().PermanentDelete(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventDeleteTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) SoftDeleteTeam(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
team.DeleteAt = model.GetMillis()
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("SoftDeleteTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("SoftDeleteTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventDeleteTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) RestoreTeam(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
team.DeleteAt = 0
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("RestoreTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("RestoreTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventRestoreTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) GetTeamStats(teamID string, restrictions *model.ViewUsersRestrictions) (*model.TeamStats, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
totalMemberCount, err := a.Srv().Store().Team().GetTotalMemberCount(teamID, restrictions)
tchan <- store.StoreResult{Data: totalMemberCount, NErr: err}
close(tchan)
}()
achan := make(chan store.StoreResult, 1)
go func() {
memberCount, err := a.Srv().Store().Team().GetActiveMemberCount(teamID, restrictions)
achan <- store.StoreResult{Data: memberCount, NErr: err}
close(achan)
}()
stats := &model.TeamStats{}
stats.TeamId = teamID
result := <-tchan
if result.NErr != nil {
return nil, model.NewAppError("GetTeamStats", "app.team.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
stats.TotalMemberCount = result.Data.(int64)
result = <-achan
if result.NErr != nil {
return nil, model.NewAppError("GetTeamStats", "app.team.get_active_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
stats.ActiveMemberCount = result.Data.(int64)
return stats, nil
}
func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
tokenID := query.Get("t")
inviteId := query.Get("id")
if tokenID != "" {
token, err := a.Srv().Store().Token().GetByToken(tokenID)
if err != nil {
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest)
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
return tokenData["teamId"], nil
}
if inviteId != "" {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
if err == nil {
return team.Id, nil
}
// soft fail, so we still create user but don't auto-join team
mlog.Warn("Error getting team by inviteId.", mlog.String("invite_id", inviteId), mlog.Err(err))
}
return "", nil
}
func (a *App) SanitizeTeam(session model.Session, team *model.Team) *model.Team {
if a.SessionHasPermissionToTeam(session, team.Id, model.PermissionManageTeam) {
return team
}
if a.SessionHasPermissionToTeam(session, team.Id, model.PermissionInviteUser) {
inviteId := team.InviteId
team.Sanitize()
team.InviteId = inviteId
return team
}
team.Sanitize()
return team
}
func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team {
for _, team := range teams {
a.SanitizeTeam(session, team)
}
return teams
}
func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented)
}
path := "teams/" + team.Id + "/teamIcon.png"
data, err := a.ReadFile(path)
if err != nil {
return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
return data, nil
}
func (a *App) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
return a.SetTeamIconFromMultiPartFile(teamID, file)
}
func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
team, getTeamErr := a.GetTeam(teamID)
if getTeamErr != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, "", http.StatusBadRequest).Wrap(getTeamErr)
}
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented)
}
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.check_image_limits.app_error",
nil, "", http.StatusBadRequest).Wrap(limitErr)
}
return a.SetTeamIconFromFile(team, file)
}
func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
// Decode image into Image object
img, _, err := image.Decode(file)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
img = imaging.MakeImageUpright(img, orientation)
// Scale team icon
teamIconWidthAndHeight := 128
img = imaging.FillCenter(img, teamIconWidthAndHeight, teamIconWidthAndHeight)
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
path := "teams/" + team.Id + "/teamIcon.png"
if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
curTime := model.GetMillis()
if err := a.Srv().Store().Team().UpdateLastTeamIconUpdate(team.Id, curTime); err != nil {
return model.NewAppError("SetTeamIcon", "api.team.team_icon.update.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
// manually set time to avoid possible cluster inconsistencies
team.LastTeamIconUpdate = curTime
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) RemoveTeamIcon(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return model.NewAppError("RemoveTeamIcon", "api.team.remove_team_icon.get_team.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Srv().Store().Team().UpdateLastTeamIconUpdate(teamID, 0); err != nil {
return model.NewAppError("RemoveTeamIcon", "api.team.team_icon.update.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
team.LastTeamIconUpdate = 0
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) InvalidateAllEmailInvites() *model.AppError {
if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeTeamInvitation); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeGuestInvitation); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.InvalidateAllResendInviteEmailJobs(); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) InvalidateAllResendInviteEmailJobs() *model.AppError {
jobs, appErr := a.Srv().Jobs.GetJobsByTypeAndStatus(model.JobTypeResendInvitationEmail, model.JobStatusPending)
if appErr != nil {
return appErr
}
for _, j := range jobs {
a.Srv().Jobs.SetJobCanceled(j)
// clean up any system values this job was using
a.Srv().Store().System().PermanentDeleteByName(j.Id)
}
return nil
}
func (a *App) ClearTeamMembersCache(teamID string) error {
perPage := 100
page := 0
for {
teamMembers, err := a.Srv().Store().Team().GetMembers(teamID, page*perPage, perPage, nil)
if err != nil {
return fmt.Errorf("failed to get team members: %v", err)
}
for _, teamMember := range teamMembers {
a.ClearSessionCacheForUser(teamMember.UserId)
message := model.NewWebSocketEvent(model.WebsocketEventMemberroleUpdated, "", "", teamMember.UserId, nil, "")
tmJSON, jsonErr := json.Marshal(teamMember)
if jsonErr != nil {
return jsonErr
}
message.Add("member", string(tmJSON))
a.Publish(message)
}
length := len(teamMembers)
if length < perPage {
break
}
page++
}
return nil
}
func (a *App) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
ntms, count, err := a.Srv().Store().Team().GetNewTeamMembersSince(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return ntms, count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import "errors"
var (
AcceptedDomainError = errors.New("the user cannot be added as the domain associated with the account is not permitted")
MemberCountError = errors.New("unable to count the team members")
MaxMemberCountError = errors.New("reached to the maximum number of allowed accounts")
)
type DomainError struct {
Domain string
}
func (DomainError) Error() string {
return "restricting team to the domain, it is not allowed by the system config"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type TeamService struct {
store store.TeamStore
groupStore store.GroupStore
channelStore store.ChannelStore // TODO: replace this with ChannelService in the future
users Users
wh WebHub
config func() *model.Config
license func() *model.License
}
// ServiceConfig is used to initialize the TeamService.
type ServiceConfig struct {
// Mandatory fields
TeamStore store.TeamStore
GroupStore store.GroupStore
ChannelStore store.ChannelStore
Users Users
WebHub WebHub
ConfigFn func() *model.Config
LicenseFn func() *model.License
}
// Users is a subset of UserService interface
type Users interface {
GetUser(userID string) (*model.User, error)
}
// WebHub is used to publish events, the name should be given appropriately
// while developing the websocket or clustering service
type WebHub interface {
Publish(message *model.WebSocketEvent)
}
func New(c ServiceConfig) (*TeamService, error) {
if err := c.validate(); err != nil {
return nil, err
}
return &TeamService{
store: c.TeamStore,
groupStore: c.GroupStore,
channelStore: c.ChannelStore,
users: c.Users,
config: c.ConfigFn,
license: c.LicenseFn,
wh: c.WebHub,
}, nil
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.TeamStore == nil || c.LicenseFn == nil || c.Users == nil || c.ChannelStore == nil || c.GroupStore == nil || c.WebHub == nil {
return errors.New("required parameters are not provided")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func (ts *TeamService) CreateTeam(team *model.Team) (*model.Team, error) {
team.InviteId = ""
rteam, err := ts.store.Save(team)
if err != nil {
return nil, err
}
if _, err := ts.createDefaultChannels(rteam.Id); err != nil {
return nil, err
}
return rteam, nil
}
func (ts *TeamService) GetTeam(teamID string) (*model.Team, error) {
team, err := ts.store.Get(teamID)
if err != nil {
return nil, err
}
return team, nil
}
func (ts *TeamService) GetTeams(teamIDs []string) ([]*model.Team, error) {
teams, err := ts.store.GetMany(teamIDs)
if err != nil {
return nil, err
}
return teams, nil
}
// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames.
func (ts *TeamService) createDefaultChannels(teamID string) ([]*model.Channel, error) {
displayNames := map[string]string{
"town-square": i18n.T("api.channel.create_default_channels.town_square"),
"off-topic": i18n.T("api.channel.create_default_channels.off_topic"),
}
channels := []*model.Channel{}
defaultChannelNames := ts.DefaultChannelNames()
for _, name := range defaultChannelNames {
displayName := i18n.TDefault(displayNames[name], name)
channel := &model.Channel{DisplayName: displayName, Name: name, Type: model.ChannelTypeOpen, TeamId: teamID}
// We should use the channel service here (coming soon). Ideally, we should just emit an event
// and let the subscribers do the job, in this case it would be the channels service.
// Currently we are adding services to the server and because of that we are using
// the channel store here. This should be replaced in the future.
if _, err := ts.channelStore.Save(channel, *ts.config().TeamSettings.MaxChannelsPerTeam); err != nil {
return nil, err
}
channels = append(channels, channel)
}
return channels, nil
}
type UpdateOptions struct {
Sanitized bool
Imported bool
}
func (ts *TeamService) UpdateTeam(team *model.Team, opts UpdateOptions) (*model.Team, error) {
oldTeam := team
var err error
if !opts.Imported {
oldTeam, err = ts.store.Get(team.Id)
if err != nil {
return nil, err
}
if err = ts.checkValidDomains(team); err != nil {
return nil, err
}
}
if opts.Sanitized {
oldTeam.DisplayName = team.DisplayName
oldTeam.Description = team.Description
oldTeam.AllowOpenInvite = team.AllowOpenInvite
oldTeam.CompanyName = team.CompanyName
oldTeam.AllowedDomains = team.AllowedDomains
oldTeam.LastTeamIconUpdate = team.LastTeamIconUpdate
oldTeam.GroupConstrained = team.GroupConstrained
}
oldTeam, err = ts.store.Update(oldTeam)
if err != nil {
return team, err
}
return oldTeam, nil
}
func (ts *TeamService) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, error) {
team, err := ts.store.Get(teamID)
if err != nil {
return nil, err
}
team.Patch(patch)
if patch.AllowOpenInvite != nil && !*patch.AllowOpenInvite {
team.InviteId = model.NewId()
}
if err = ts.checkValidDomains(team); err != nil {
return nil, err
}
team, err = ts.store.Update(team)
if err != nil {
return team, err
}
return team, nil
}
// JoinUserToTeam adds a user to the team and it returns three values:
// 1. a pointer to the team member, if successful
// 2. a boolean: true if the user has a non-deleted team member for that team already, otherwise false.
// 3. a pointer to an AppError if something went wrong.
func (ts *TeamService) JoinUserToTeam(team *model.Team, user *model.User) (*model.TeamMember, bool, error) {
if !ts.IsTeamEmailAllowed(user, team) {
return nil, false, AcceptedDomainError
}
tm := &model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
CreateAt: model.GetMillis(),
}
if !user.IsGuest() {
userShouldBeAdmin, err := ts.userIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return nil, false, err
}
tm.SchemeAdmin = userShouldBeAdmin
}
if team.Email == user.Email {
tm.SchemeAdmin = true
}
rtm, err := ts.store.GetMember(context.Background(), team.Id, user.Id)
if err != nil {
// Membership appears to be missing. Lets try to add.
tmr, nErr := ts.store.SaveMember(tm, *ts.config().TeamSettings.MaxUsersPerTeam)
if nErr != nil {
return nil, false, nErr
}
return tmr, false, nil
}
// Membership already exists. Check if deleted and update, otherwise do nothing
// Do nothing if already added
if rtm.DeleteAt == 0 {
return rtm, true, nil
}
membersCount, err := ts.store.GetActiveMemberCount(tm.TeamId, nil)
if err != nil {
return nil, false, MemberCountError
}
if membersCount >= int64(*ts.config().TeamSettings.MaxUsersPerTeam) {
return nil, false, MaxMemberCountError
}
member, nErr := ts.store.UpdateMember(tm)
if nErr != nil {
return nil, false, nErr
}
return member, false, nil
}
// RemoveTeamMember removes the team member from the team. This method sends
// the websocket message before actually removing so the user being removed gets it.
func (ts *TeamService) RemoveTeamMember(teamMember *model.TeamMember) error {
/*
MM-43850: send leave_team event to user using `ReliableClusterSend` to improve safety
*/
// message for other team members
omitUsers := make(map[string]bool, 1)
omitUsers[teamMember.UserId] = true
messageTeam := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", omitUsers, "")
messageTeam.Add("user_id", teamMember.UserId)
messageTeam.Add("team_id", teamMember.TeamId)
ts.wh.Publish(messageTeam)
// message for teamMember.UserId
messageUser := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, "", "", teamMember.UserId, nil, "")
messageUser.Add("user_id", teamMember.UserId)
messageUser.Add("team_id", teamMember.TeamId)
ts.wh.Publish(messageUser)
// delete team member
teamMember.Roles = ""
teamMember.DeleteAt = model.GetMillis()
if _, nErr := ts.store.UpdateMember(teamMember); nErr != nil {
return nErr
}
return nil
}
// GetMember return the team member from the team.
func (ts *TeamService) GetMember(teamID string, userID string) (*model.TeamMember, error) {
member, err := ts.store.GetMember(context.Background(), teamID, userID)
if err != nil {
return nil, err
}
return member, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// By default the list will be (not necessarily in this order):
//
// ['town-square', 'off-topic']
//
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
//
// ['town-square', 'game-of-thrones', 'wow']
func (ts *TeamService) DefaultChannelNames() []string {
names := []string{"town-square"}
if len(ts.config().TeamSettings.ExperimentalDefaultChannels) == 0 {
names = append(names, "off-topic")
} else {
seenChannels := map[string]bool{"town-square": true}
for _, channelName := range ts.config().TeamSettings.ExperimentalDefaultChannels {
if !seenChannels[channelName] {
names = append(names, channelName)
seenChannels[channelName] = true
}
}
}
return names
}
func IsEmailAddressAllowed(email string, allowedDomains []string) bool {
for _, restriction := range allowedDomains {
domains := normalizeDomains(restriction)
if len(domains) <= 0 {
continue
}
matched := false
for _, d := range domains {
if strings.HasSuffix(email, "@"+d) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func (ts *TeamService) IsTeamEmailAllowed(user *model.User, team *model.Team) bool {
if user.IsBot {
return true
}
email := strings.ToLower(user.Email)
allowedDomains := ts.GetAllowedDomains(user, team)
return IsEmailAddressAllowed(email, allowedDomains)
}
func (ts *TeamService) GetAllowedDomains(user *model.User, team *model.Team) []string {
if user.IsGuest() {
return []string{*ts.config().GuestAccountsSettings.RestrictCreationToDomains}
}
// First check per team allowedDomains, then app wide restrictions
return []string{team.AllowedDomains, *ts.config().TeamSettings.RestrictCreationToDomains}
}
func (ts *TeamService) checkValidDomains(team *model.Team) error {
validDomains := normalizeDomains(*ts.config().TeamSettings.RestrictCreationToDomains)
if len(validDomains) > 0 {
for _, domain := range normalizeDomains(team.AllowedDomains) {
matched := false
for _, d := range validDomains {
if domain == d {
matched = true
break
}
}
if !matched {
return &DomainError{Domain: domain}
}
}
}
return nil
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
}
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
// admins in the given syncable.
func (ts *TeamService) userIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, error) {
groupIDs, err := ts.groupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
return false, err
}
if len(groupIDs) == 0 {
return false, nil
}
return true, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
func (s *Server) GetTelemetryService() *telemetry.TelemetryService {
return s.telemetryService
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) CreateTermsOfService(text, userID string) (*model.TermsOfService, *model.AppError) {
termsOfService := &model.TermsOfService{
Text: text,
UserId: userID,
}
if _, appErr := a.GetUser(userID); appErr != nil {
return nil, appErr
}
var err error
if termsOfService, err = a.Srv().Store().TermsOfService().Save(termsOfService); err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateTermsOfService", "app.terms_of_service.create.existing.app_error", nil, "id="+termsOfService.Id, http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateTermsOfService", "app.terms_of_service.create.app_error", nil, "terms_of_service_id="+termsOfService.Id, http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
func (a *App) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) {
termsOfService, err := a.Srv().Store().TermsOfService().GetLatest(true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetLatestTermsOfService", "app.terms_of_service.get.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetLatestTermsOfService", "app.terms_of_service.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
func (a *App) GetTermsOfService(id string) (*model.TermsOfService, *model.AppError) {
termsOfService, err := a.Srv().Store().TermsOfService().Get(id, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTermsOfService", "app.terms_of_service.get.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTermsOfService", "app.terms_of_service.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
)
func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool {
state, ok := pluginStates[pluginId]
if !ok {
return false
}
return state.Enable
}
func (a *App) getMarketplacePlugins() ([]string, error) {
ts := a.Srv().telemetryService
config := a.Srv().Config()
marketplacePlugins, err := ts.GetAllMarketplacePlugins(model.PluginSettingsDefaultMarketplaceURL)
if err != nil {
return nil, err
}
activePlugins := []string{}
for _, p := range marketplacePlugins {
id := p.Manifest.Id
if pluginActivated(config.PluginSettings.PluginStates, id) {
activePlugins = append(activePlugins, id)
}
}
return activePlugins, nil
}
func (a *App) getTrueUpProfile() (*model.TrueUpReviewProfile, error) {
license := a.Channels().License()
if license == nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Customer Info & Usage Analytics
activeUserCount, err := a.Srv().Store().Status().GetTotalActiveUsersCount()
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Webhook, calls, boards, and playbook counts
incomingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_in_count_fail", nil, "Could not get the total incoming webhook count", http.StatusInternalServerError)
}
outgoingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_out_count_fail", nil, "Could not get the total outgoing webhook count", http.StatusInternalServerError)
}
// Plugin Data
trueUpReviewPlugins := model.TrueUpReviewPlugins{
PluginNames: []string{},
}
if plugins, err := a.getMarketplacePlugins(); err == nil {
trueUpReviewPlugins.PluginNames = plugins
trueUpReviewPlugins.TotalPlugins = len(plugins)
}
// Authentication Features
config := a.Config()
mfaUsed := config.ServiceSettings.EnforceMultifactorAuthentication
ldapUsed := config.LdapSettings.Enable
samlUsed := config.SamlSettings.Enable
openIdUsed := config.OpenIdSettings.Enable
guestAccessAllowed := config.GuestAccountsSettings.Enable
authFeatures := map[string]*bool{
model.TrueUpReviewAuthFeaturesMfa: mfaUsed,
model.TrueUpReviewAuthFeaturesADLdap: ldapUsed,
model.TrueUpReviewAuthFeaturesSaml: samlUsed,
model.TrueUpReviewAuthFeatureOpenId: openIdUsed,
model.TrueUpReviewAuthFeatureGuestAccess: guestAccessAllowed,
}
authFeatureList := []string{}
for feature, used := range authFeatures {
if used != nil && *used {
authFeatureList = append(authFeatureList, feature)
}
}
reviewProfile := model.TrueUpReviewProfile{
ServerId: a.TelemetryId(),
ServerVersion: model.CurrentVersion,
ServerInstallationType: os.Getenv(telemetry.EnvVarInstallType),
LicenseId: license.Id,
LicensedSeats: *license.Features.Users,
LicensePlan: license.SkuName,
CustomerName: license.Customer.Name,
ActiveUsers: activeUserCount,
TotalIncomingWebhooks: incomingWebhookCount,
TotalOutgoingWebhooks: outgoingWebhookCount,
Plugins: trueUpReviewPlugins,
AuthenticationFeatures: authFeatureList,
}
return &reviewProfile, nil
}
func (a *App) GetTrueUpProfile() (map[string]any, error) {
profile, err := a.getTrueUpProfile()
if err != nil {
return nil, err
}
profileJson, err := json.Marshal(profile)
if err != nil {
return nil, err
}
telemetryProperties := map[string]any{}
json.Unmarshal(profileJson, &telemetryProperties)
delete(telemetryProperties, "plugins")
plugins := profile.Plugins.ToMap()
for key, pluginValue := range plugins {
telemetryProperties[key] = pluginValue
}
delete(telemetryProperties, "authentication_features")
telemetryProperties["authentication_features"] = strings.Join(profile.AuthenticationFeatures, ",")
return telemetryProperties, nil
}
func (a *App) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
nextDueDate := utils.GetNextTrueUpReviewDueDate(time.Now())
status, err := a.Srv().Store().TrueUpReview().GetTrueUpReviewStatus(nextDueDate.UnixMilli())
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
a.Log().Warn("Could not find true up review status")
default:
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "Could not get true up status records", http.StatusInternalServerError).Wrap(err)
}
status, err = a.Srv().Store().TrueUpReview().CreateTrueUpReviewStatusRecord(&model.TrueUpReviewStatus{DueDate: nextDueDate.UnixMilli(), Completed: false})
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.create_error", nil, "Could not create true up status record", http.StatusInternalServerError)
}
}
return status, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const minFirstPartSize = 5 * 1024 * 1024 // 5MB
func (a *App) genFileInfoFromReader(name string, file io.ReadSeeker, size int64) (*model.FileInfo, error) {
ext := strings.ToLower(filepath.Ext(name))
info := &model.FileInfo{
Name: name,
MimeType: mime.TypeByExtension(ext),
Size: size,
Extension: ext,
}
if ext != "" {
// The client expects a file extension without the leading period
info.Extension = ext[1:]
}
if info.IsImage() {
config, _, err := a.ch.imgDecoder.DecodeConfig(file)
if err != nil {
return nil, err
}
info.Width = config.Width
info.Height = config.Height
}
return info, nil
}
func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader) *model.AppError {
filePath := info.Path
// using a pipe to avoid loading the whole file content in memory.
r, w := io.Pipe()
errChan := make(chan *model.AppError, 1)
hookHasRunCh := make(chan struct{})
go func() {
defer w.Close()
defer close(hookHasRunCh)
defer close(errChan)
var rejErr *model.AppError
var once sync.Once
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
once.Do(func() {
hookHasRunCh <- struct{}{}
})
newInfo, rejStr := hooks.FileWillBeUploaded(pluginContext, info, file, w)
if rejStr != "" {
rejErr = model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.rejected",
map[string]any{"Filename": info.Name, "Reason": rejStr}, "", http.StatusBadRequest)
return false
}
if newInfo != nil {
info = newInfo
}
return true
}, plugin.FileWillBeUploadedID)
if rejErr != nil {
errChan <- rejErr
}
}()
// If the plugin hook has not run we can return early.
if _, ok := <-hookHasRunCh; !ok {
return nil
}
tmpPath := filePath + ".tmp"
written, err := a.WriteFile(r, tmpPath)
if err != nil {
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
r.CloseWithError(err) // always returns nil
return err
}
if err = <-errChan; err != nil {
if fileErr := a.RemoveFile(info.Path); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
return err
}
if written > 0 {
info.Size = written
if fileErr := a.MoveFile(tmpPath, info.Path); fileErr != nil {
return model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.move_fail",
nil, "", http.StatusInternalServerError).Wrap(fileErr)
}
} else {
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
}
return nil
}
func (a *App) CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError) {
us.FileOffset = 0
now := time.Now()
us.CreateAt = model.GetMillisForTime(now)
if us.Type == model.UploadTypeAttachment {
us.Path = now.Format("20060102") + "/teams/noteam/channels/" + us.ChannelId + "/users/" + us.UserId + "/" + us.Id + "/" + filepath.Base(us.Filename)
} else if us.Type == model.UploadTypeImport {
us.Path = filepath.Clean(*a.Config().ImportSettings.Directory) + "/" + us.Id + "_" + filepath.Base(us.Filename)
}
if err := us.IsValid(); err != nil {
return nil, err
}
if us.Type == model.UploadTypeAttachment {
channel, err := a.GetChannel(c, us.ChannelId)
if err != nil {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.incorrect_channel_id.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusBadRequest)
}
if channel.DeleteAt != 0 {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.cannot_upload_to_deleted_channel.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusBadRequest)
}
}
us, storeErr := a.Srv().Store().UploadSession().Save(us)
if storeErr != nil {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.save.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
return us, nil
}
func (a *App) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) {
us, err := a.Srv().Store().UploadSession().Get(c.Context(), uploadId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return us, nil
}
func (a *App) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
uss, err := a.Srv().Store().UploadSession().GetForUser(userID)
if err != nil {
return nil, model.NewAppError("GetUploadsForUser", "app.upload.get_for_user.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
return uss, nil
}
func (a *App) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
// prevent more than one caller to upload data at the same time for a given upload session.
// This is to avoid possible inconsistencies.
a.ch.uploadLockMapMut.Lock()
locked := a.ch.uploadLockMap[us.Id]
if locked {
// session lock is already taken, return error.
a.ch.uploadLockMapMut.Unlock()
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
nil, "", http.StatusBadRequest)
}
// grab the session lock.
a.ch.uploadLockMap[us.Id] = true
a.ch.uploadLockMapMut.Unlock()
// reset the session lock on exit.
defer func() {
a.ch.uploadLockMapMut.Lock()
delete(a.ch.uploadLockMap, us.Id)
a.ch.uploadLockMapMut.Unlock()
}()
// fetch the session from store to check for inconsistencies.
c.SetContext(WithMaster(c.Context()))
if storedSession, err := a.GetUploadSession(c, us.Id); err != nil {
return nil, err
} else if us.FileOffset != storedSession.FileOffset {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
nil, "FileOffset mismatch", http.StatusBadRequest)
}
uploadPath := us.Path
if us.Type == model.UploadTypeImport {
uploadPath += model.IncompleteUploadSuffix
}
// make sure it's not possible to upload more data than what is expected.
lr := &io.LimitedReader{
R: rd,
N: us.FileSize - us.FileOffset,
}
var err *model.AppError
var written int64
if us.FileOffset == 0 {
// new upload
written, err = a.WriteFile(lr, uploadPath)
if err != nil && written == 0 {
return nil, err
}
if written < minFirstPartSize && written != us.FileSize {
a.RemoveFile(uploadPath)
var errStr string
if err != nil {
errStr = err.Error()
}
return nil, model.NewAppError("UploadData", "app.upload.upload_data.first_part_too_small.app_error",
map[string]any{"Size": minFirstPartSize}, errStr, http.StatusBadRequest)
}
} else if us.FileOffset < us.FileSize {
// resume upload
written, err = a.AppendFile(lr, uploadPath)
}
if written > 0 {
us.FileOffset += written
if storeErr := a.Srv().Store().UploadSession().Update(us); storeErr != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.update.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
if err != nil {
return nil, err
}
// upload is incomplete
if us.FileOffset != us.FileSize {
return nil, nil
}
// upload is done, create FileInfo
file, err := a.FileReader(uploadPath)
if err != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// generate file info
info, genErr := a.genFileInfoFromReader(us.Filename, file, us.FileSize)
file.Close()
if genErr != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.gen_info.app_error", nil, "", http.StatusInternalServerError).Wrap(genErr)
}
info.CreatorId = us.UserId
info.Path = us.Path
info.RemoteId = model.NewString(us.RemoteId)
if us.ReqFileId != "" {
info.Id = us.ReqFileId
}
// run plugins upload hook
if err := a.runPluginsHook(c, info, file); err != nil {
return nil, err
}
// image post-processing
if info.IsImage() && !info.IsSvg() {
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
map[string]any{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
}
nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
imgData, fileErr := a.ReadFile(uploadPath)
if fileErr != nil {
return nil, fileErr
}
a.HandleImages([]string{info.PreviewPath}, []string{info.ThumbnailPath}, [][]byte{imgData})
}
if us.Type == model.UploadTypeImport {
if err := a.MoveFile(uploadPath, us.Path); err != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.move_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
var storeErr error
if info, storeErr = a.Srv().Store().FileInfo().Save(info); storeErr != nil {
var appErr *model.AppError
switch {
case errors.As(storeErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("uploadData", "app.upload.upload_data.save.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *info
a.Srv().Go(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
// delete upload session
if storeErr := a.Srv().Store().UploadSession().Delete(us.Id); storeErr != nil {
mlog.Warn("Failed to delete UploadSession", mlog.Err(storeErr))
}
return info, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// GetPostsUsage returns the total posts count rounded down to the most
// significant digit
func (a *App) GetPostsUsage() (int64, *model.AppError) {
count, err := a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true, UsersPostsOnly: true, AllowFromCache: true})
if err != nil {
return 0, model.NewAppError("GetPostsUsage", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return utils.RoundOffToZeroesResolution(float64(count), 3), nil
}
// GetStorageUsage returns the sum of files' sizes stored on this instance
func (a *App) GetStorageUsage() (int64, *model.AppError) {
usage, err := a.Srv().Store().FileInfo().GetStorageUsage(true, false)
if err != nil {
return 0, model.NewAppError("GetStorageUsage", "app.usage.get_storage_usage.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return usage, nil
}
func (a *App) GetTeamsUsage() (*model.TeamsUsage, *model.AppError) {
usage := &model.TeamsUsage{}
includeDeleted := false
teamCount, err := a.Srv().Store().Team().AnalyticsTeamCount(&model.TeamSearch{IncludeDeleted: &includeDeleted})
if err != nil {
return nil, model.NewAppError("GetTeamsUsage", "app.post.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
usage.Active = teamCount
allTeams, appErr := a.GetAllTeams()
if appErr != nil {
return nil, appErr
}
cloudArchivedTeamCount := 0
for _, team := range allTeams {
if team.DeleteAt > 0 && team.CloudLimitsArchived {
cloudArchivedTeamCount += 1
}
}
usage.CloudArchived = int64(cloudArchivedTeamCount)
return usage, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
PasswordRecoverExpiryTime = 1000 * 60 * 60 * 24 // 24 hours
InvitationExpiryTime = 1000 * 60 * 60 * 48 // 48 hours
ImageProfilePixelDimension = 128
)
func (a *App) CreateUserWithToken(c request.CTX, user *model.User, token *model.Token) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
team, nErr := a.Srv().Store().Team().Get(tokenData["teamId"])
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// find the sender id and grab the channels in order to validate
// the sender id still belongs to team and to private channels
senderId := tokenData["senderId"]
channelIds := strings.Split(tokenData["channels"], " ")
// filter the channels the original inviter has still permissions over
channelIds = a.ValidateUserPermissionsOnChannels(c, senderId, channelIds)
channels, nErr := a.Srv().Store().Channel().GetChannelsByIds(channelIds, false)
if nErr != nil {
return nil, model.NewAppError("CreateUserWithToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
emailFromToken := tokenData["email"]
if emailFromToken != user.Email {
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.bad_token_email_data.app_error", nil, "", http.StatusBadRequest)
}
user.Email = tokenData["email"]
user.EmailVerified = true
var ruser *model.User
var err *model.AppError
if token.Type == TokenTypeTeamInvitation {
ruser, err = a.CreateUser(c, user)
} else {
ruser, err = a.CreateGuest(c, user)
}
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(c, team.Id, ruser)
if token.Type == TokenTypeGuestInvitation || (token.Type == TokenTypeTeamInvitation && len(channels) > 0) {
for _, channel := range channels {
_, err := a.AddChannelMember(c, ruser.Id, channel, ChannelMemberOpts{})
if err != nil {
c.Logger().Warn("Failed to add channel member", mlog.Err(err))
}
}
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Error while deleting token", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserWithInviteId(c request.CTX, user *model.User, inviteId, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
team, nErr := a.Srv().Store().Team().GetByInviteId(inviteId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if team.IsGroupConstrained() {
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
}
if !users.CheckUserDomain(user, team.AllowedDomains) {
return nil, model.NewAppError("CreateUserWithInviteId", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": team.AllowedDomains}, "", http.StatusForbidden)
}
user.EmailVerified = false
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(c, team.Id, ruser)
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserAsAdmin(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserFromSignup(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if !a.IsFirstUserAccount() && !*a.Config().TeamSettings.EnableOpenServer {
err := model.NewAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email, http.StatusForbidden)
return nil, err
}
user.EmailVerified = false
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
}
return ruser, nil
}
func (a *App) IsUserSignUpAllowed() *model.AppError {
if !*a.Config().EmailSettings.EnableSignUpWithEmail || !*a.Config().TeamSettings.EnableUserCreation {
err := model.NewAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func (a *App) IsFirstUserAccount() bool {
return a.ch.srv.platform.IsFirstUserAccount()
}
func (a *App) IsFirstAdmin(user *model.User) bool {
if !user.IsSystemAdmin() {
return false
}
adminID, err := a.Srv().Store().User().GetFirstSystemAdminID()
if err != nil {
return false
}
return adminID == user.Id
}
// CreateUser creates a user and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(c, user, false)
}
// CreateGuest creates a guest and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateGuest(c request.CTX, user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(c, user, true)
}
func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*model.User, *model.AppError) {
if err := a.isUniqueToGroupNames(user.Username); err != nil {
err.Where = "createUserOrGuest"
return nil, err
}
ruser, nErr := a.ch.srv.userService.CreateUser(user, users.UserCreateOptions{Guest: guest})
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var nfErr *users.ErrInvalidPassword
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.Is(nErr, users.AcceptedDomainError):
return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("createUserOrGuest", "api.user.check_user_password.invalid.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.Is(nErr, users.UserStoreIsEmptyError):
return nil, model.NewAppError("createUserOrGuest", "app.user.store_is_empty.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
case errors.As(nErr, &invErr):
switch invErr.Field {
case "email":
return nil, model.NewAppError("createUserOrGuest", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case "username":
return nil, model.NewAppError("createUserOrGuest", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.EmailVerified {
a.InvalidateCacheForUser(ruser.Id)
nUser, err := a.ch.srv.userService.GetUser(ruser.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("createUserOrGuest", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.sendUpdatedUserEvent(*nUser)
}
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"}
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref}
if a.Config().FeatureFlags.InsightsEnabled {
// We don't want to show the insights intro modal for new users
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":true}"})
} else {
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":false}"})
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
c.Logger().Warn("Encountered error saving user preferences", mlog.Err(err))
}
go a.UpdateViewedProductNoticesForNewUser(ruser.Id)
// This message goes to everyone, so the teamID, channelID and userID are irrelevant
message := model.NewWebSocketEvent(model.WebsocketEventNewUser, "", "", "", nil, "")
message.Add("user_id", ruser.Id)
a.Publish(message)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasBeenCreated(pluginContext, ruser)
return true
}, plugin.UserHasBeenCreatedID)
})
// For cloud yearly subscriptions, if the current user count of the workspace exceeds the number of seats initially purchased
// (plus the “threshold” of 10%), then a subscriptionHistoryEvent object would need to be created and added to the subscriptionHistory
// table in CWS. This is then used to calculate how much the customers have to pay in addition for the extra users. If the
// workspace is currently on a monthly plan, then this function will not do anything.
if a.Channels().License().IsCloud() {
go func(userId string) {
_, err := a.SendSubscriptionHistoryEvent(userId)
if err != nil {
c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(err))
}
}(ruser.Id)
}
return ruser, nil
}
func (a *App) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
if !*a.Config().TeamSettings.EnableUserCreation {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented)
}
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
user, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err1)
}
if user.AuthService == "" {
user.AuthService = service
}
found := true
count := 0
for found {
if found = a.ch.srv.userService.IsUsernameTaken(user.Username); found {
user.Username = user.Username + strconv.Itoa(count)
count++
}
}
userByAuth, _ := a.ch.srv.userService.GetUserByAuth(user.AuthData, service)
if userByAuth != nil {
return userByAuth, nil
}
userByEmail, _ := a.ch.srv.userService.GetUserByEmail(user.Email)
if userByEmail != nil {
if userByEmail.AuthService == "" {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": model.UserAuthServiceEmail}, "email="+user.Email, http.StatusBadRequest)
}
if provider.IsSameUser(userByEmail, user) {
if _, err := a.Srv().Store().User().UpdateAuthData(userByEmail.Id, user.AuthService, user.AuthData, "", false); err != nil {
// if the user is not updated, write a warning to the log, but don't prevent user login
c.Logger().Warn("Error attempting to update user AuthData", mlog.Err(err))
}
return userByEmail, nil
}
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": userByEmail.AuthService}, "email="+user.Email+" authData="+*user.AuthData, http.StatusBadRequest)
}
user.EmailVerified = true
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if teamID != "" {
err = a.AddUserToTeamByTeamId(c, teamID, user)
if err != nil {
return nil, err
}
err = a.AddDirectChannels(c, teamID, user)
if err != nil {
c.Logger().Warn("Failed to add direct channels", mlog.Err(err))
}
}
return ruser, nil
}
func (a *App) GetUser(userID string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUser(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUser", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUsers(userIDs []string) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsers(userIDs)
if err != nil {
return nil, model.NewAppError("GetUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) {
result, err := a.ch.srv.userService.GetUserByUsername(username)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return result, nil
}
func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUserByEmail(email)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUserByAuth(authData, authService)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
default:
return nil, model.NewAppError("GetUserByAuth", "app.user.get_by_auth.other.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersFromProfiles(options)
if err != nil {
return nil, model.NewAppError("GetUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersEtag(restrictionsHash string) string {
return a.ch.srv.userService.GetUsersEtag(restrictionsHash)
}
func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersInTeam(options)
if err != nil {
return nil, model.NewAppError("GetUsersInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersInTeamPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersNotInTeamPage(teamID, groupConstrained, page*perPage, perPage, asAdmin, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
return a.ch.srv.userService.GetUsersInTeamEtag(teamID, restrictionsHash)
}
func (a *App) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
return a.ch.srv.userService.GetUsersNotInTeamEtag(teamID, restrictionsHash)
}
func (a *App) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannel(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannelByStatus(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannelByStatus", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannelByAdmin(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannelByAdmin", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannelByStatus(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInChannelPageByAdmin(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannelByAdmin(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersWithoutTeamPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersWithoutTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersWithoutTeam(options)
if err != nil {
return nil, model.NewAppError("GetUsersWithoutTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers.
func (a *App) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetTeamGroupUsers(teamID)
if err != nil {
return nil, model.NewAppError("GetTeamGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// GetChannelGroupUsers returns the users who are associated to the channel via GroupChannels and GroupMembers.
func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetChannelGroupUsers(channelID)
if err != nil {
return nil, model.NewAppError("GetChannelGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersByIds(userIDs, options)
if err != nil {
return nil, model.NewAppError("GetUsersByIds", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
usersByChannelId, err := a.Srv().Store().User().GetProfileByGroupChannelIdsForUser(c.Session().UserId, channelIDs)
if err != nil {
return nil, model.NewAppError("GetUsersByGroupChannelIds", "app.user.get_profile_by_group_channel_ids_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for channelID, userList := range usersByChannelId {
usersByChannelId[channelID] = a.sanitizeProfiles(userList, asAdmin)
}
return usersByChannelId, nil
}
func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersByUsernames(usernames, &model.UserGetOptions{ViewRestrictions: viewRestrictions})
if err != nil {
return nil, model.NewAppError("GetUsersByUsernames", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
for _, u := range users {
a.SanitizeProfile(u, asAdmin)
}
return users
}
func (a *App) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return nil, model.NewAppError("GenerateMfaSecret", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
mfaSecret, err := a.ch.srv.userService.GenerateMfaSecret(user)
if err != nil {
return nil, model.NewAppError("GenerateMfaSecret", "mfa.generate_qr_code.create_code.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return mfaSecret, nil
}
func (a *App) ActivateMfa(userID, token string) *model.AppError {
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
if user.AuthService != "" && user.AuthService != model.UserAuthServiceLdap {
return model.NewAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "", http.StatusBadRequest)
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return model.NewAppError("ActivateMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.ch.srv.userService.ActivateMfa(user, token); err != nil {
switch {
case errors.Is(err, mfa.InvalidToken):
return model.NewAppError("ActivateMfa", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized)
default:
return model.NewAppError("ActivateMfa", "mfa.activate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func (a *App) DeactivateMfa(userID string) *model.AppError {
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
if err := a.ch.srv.userService.DeactivateMfa(user); err != nil {
return model.NewAppError("DeactivateMfa", "mfa.deactivate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
return a.ch.srv.GetProfileImage(user)
}
func (a *App) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
return a.ch.srv.GetDefaultProfileImage(user)
}
func (a *App) SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError {
img, appErr := a.GetDefaultProfileImage(user)
if appErr != nil {
return appErr
}
path := getProfileImagePath(user.Id)
if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
return err
}
if err := a.Srv().Store().User().ResetLastPictureUpdate(user.Id); err != nil {
c.Logger().Warn("Failed to reset last picture update", mlog.Err(err))
}
a.InvalidateCacheForUser(user.Id)
updatedUser, appErr := a.GetUser(user.Id)
if appErr != nil {
c.Logger().Warn("Error in getting users profile forcing logout", mlog.String("user_id", user.Id), mlog.Err(appErr))
return nil
}
options := a.Config().GetSanitizeOptions()
updatedUser.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", updatedUser)
a.Publish(message)
return nil
}
func (a *App) SetProfileImage(c request.CTX, userID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
return a.SetProfileImageFromMultiPartFile(c, userID, file)
}
func (a *App) SetProfileImageFromMultiPartFile(c request.CTX, userID string, file multipart.File) *model.AppError {
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
}
return a.SetProfileImageFromFile(c, userID, file)
}
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
// Decode image into Image object
img, _, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
img = imaging.MakeImageUpright(img, orientation)
// Scale profile image
profileWidthAndHeight := 128
img = imaging.FillCenter(img, profileWidthAndHeight, profileWidthAndHeight)
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return buf, nil
}
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
buf, err := a.AdjustImage(file)
if err != nil {
return err
}
path := getProfileImagePath(userID)
if storedData, err := a.ReadFile(path); err == nil && bytes.Equal(storedData, buf.Bytes()) {
return nil
}
if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().User().UpdateLastPictureUpdate(userID); err != nil {
c.Logger().Warn("Error with updating last picture update", mlog.Err(err))
}
a.invalidateUserCacheAndPublish(userID)
a.onUserProfileChange(userID)
return nil
}
func (a *App) UpdatePasswordAsUser(c request.CTX, userID, currentPassword, newPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if user == nil {
return model.NewAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "", http.StatusBadRequest)
}
if user.AuthData != nil && *user.AuthData != "" {
return model.NewAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService, http.StatusBadRequest)
}
if err := a.DoubleCheckPassword(user, currentPassword); err != nil {
if err.Id == "api.user.check_user_password.invalid.app_error" {
err = model.NewAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "", http.StatusBadRequest)
}
return err
}
T := i18n.GetUserTranslations(user.Locale)
return a.UpdatePasswordSendEmail(c, user, newPassword, T("api.user.update_password.menu"))
}
func (a *App) userDeactivated(c request.CTX, userID string) *model.AppError {
a.SetStatusOffline(userID, false)
user, err := a.GetUser(userID)
if err != nil {
return err
}
// when disable a user, userDeactivated is called for the user and the
// bots the user owns. Only notify once, when the user is the owner, not the
// owners bots
if !user.IsBot {
a.notifySysadminsBotOwnerDeactivated(c, userID)
}
if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated {
a.disableUserBots(c, userID)
}
return nil
}
func (a *App) invalidateUserChannelMembersCaches(c request.CTX, userID string) *model.AppError {
teamsForUser, err := a.GetTeamsForUser(userID)
if err != nil {
return err
}
for _, team := range teamsForUser {
channelsForUser, err := a.GetChannelsForTeamForUser(c, team.Id, userID, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if err != nil {
return err
}
for _, channel := range channelsForUser {
a.invalidateCacheForChannelMembers(channel.Id)
}
}
return nil
}
func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
user.UpdateAt = model.GetMillis()
if active {
user.DeleteAt = 0
} else {
user.DeleteAt = user.UpdateAt
}
userUpdate, err := a.ch.srv.userService.UpdateUser(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateActive", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateActive", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
ruser := userUpdate.New
if !active {
if err := a.RevokeAllSessions(ruser.Id); err != nil {
return nil, err
}
if err := a.userDeactivated(c, ruser.Id); err != nil {
return nil, err
}
}
a.invalidateUserChannelMembersCaches(c, user.Id)
a.InvalidateCacheForUser(user.Id)
a.sendUpdatedUserEvent(*ruser)
return ruser, nil
}
func (a *App) DeactivateGuests(c *request.Context) *model.AppError {
userIDs, err := a.ch.srv.userService.DeactivateAllGuests()
if err != nil {
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, userID := range userIDs {
if err := a.Srv().Platform().RevokeAllSessions(userID); err != nil {
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, userID := range userIDs {
if err := a.userDeactivated(c, userID); err != nil {
return err
}
}
a.Srv().Store().Channel().ClearCaches()
a.Srv().Store().User().ClearCaches()
message := model.NewWebSocketEvent(model.WebsocketEventGuestsDeactivated, "", "", "", nil, "")
a.Publish(message)
return nil
}
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
return a.ch.srv.userService.GetSanitizeOptions(asAdmin)
}
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
options := a.ch.srv.userService.GetSanitizeOptions(asAdmin)
user.SanitizeProfile(options)
}
func (a *App) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
updatedUser, err := a.UpdateUser(c, user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
// CheckProviderAttributes returns the empty string if the patch can be applied without
// overriding attributes set by the user's login provider; otherwise, the name of the offending
// field is returned.
func (a *App) CheckProviderAttributes(user *model.User, patch *model.UserPatch) string {
tryingToChange := func(userValue *string, patchValue *string) bool {
return patchValue != nil && *patchValue != *userValue
}
// If any login provider is used, then the username may not be changed
if user.AuthService != "" && tryingToChange(&user.Username, patch.Username) {
return "username"
}
LdapSettings := &a.Config().LdapSettings
SamlSettings := &a.Config().SamlSettings
conflictField := ""
if a.Ldap() != nil &&
(user.IsLDAPUser() || (user.IsSAMLUser() && *SamlSettings.EnableSyncWithLdap)) {
conflictField = a.Ldap().CheckProviderAttributes(LdapSettings, user, patch)
} else if a.Saml() != nil && user.IsSAMLUser() {
conflictField = a.Saml().CheckProviderAttributes(SamlSettings, user, patch)
} else if user.IsOAuthUser() {
if tryingToChange(&user.FirstName, patch.FirstName) || tryingToChange(&user.LastName, patch.LastName) {
conflictField = "full name"
}
}
return conflictField
}
func (a *App) PatchUser(c request.CTX, userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
user.Patch(patch)
updatedUser, err := a.UpdateUser(c, user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
func (a *App) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
userAuth.Password = ""
if _, err := a.Srv().Store().User().UpdateAuthData(userID, userAuth.AuthService, userAuth.AuthData, "", false); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return userAuth, nil
}
func (a *App) sendUpdatedUserEvent(user model.User) {
// exclude event creator user from admin, member user broadcast
omitUsers := make(map[string]bool, 1)
omitUsers[user.Id] = true
// declare admin and unsanitized copy of user
adminCopyOfUser := user.DeepCopy()
unsanitizedCopyOfUser := user.DeepCopy()
a.SanitizeProfile(adminCopyOfUser, true)
adminMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
adminMessage.Add("user", adminCopyOfUser)
adminMessage.GetBroadcast().ContainsSensitiveData = true
a.Publish(adminMessage)
a.SanitizeProfile(&user, false)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
message.Add("user", &user)
message.GetBroadcast().ContainsSanitizedData = true
a.Publish(message)
// send unsanitized user to event creator
sourceUserMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", unsanitizedCopyOfUser.Id, nil, "")
sourceUserMessage.Add("user", unsanitizedCopyOfUser)
a.Publish(sourceUserMessage)
}
func (a *App) isUniqueToGroupNames(val string) *model.AppError {
if val == "" {
return nil
}
var notFoundErr *store.ErrNotFound
group, err := a.Srv().Store().Group().GetByName(val, model.GroupSearchOpts{})
if err != nil && !errors.As(err, ¬FoundErr) {
return model.NewAppError("isUniqueToGroupNames", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if group != nil {
return model.NewAppError("isUniqueToGroupNames", model.NoTranslation, nil, fmt.Sprintf("group name %s exists", val), http.StatusBadRequest)
}
return nil
}
func (a *App) UpdateUser(c request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
prev, err := a.ch.srv.userService.GetUser(user.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpdateUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if prev.CreateAt != user.CreateAt {
user.CreateAt = prev.CreateAt
}
if user.Username != prev.Username {
if err := a.isUniqueToGroupNames(user.Username); err != nil {
err.Where = "UpdateUser"
return nil, err
}
}
var newEmail string
if user.Email != prev.Email {
if !users.CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
if !prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if !users.CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
if prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_guest_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if *a.Config().EmailSettings.RequireEmailVerification {
newEmail = user.Email
// Don't set new eMail on user account if email verification is required, this will be done as a post-verification action
// to avoid users being able to set non-controlled eMails as their account email
if _, appErr := a.GetUserByEmail(newEmail); appErr == nil {
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "user_id="+user.Id, http.StatusBadRequest)
}
// When a bot is created, prev.Email will be an autogenerated faked email,
// which will not match a CLI email input during bot to user conversions.
// To update a bot users email, do not set the email to the faked email
// stored in prev.Email. Allow using the email defined in the CLI
if !user.IsBot {
user.Email = prev.Email
}
}
}
userUpdate, err := a.ch.srv.userService.UpdateUser(user, false)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var conErr *store.ErrConflict
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &conErr):
if conErr.Resource == "Username" {
return nil, model.NewAppError("UpdateUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if sendNotifications {
if userUpdate.New.Email != userUpdate.Old.Email || newEmail != "" {
if *a.Config().EmailSettings.RequireEmailVerification {
a.Srv().Go(func() {
if err := a.SendEmailVerification(userUpdate.New, newEmail, ""); err != nil {
c.Logger().Error("Failed to send email verification", mlog.Err(err))
}
})
} else {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send email change email", mlog.Err(err))
}
})
}
}
if userUpdate.New.Username != userUpdate.Old.Username {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send change username email", mlog.Err(err))
}
})
}
a.sendUpdatedUserEvent(*userUpdate.New)
}
a.InvalidateCacheForUser(user.Id)
a.onUserProfileChange(user.Id)
return userUpdate.New, nil
}
func (a *App) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if _, err = a.UpdateActive(c, user, active); err != nil {
return err
}
return nil
}
func (a *App) updateUserNotifyProps(userID string, props map[string]string) *model.AppError {
err := a.ch.srv.userService.UpdateUserNotifyProps(userID, props)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(userID)
a.onUserProfileChange(userID)
return nil
}
func (a *App) UpdateMfa(c request.CTX, activate bool, userID, token string) *model.AppError {
if activate {
if err := a.ActivateMfa(userID, token); err != nil {
return err
}
} else {
if err := a.DeactivateMfa(userID); err != nil {
return err
}
}
a.Srv().Go(func() {
user, err := a.GetUser(userID)
if err != nil {
c.Logger().Error("Failed to get user", mlog.Err(err))
return
}
if err := a.Srv().EmailService.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send mfa change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdatePasswordByUserIdSendEmail(c request.CTX, userID, newPassword, method string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdatePasswordSendEmail(c, user, newPassword, method)
}
func (a *App) UpdatePassword(user *model.User, newPassword string) *model.AppError {
if err := a.IsPasswordValid(newPassword); err != nil {
return err
}
hashedPassword := model.HashPassword(newPassword)
if err := a.Srv().Store().User().UpdatePassword(user.Id, hashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword, method string) *model.AppError {
if err := a.UpdatePassword(user, newPassword); err != nil {
return err
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send password change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdateHashedPasswordByUserId(userID, newHashedPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdateHashedPassword(user, newHashedPassword)
}
func (a *App) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
if err := a.Srv().Store().User().UpdatePassword(user.Id, newHashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) ResetPasswordFromToken(c request.CTX, userSuppliedTokenString, newPassword string) *model.AppError {
return a.resetPasswordFromToken(c, userSuppliedTokenString, newPassword, model.GetMillis())
}
func (a *App) resetPasswordFromToken(c request.CTX, userSuppliedTokenString, newPassword string, nowMilli int64) *model.AppError {
token, err := a.GetPasswordRecoveryToken(userSuppliedTokenString)
if err != nil {
return err
}
if nowMilli-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("resetPassword", "api.user.reset_password.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
if user.Email != tokenData.Email {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
if user.IsSSOUser() {
return model.NewAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
T := i18n.GetUserTranslations(user.Locale)
if err := a.UpdatePasswordSendEmail(c, user, newPassword, T("api.user.reset_password.method")); err != nil {
return err
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
user, err := a.GetUserByEmail(email)
if err != nil {
return false, nil
}
if user.AuthData != nil && *user.AuthData != "" {
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
token, err := a.CreatePasswordRecoveryToken(user.Id, user.Email)
if err != nil {
return false, err
}
result, eErr := a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
if eErr != nil {
return result, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+eErr.Error(), http.StatusInternalServerError)
}
return result, nil
}
func (a *App) CreatePasswordRecoveryToken(userID, email string) (*model.Token, *model.AppError) {
tokenExtra := struct {
UserId string
Email string
}{
userID,
email,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, model.NewAppError("CreatePasswordRecoveryToken", "api.user.create_password_token.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
token := model.NewToken(TokenTypePasswordRecovery, string(jsonData))
if err := a.Srv().Store().Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreatePasswordRecoveryToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return token, nil
}
func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.invalid_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if rtoken.Type != TokenTypePasswordRecovery {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
var status int
switch err.(type) {
case *store.ErrNotFound:
status = http.StatusNotFound
default:
status = http.StatusInternalServerError
}
return nil, model.NewAppError("GetTokenById", "api.user.create_user.signup_link_invalid.app_error", nil, "", status).Wrap(err)
}
return rtoken, nil
}
func (a *App) DeleteToken(token *model.Token) *model.AppError {
err := a.Srv().Store().Token().Delete(token.Token)
if err != nil {
return model.NewAppError("DeleteToken", "app.recover.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) UpdateUserRoles(c request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
return a.UpdateUserRolesWithUser(c, user, newRoles, sendWebSocketEvent)
}
func (a *App) UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
return nil, err
}
user.Roles = newRoles
uchan := make(chan store.StoreResult, 1)
go func() {
userUpdate, err := a.Srv().Store().User().Update(user, true)
uchan <- store.StoreResult{Data: userUpdate, NErr: err}
close(uchan)
}()
schan := make(chan store.StoreResult, 1)
go func() {
id, err := a.Srv().Store().Session().UpdateRoles(user.Id, newRoles)
schan <- store.StoreResult{Data: id, NErr: err}
close(schan)
}()
result := <-uchan
if result.NErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(result.NErr, &appErr):
return nil, appErr
case errors.As(result.NErr, &invErr):
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(result.NErr)
default:
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
ruser := result.Data.(*model.UserUpdate).New
if result := <-schan; result.NErr != nil {
// soft error since the user roles were still updated
c.Logger().Warn("Failed during updating user roles", mlog.Err(result.NErr))
}
a.InvalidateCacheForUser(user.Id)
a.ClearSessionCacheForUser(user.Id)
if sendWebSocketEvent {
message := model.NewWebSocketEvent(model.WebsocketEventUserRoleUpdated, "", "", user.Id, nil, "")
message.Add("user_id", user.Id)
message.Add("roles", newRoles)
a.Publish(message)
}
return ruser, nil
}
func (a *App) PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError {
c.Logger().Warn("Attempting to permanently delete account", mlog.String("user_id", user.Id), mlog.String("user_email", user.Email))
if user.IsInRole(model.SystemAdminRoleId) {
c.Logger().Warn("You are deleting a user that is a system administrator. You may need to set another account as the system administrator using the command line tools.", mlog.String("user_email", user.Email))
}
if _, err := a.UpdateActive(c, user, false); err != nil {
return err
}
if err := a.Srv().Store().Session().PermanentDeleteSessionsByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.session.permanent_delete_sessions_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().UserAccessToken().DeleteAllForUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user_access_token.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().OAuth().PermanentDeleteAuthDataByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.oauth.permanent_delete_auth_data_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_outgoing_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Command().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanentdeleteuser.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Preference().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.preference.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.channel.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Group().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.group.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Post().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteUser", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("PermanentDeleteUser", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
infos, err := a.Srv().Store().FileInfo().GetForUser(user.Id)
if err != nil {
c.Logger().Warn("Error getting file list for user from FileInfoStore", mlog.Err(err))
}
for _, info := range infos {
res, err := a.FileExists(info.Path)
if err != nil {
c.Logger().Warn(
"Error checking existence of file",
mlog.String("path", info.Path),
mlog.Err(err),
)
continue
}
if !res {
c.Logger().Warn("File not found", mlog.String("path", info.Path))
continue
}
err = a.RemoveFile(info.Path)
if err != nil {
c.Logger().Warn(
"Unable to remove file",
mlog.String("path", info.Path),
mlog.Err(err),
)
}
}
// delete directory containing user's profile image
profileImageDirectory := getProfileImageDirectory(user.Id)
profileImagePath := getProfileImagePath(user.Id)
resProfileImageExists, errProfileImageExists := a.FileExists(profileImagePath)
fileHandlingErrorsFound := false
if errProfileImageExists != nil {
fileHandlingErrorsFound = true
mlog.Warn(
"Error checking existence of profile image.",
mlog.String("path", profileImagePath),
mlog.Err(errProfileImageExists),
)
}
if resProfileImageExists {
errRemoveDirectory := a.RemoveDirectory(profileImageDirectory)
if errRemoveDirectory != nil {
fileHandlingErrorsFound = true
mlog.Warn(
"Unable to remove profile image directory",
mlog.String("path", profileImageDirectory),
mlog.Err(errRemoveDirectory),
)
}
}
if _, err := a.Srv().Store().FileInfo().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().User().PermanentDelete(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Audit().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.audit.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Team().RemoveAllMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.team.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
if fileHandlingErrorsFound {
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "Couldn't delete profile image of the user.", http.StatusAccepted)
}
c.Logger().Warn("Permanently deleted account", mlog.String("user_email", user.Email), mlog.String("user_id", user.Id))
return nil
}
func (a *App) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
users, err := a.Srv().Store().User().GetAll()
if err != nil {
return model.NewAppError("PermanentDeleteAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.PermanentDeleteUser(c, user)
}
return nil
}
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
if err != nil {
switch {
case errors.Is(err, email.CreateEmailTokenError):
return model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
default:
return model.NewAppError("CreateVerifyEmailToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, err := a.GetStatus(user.Id); err != nil {
if err.StatusCode != http.StatusNotFound {
return err
}
eErr := a.Srv().EmailService.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
if eErr != nil {
return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, "", http.StatusInternalServerError).Wrap(eErr)
}
return nil
}
if err := a.Srv().EmailService.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token); err != nil {
return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError {
token, err := a.GetVerifyEmailToken(userSuppliedTokenString)
if err != nil {
return err
}
if model.GetMillis()-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
tokenData.Email = strings.ToLower(tokenData.Email)
if err := a.VerifyUserEmail(tokenData.UserId, tokenData.Email); err != nil {
return err
}
if user.Email != tokenData.Email {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.bad_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if rtoken.Type != TokenTypeVerifyEmail {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
// GetTotalUsersStats is used for the DM list total
func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store().User().Count(model.UserCountOptions{
IncludeBotAccounts: true,
ViewRestrictions: viewRestrictions,
})
if err != nil {
return nil, model.NewAppError("GetTotalUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
// GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions.
func (a *App) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store().User().Count(*options)
if err != nil {
return nil, model.NewAppError("GetFilteredUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
if _, err := a.Srv().Store().User().VerifyEmail(userID, email); err != nil {
return model.NewAppError("VerifyUserEmail", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(userID)
user, err := a.GetUser(userID)
if err != nil {
return err
}
a.sendUpdatedUserEvent(*user)
return nil
}
func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
if props.WithoutTeam {
return a.SearchUsersWithoutTeam(props.Term, options)
}
if props.InChannelId != "" {
return a.SearchUsersInChannel(props.InChannelId, props.Term, options)
}
if props.NotInChannelId != "" {
return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, options)
}
if props.NotInTeamId != "" {
return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, options)
}
if props.InGroupId != "" {
return a.SearchUsersInGroup(props.InGroupId, props.Term, options)
}
if props.NotInGroupId != "" {
return a.SearchUsersNotInGroup(props.NotInGroupId, props.Term, options)
}
return a.SearchUsersInTeam(props.TeamId, props.Term, options)
}
func (a *App) SearchUsersInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchInChannel(channelID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInChannel(teamID, channelID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInTeam(teamID, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInTeam(notInTeamId, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchWithoutTeam(term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersWithoutTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchInGroup(groupID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInGroup(groupID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
term = strings.TrimSpace(term)
autocomplete, err := a.Srv().Store().User().AutocompleteUsersInChannel(teamID, channelID, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range autocomplete.InChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
for _, user := range autocomplete.OutOfChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
return autocomplete, nil
}
func (a *App) AutocompleteUsersInTeam(teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
autocomplete := &model.UserAutocompleteInTeam{}
autocomplete.InTeam = users
return autocomplete, nil
}
func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OAuthProvider, service string, tokenUser *model.User) *model.AppError {
oauthUser, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return model.NewAppError("UpdateOAuthUserAttrs", "api.user.update_oauth_user_attrs.get_user.app_error", map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
userAttrsChanged := false
if oauthUser.Username != user.Username {
if existingUser, _ := a.GetUserByUsername(oauthUser.Username); existingUser == nil {
user.Username = oauthUser.Username
userAttrsChanged = true
}
}
if oauthUser.GetFullName() != user.GetFullName() {
user.FirstName = oauthUser.FirstName
user.LastName = oauthUser.LastName
userAttrsChanged = true
}
if oauthUser.Email != user.Email {
if existingUser, _ := a.GetUserByEmail(oauthUser.Email); existingUser == nil {
user.Email = oauthUser.Email
userAttrsChanged = true
}
}
if user.DeleteAt > 0 {
// Make sure they are not disabled
user.DeleteAt = 0
userAttrsChanged = true
}
if userAttrsChanged {
users, err := a.Srv().Store().User().Update(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
user = users.New
a.InvalidateCacheForUser(user.Id)
}
return nil
}
func (a *App) RestrictUsersGetByPermissions(userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
// FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the team excluding bots.
func (a *App) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
teamGroupUsers, err := a.GetTeamGroupUsers(team.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, teamGroupUsers)
}
// FilterNonGroupChannelMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the channel excluding bots
func (a *App) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
channelGroupUsers, err := a.GetChannelGroupUsers(channel.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, channelGroupUsers)
}
// filterNonGroupUsers is a helper function that takes a list of user ids and a list of users
// and returns the list of normal users present in userIDs but not in groupUsers.
func (a *App) filterNonGroupUsers(userIDs []string, groupUsers []*model.User) ([]string, error) {
nonMemberIds := []string{}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, false)
if err != nil {
return nil, err
}
for _, user := range users {
userIsMember := user.IsBot
for _, pu := range groupUsers {
if pu.Id == user.Id {
userIsMember = true
break
}
}
if !userIsMember {
nonMemberIds = append(nonMemberIds, user.Id)
}
}
return nonMemberIds, nil
}
func (a *App) RestrictUsersSearchByPermissions(userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
func (a *App) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
if userID == otherUserId {
return true, nil
}
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return false, err
}
if restrictions == nil {
return true, nil
}
if len(restrictions.Teams) > 0 {
result, err := a.Srv().Store().Team().UserBelongsToTeams(otherUserId, restrictions.Teams)
if err != nil {
return false, model.NewAppError("UserCanSeeOtherUser", "app.team.user_belongs_to_teams.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if result {
return true, nil
}
}
if len(restrictions.Channels) > 0 {
result, err := a.userBelongsToChannels(otherUserId, restrictions.Channels)
if err != nil {
return false, err
}
if result {
return true, nil
}
}
return false, nil
}
func (a *App) userBelongsToChannels(userID string, channelIDs []string) (bool, *model.AppError) {
belongs, err := a.Srv().Store().Channel().UserBelongsToChannels(userID, channelIDs)
if err != nil {
return false, model.NewAppError("userBelongsToChannels", "app.channel.user_belongs_to_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return belongs, nil
}
func (a *App) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError) {
if a.HasPermissionTo(userID, model.PermissionViewMembers) {
return nil, nil
}
teamIDs, nErr := a.Srv().Store().Team().GetUserTeamIds(userID, true)
if nErr != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.team.get_user_team_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
teamIDsWithPermission := []string{}
for _, teamID := range teamIDs {
if a.HasPermissionToTeam(userID, teamID, model.PermissionViewMembers) {
teamIDsWithPermission = append(teamIDsWithPermission, teamID)
}
}
userChannelMembers, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(userID, true, true)
if err != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channelIDs := []string{}
for channelID := range userChannelMembers {
channelIDs = append(channelIDs, channelID)
}
return &model.ViewUsersRestrictions{Teams: teamIDsWithPermission, Channels: channelIDs}, nil
}
// PromoteGuestToUser Convert user's roles and all his membership's roles from
// guest roles to regular user roles.
func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
nErr := a.ch.srv.userService.PromoteGuestToUser(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
userTeams, nErr := a.Srv().Store().Team().GetTeamsByUserId(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, team := range userTeams {
// Soft error if there is an issue joining the default channels
if err := a.JoinDefaultChannels(c, team.Id, user, false, requestorId); err != nil {
c.Logger().Warn("Failed to join default channels", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.String("requestor_id", requestorId), mlog.Err(err))
}
}
promotedUser, err := a.GetUser(user.Id)
if err != nil {
c.Logger().Warn("Failed to get user on promote guest to user", mlog.Err(err))
} else {
a.sendUpdatedUserEvent(*promotedUser)
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest()); uErr != nil {
c.Logger().Warn("Unable to update user sessions", mlog.String("user_id", promotedUser.Id), mlog.Err(uErr))
}
}
teamMembers, err := a.GetTeamMembersForUser(user.Id, "", true)
if err != nil {
c.Logger().Warn("Failed to get team members for user on promote guest to user", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, appErr := a.GetChannelMembersForUser(c, member.TeamId, user.Id)
if appErr != nil {
c.Logger().Warn("Failed to get channel members for user on promote guest to user", mlog.Err(appErr))
}
for _, member := range channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("PromoteGuestToUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
// DemoteUserToGuest Convert user's roles and all his membership's roles from
// regular user roles to guest roles.
func (a *App) DemoteUserToGuest(c request.CTX, user *model.User) *model.AppError {
demotedUser, nErr := a.ch.srv.userService.DemoteUserToGuest(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendUpdatedUserEvent(*demotedUser)
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest()); uErr != nil {
c.Logger().Warn("Unable to update user sessions", mlog.String("user_id", demotedUser.Id), mlog.Err(uErr))
}
teamMembers, err := a.GetTeamMembersForUser(user.Id, "", true)
if err != nil {
c.Logger().Warn("Failed to get team members for users on demote user to guest", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, appErr := a.GetChannelMembersForUser(c, member.TeamId, user.Id)
if appErr != nil {
c.Logger().Warn("Failed to get channel members for users on demote user to guest", mlog.Err(appErr))
continue
}
for _, member := range channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("DemoteUserToGuest", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
func (a *App) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
omitUsers := make(map[string]bool, 1)
omitUsers[userID] = true
event := model.NewWebSocketEvent(model.WebsocketEventTyping, "", channelID, "", omitUsers, "")
event.Add("parent_id", parentId)
event.Add("user_id", userID)
a.Publish(event)
return nil
}
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
func (a *App) invalidateUserCacheAndPublish(userID string) {
a.InvalidateCacheForUser(userID)
user, userErr := a.GetUser(userID)
if userErr != nil {
mlog.Error("Error in getting users profile", mlog.String("user_id", userID), mlog.Err(userErr))
return
}
options := a.Config().GetSanitizeOptions()
user.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", user)
a.Publish(message)
}
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.
func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) {
users, err := a.Srv().Store().User().GetKnownUsers(userID)
if err != nil {
return nil, model.NewAppError("GetKnownUsers", "app.user.get_known_users.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// ConvertBotToUser converts a bot to user.
func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
user, nErr := a.Srv().Store().User().Get(c.Context(), bot.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("ConvertBotToUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("ConvertBotToUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if sysadmin && !user.IsInRole(model.SystemAdminRoleId) {
_, appErr := a.UpdateUserRoles(c,
user.Id,
fmt.Sprintf("%s %s", user.Roles, model.SystemAdminRoleId),
false)
if appErr != nil {
return nil, appErr
}
}
user.Patch(userPatch)
user, err := a.UpdateUser(c, user, false)
if err != nil {
return nil, err
}
err = a.UpdatePassword(user, *userPatch.Password)
if err != nil {
return nil, err
}
appErr := a.Srv().Store().Bot().PermanentDelete(bot.UserId)
if appErr != nil {
return nil, model.NewAppError("ConvertBotToUser", "app.user.convert_bot_to_user.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
return user, nil
}
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
var result model.Threads
var eg errgroup.Group
postPriorityIsEnabled := a.isPostPriorityEnabled()
if postPriorityIsEnabled {
options.IncludeIsUrgent = true
}
if !options.ThreadsOnly {
eg.Go(func() error {
totalUnreadThreads, err := a.Srv().Store().Thread().GetTotalUnreadThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count unread threads for user id=%s", userID)
}
result.TotalUnreadThreads = totalUnreadThreads
return nil
})
// Unread is a legacy flag that caused GetTotalThreads to compute the same value as
// GetTotalUnreadThreads. If unspecified, do this work normally; otherwise, skip,
// and send back duplicate values down below.
if !options.Unread {
eg.Go(func() error {
totalCount, err := a.Srv().Store().Thread().GetTotalThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.Total = totalCount
return nil
})
}
eg.Go(func() error {
totalUnreadMentions, err := a.Srv().Store().Thread().GetTotalUnreadMentions(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.TotalUnreadMentions = totalUnreadMentions
return nil
})
if postPriorityIsEnabled {
eg.Go(func() error {
totalUnreadUrgentMentions, err := a.Srv().Store().Thread().GetTotalUnreadUrgentMentions(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count urgent mentioned threads for user id=%s", userID)
}
result.TotalUnreadUrgentMentions = totalUnreadUrgentMentions
return nil
})
}
}
if !options.TotalsOnly {
eg.Go(func() error {
threads, err := a.Srv().Store().Thread().GetThreadsForUser(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to get threads for user id=%s", userID)
}
result.Threads = threads
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if options.Unread {
result.Total = result.TotalUnreadThreads
}
for _, thread := range result.Threads {
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
}
return &result, nil
}
func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.ThreadMembership, *model.AppError) {
threadMembership, nErr := a.Srv().Store().Thread().GetMembershipForUser(userId, threadId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.not_found", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return threadMembership, nil
}
func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled())
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound)
default:
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
return thread, nil
}
func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
nErr := a.Srv().Store().Thread().MarkAllAsReadByTeam(userID, teamID)
if nErr != nil {
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
a.Publish(message)
return nil
}
func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError {
opts := store.ThreadMembershipOpts{
Following: state,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: state,
UpdateParticipants: false,
}
_, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
thread, err := a.Srv().Store().Thread().Get(threadID)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
replyCount := int64(0)
if thread != nil {
replyCount = thread.ReplyCount
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadFollowChanged, teamID, "", userID, nil, "")
message.Add("thread_id", threadID)
message.Add("state", state)
message.Add("reply_count", replyCount)
a.Publish(message)
return nil
}
func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, teamID, threadID string) *model.AppError {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
tm, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
post, appErr := a.GetSinglePost(threadID, false)
if appErr != nil {
return appErr
}
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
tm.UnreadMentions, appErr = a.countThreadMentions(c, user, post, teamID, post.CreateAt-1)
if appErr != nil {
return appErr
}
tm.LastViewed = post.CreateAt - 1
_, err = a.Srv().Store().Thread().UpdateMembership(tm)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled())
if err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return nil
}
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.sanitizeProfiles(userThread.Participants, false)
userThread.Post.SanitizeProps()
sanitizedPost, appErr := a.SanitizePostMetadataForUser(c, userThread.Post, userID)
if appErr != nil {
return appErr
}
userThread.Post = sanitizedPost
payload, jsonErr := json.Marshal(userThread)
if jsonErr != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("thread", string(payload))
message.Add("previous_unread_replies", int64(0))
message.Add("previous_unread_mentions", int64(0))
a.Publish(message)
return nil
}
func (a *App) UpdateThreadReadForUserByPost(c request.CTX, currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
if post.RootId != threadID && postID != threadID {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user_by_post.app_error", nil, "", http.StatusBadRequest)
}
return a.UpdateThreadReadForUser(c, currentSessionId, userID, teamID, threadID, post.CreateAt-1)
}
func (a *App) UpdateThreadReadForUser(c request.CTX, currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
opts := store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
}
membership, storeErr := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if storeErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
previousUnreadMentions := membership.UnreadMentions
previousUnreadReplies, nErr := a.Srv().Store().Thread().GetThreadUnreadReplyCount(membership)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
post, err := a.GetSinglePost(threadID, false)
if err != nil {
return nil, err
}
membership.UnreadMentions, err = a.countThreadMentions(c, user, post, teamID, timestamp)
if err != nil {
return nil, err
}
_, nErr = a.Srv().Store().Thread().UpdateMembership(membership)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
membership.LastViewed = timestamp
nErr = a.Srv().Store().Thread().MarkAsRead(userID, threadID, timestamp)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
thread, err := a.GetThreadForUser(membership, false)
if err != nil {
return nil, err
}
// Clear if user has read the messages
if thread.UnreadReplies == 0 && a.IsCRTEnabledForUser(c, userID) {
a.clearPushNotification(currentSessionId, userID, post.ChannelId, threadID)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
message.Add("thread_id", threadID)
message.Add("timestamp", timestamp)
message.Add("unread_mentions", membership.UnreadMentions)
message.Add("unread_replies", thread.UnreadReplies)
message.Add("previous_unread_mentions", previousUnreadMentions)
message.Add("previous_unread_replies", previousUnreadReplies)
message.Add("channel_id", post.ChannelId)
a.Publish(message)
return thread, nil
}
func (a *App) GetUsersWithInvalidEmails(page int, perPage int) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetUsersWithInvalidEmails(page, perPage, *a.Config().TeamSettings.RestrictCreationToDomains)
if err != nil {
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func getProfileImagePath(userID string) string {
return filepath.Join("users", userID, "profile.png")
}
func getProfileImageDirectory(userID string) string {
return filepath.Join("users", userID)
}
func (a *App) UserIsFirstAdmin(user *model.User) bool {
if !user.IsSystemAdmin() {
return false
}
systemAdminUsers, errServer := a.Srv().Store().User().GetSystemAdminProfiles()
if errServer != nil {
mlog.Warn("Failed to get system admins to check for first admin from Mattermost.")
return false
}
for _, systemAdminUser := range systemAdminUsers {
systemAdminUser := systemAdminUser
if systemAdminUser.CreateAt < user.CreateAt {
return false
}
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"strings"
"github.com/avct/uasurfer"
)
var platformNames = map[uasurfer.Platform]string{
uasurfer.PlatformUnknown: "Windows",
uasurfer.PlatformWindows: "Windows",
uasurfer.PlatformMac: "Macintosh",
uasurfer.PlatformLinux: "Linux",
uasurfer.PlatformiPad: "iPad",
uasurfer.PlatformiPhone: "iPhone",
uasurfer.PlatformiPod: "iPod",
uasurfer.PlatformBlackberry: "BlackBerry",
uasurfer.PlatformWindowsPhone: "Windows Phone",
}
func getPlatformName(ua *uasurfer.UserAgent) string {
platform := ua.OS.Platform
name, ok := platformNames[platform]
if !ok {
return platformNames[uasurfer.PlatformUnknown]
}
return name
}
var osNames = map[uasurfer.OSName]string{
uasurfer.OSUnknown: "",
uasurfer.OSWindowsPhone: "Windows Phone",
uasurfer.OSWindows: "Windows",
uasurfer.OSMacOSX: "Mac OS",
uasurfer.OSiOS: "iOS",
uasurfer.OSAndroid: "Android",
uasurfer.OSBlackberry: "BlackBerry",
uasurfer.OSChromeOS: "Chrome OS",
uasurfer.OSKindle: "Kindle",
uasurfer.OSWebOS: "webOS",
uasurfer.OSLinux: "Linux",
}
func getOSName(ua *uasurfer.UserAgent) string {
os := ua.OS
if os.Name == uasurfer.OSWindows {
major := os.Version.Major
minor := os.Version.Minor
switch {
case major == 5 && minor == 0:
return "Windows 2000"
case major == 5 && minor == 1:
return "Windows XP"
case major == 5 && minor == 2:
return "Windows XP x64 Edition"
case major == 6 && minor == 0:
return "Windows Vista"
case major == 6 && minor == 1:
return "Windows 7"
case major == 6 && minor == 2:
return "Windows 8"
case major == 6 && minor == 3:
return "Windows 8.1"
case major == 10:
return "Windows 10"
default:
return "Windows"
}
}
name, ok := osNames[os.Name]
if ok {
return name
}
return osNames[uasurfer.OSUnknown]
}
func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {
if index := strings.Index(userAgentString, "Mattermost/"); index != -1 {
afterVersion := userAgentString[index+len("Mattermost/"):]
return strings.Fields(afterVersion)[0]
}
if index := strings.Index(userAgentString, "mmctl/"); index != -1 {
afterVersion := userAgentString[index+len("mmctl/"):]
return strings.Fields(afterVersion)[0]
}
if index := strings.Index(userAgentString, "Franz/"); index != -1 {
afterVersion := userAgentString[index+len("Franz/"):]
return strings.Fields(afterVersion)[0]
}
return getUAVersion(ua.Browser.Version)
}
func getUAVersion(version uasurfer.Version) string {
if version.Patch == 0 {
return fmt.Sprintf("%v.%v", version.Major, version.Minor)
}
return fmt.Sprintf("%v.%v.%v", version.Major, version.Minor, version.Patch)
}
var browserNames = map[uasurfer.BrowserName]string{
uasurfer.BrowserUnknown: "Unknown",
uasurfer.BrowserChrome: "Chrome",
uasurfer.BrowserIE: "Internet Explorer",
uasurfer.BrowserSafari: "Safari",
uasurfer.BrowserFirefox: "Firefox",
uasurfer.BrowserAndroid: "Android",
uasurfer.BrowserOpera: "Opera",
uasurfer.BrowserBlackberry: "BlackBerry",
}
func getBrowserName(ua *uasurfer.UserAgent, userAgentString string) string {
browser := ua.Browser.Name
if strings.Contains(userAgentString, "Mattermost") {
return "Desktop App"
}
if strings.Contains(userAgentString, "mmctl") {
return "mmctl"
}
if browser == uasurfer.BrowserIE && ua.Browser.Version.Major > 11 {
return "Edge"
}
if name, ok := browserNames[browser]; ok {
return name
}
return browserNames[uasurfer.BrowserUnknown]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) {
u, err := a.Srv().Store().UserTermsOfService().GetByUser(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserTermsOfService", "app.user_terms_of_service.get_by_user.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserTermsOfService", "app.user_terms_of_service.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return u, nil
}
func (a *App) SaveUserTermsOfService(userID, termsOfServiceId string, accepted bool) *model.AppError {
if accepted {
userTermsOfService := &model.UserTermsOfService{
UserId: userID,
TermsOfServiceId: termsOfServiceId,
}
if _, err := a.Srv().Store().UserTermsOfService().Save(userTermsOfService); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("SaveUserTermsOfService", "app.user_terms_of_service.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
} else {
if err := a.Srv().Store().UserTermsOfService().Delete(userID, termsOfServiceId); err != nil {
return model.NewAppError("SaveUserTermsOfService", "app.user_terms_of_service.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import "errors"
var (
AcceptedDomainError = errors.New("the email provided does not belong to an accepted domain")
VerifyUserError = errors.New("could not update verify email field")
UserCountError = errors.New("could not get the total number of the users.")
UserCreationDisabledError = errors.New("user creation is not allowed")
UserStoreIsEmptyError = errors.New("could not check if the user store is empty")
DeleteAllAccessDataError = errors.New("could not delete all access data")
DefaultFontError = errors.New("could not get default font")
UserInitialsError = errors.New("could not get user initials")
ImageEncodingError = errors.New("could not encode image")
)
// ErrInvalidPassword indicates an error against the password settings
type ErrInvalidPassword struct {
id string
}
func NewErrInvalidPassword(id string) *ErrInvalidPassword {
return &ErrInvalidPassword{
id: id,
}
}
func (e *ErrInvalidPassword) Error() string {
return "invalid password"
}
func (e *ErrInvalidPassword) Id() string {
return e.id
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"errors"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/mattermost/mattermost-server/v6/model"
)
func CheckUserPassword(user *model.User, password string) error {
if err := ComparePassword(user.Password, password); err != nil {
return NewErrInvalidPassword("")
}
return nil
}
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
return string(hash)
}
func ComparePassword(hash string, password string) error {
if password == "" || hash == "" {
return errors.New("empty password or hash")
}
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
func (us *UserService) isPasswordValid(password string) error {
return IsPasswordValidWithSettings(password, &us.config().PasswordSettings)
}
// IsPasswordValidWithSettings is a utility functions that checks if the given password
// conforms to the password settings. It returns the error id as error value.
func IsPasswordValidWithSettings(password string, settings *model.PasswordSettings) error {
id := "model.user.is_valid.pwd"
isError := false
if len(password) < *settings.MinimumLength || len(password) > model.PasswordMaximumLength {
isError = true
}
if *settings.Lowercase {
if !strings.ContainsAny(password, model.LowercaseLetters) {
isError = true
}
id = id + "_lowercase"
}
if *settings.Uppercase {
if !strings.ContainsAny(password, model.UppercaseLetters) {
isError = true
}
id = id + "_uppercase"
}
if *settings.Number {
if !strings.ContainsAny(password, model.NUMBERS) {
isError = true
}
id = id + "_number"
}
if *settings.Symbol {
if !strings.ContainsAny(password, model.SYMBOLS) {
isError = true
}
id = id + "_symbol"
}
if isError {
return NewErrInvalidPassword(id + ".app_error")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"bytes"
"hash/fnv"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
)
const (
imageProfilePixelDimension = 128
)
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
if *us.config().FileSettings.DriverName == "" {
img, err := us.GetDefaultProfileImage(user)
if err != nil {
return nil, false, err
}
return img, false, nil
}
path := path.Join("users", user.Id, "profile.png")
data, err := us.ReadFile(path)
if err != nil {
img, appErr := us.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
license := us.license()
insecure := us.config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err := filestore.NewFileBackend(us.config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err != nil {
return nil, err
}
return backend, nil
}
func (us *UserService) ReadFile(path string) ([]byte, error) {
backend, err := us.FileBackend()
if err != nil {
return nil, err
}
result, nErr := backend.ReadFile(path)
if nErr != nil {
return nil, nErr
}
return result, nil
}
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
backend, err := us.FileBackend()
if err != nil {
return 0, err
}
result, nErr := backend.WriteFile(fr, path)
if nErr != nil {
return result, nErr
}
return result, nil
}
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
if user.IsBot {
return botDefaultImage, nil
}
return createProfileImage(user.Username, user.Id, *us.config().FileSettings.InitialFont)
}
func createProfileImage(username string, userID string, initialFont string) ([]byte, error) {
colors := []color.NRGBA{
{197, 8, 126, 255},
{227, 207, 18, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
}
h := fnv.New32a()
h.Write([]byte(userID))
seed := h.Sum32()
initial := string(strings.ToUpper(username)[0])
font, err := getFont(initialFont)
if err != nil {
return nil, DefaultFontError
}
color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, imageProfilePixelDimension, imageProfilePixelDimension))
srcImg := image.White
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
size := float64(imageProfilePixelDimension / 2)
c := freetype.NewContext()
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(dstImg.Bounds())
c.SetDst(dstImg)
c.SetSrc(srcImg)
pt := freetype.Pt(imageProfilePixelDimension/5, imageProfilePixelDimension*2/3)
_, err = c.DrawString(initial, pt)
if err != nil {
return nil, UserInitialsError
}
buf := new(bytes.Buffer)
enc := png.Encoder{
CompressionLevel: png.BestCompression,
}
if imgErr := enc.Encode(buf, dstImg); imgErr != nil {
return nil, ImageEncodingError
}
return buf.Bytes(), nil
}
func getFont(initialFont string) (*truetype.Font, error) {
// Some people have the old default font still set, so just treat that as if they're using the new default
if initialFont == "luximbi.ttf" {
initialFont = "nunito-bold.ttf"
}
fontDir, _ := fileutils.FindDir("fonts")
fontBytes, err := os.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, err
}
return freetype.ParseFont(fontBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type UserService struct {
store store.UserStore
sessionStore store.SessionStore
oAuthStore store.OAuthStore
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
config func() *model.Config
license func() *model.License
}
// ServiceConfig is used to initialize the UserService.
type ServiceConfig struct {
// Mandatory fields
UserStore store.UserStore
SessionStore store.SessionStore
OAuthStore store.OAuthStore
ConfigFn func() *model.Config
LicenseFn func() *model.License
// Optional fields
Metrics einterfaces.MetricsInterface
Cluster einterfaces.ClusterInterface
}
func New(c ServiceConfig) (*UserService, error) {
if err := c.validate(); err != nil {
return nil, err
}
return &UserService{
store: c.UserStore,
sessionStore: c.SessionStore,
oAuthStore: c.OAuthStore,
config: c.ConfigFn,
license: c.LicenseFn,
metrics: c.Metrics,
cluster: c.Cluster,
}, nil
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.UserStore == nil || c.SessionStore == nil || c.OAuthStore == nil || c.LicenseFn == nil {
return errors.New("required parameters are not provided")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"context"
"encoding/base64"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
type UserCreateOptions struct {
Guest bool
FromImport bool
}
// CreateUser creates a user
func (us *UserService) CreateUser(user *model.User, opts UserCreateOptions) (*model.User, error) {
if opts.FromImport {
return us.createUser(user)
}
user.Roles = model.SystemUserRoleId
if opts.Guest {
user.Roles = model.SystemGuestRoleId
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *us.config().TeamSettings.RestrictCreationToDomains) {
return nil, AcceptedDomainError
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *us.config().GuestAccountsSettings.RestrictCreationToDomains) {
return nil, AcceptedDomainError
}
// Below is a special case where the first user in the entire
// system is granted the system_admin role
if ok, err := us.store.IsEmpty(true); err != nil {
return nil, errors.Wrap(UserStoreIsEmptyError, err.Error())
} else if ok {
user.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
}
if _, ok := i18n.GetSupportedLocales()[user.Locale]; !ok {
user.Locale = *us.config().LocalizationSettings.DefaultClientLocale
}
return us.createUser(user)
}
func (us *UserService) createUser(user *model.User) (*model.User, error) {
user.MakeNonNil()
if err := us.isPasswordValid(user.Password); user.AuthService == "" && err != nil {
return nil, err
}
ruser, err := us.store.Save(user)
if err != nil {
return nil, err
}
if user.EmailVerified {
if err := us.verifyUserEmail(ruser.Id, user.Email); err != nil {
mlog.Warn("Failed to set email verified", mlog.Err(err))
}
}
// Determine whether to send the created user a welcome email
ruser.DisableWelcomeEmail = user.DisableWelcomeEmail
ruser.Sanitize(map[string]bool{})
return ruser, nil
}
func (us *UserService) verifyUserEmail(userID, email string) error {
if _, err := us.store.VerifyEmail(userID, email); err != nil {
return VerifyUserError
}
return nil
}
func (us *UserService) GetUser(userID string) (*model.User, error) {
return us.store.Get(context.Background(), userID)
}
func (us *UserService) GetUsers(userIDs []string) ([]*model.User, error) {
return us.store.GetMany(context.Background(), userIDs)
}
func (us *UserService) GetUserByUsername(username string) (*model.User, error) {
return us.store.GetByUsername(username)
}
func (us *UserService) GetUserByEmail(email string) (*model.User, error) {
return us.store.GetByEmail(email)
}
func (us *UserService) GetUserByAuth(authData *string, authService string) (*model.User, error) {
return us.store.GetByAuth(authData, authService)
}
func (us *UserService) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetAllProfiles(options)
}
func (us *UserService) GetUsersByUsernames(usernames []string, options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetProfilesByUsernames(usernames, options.ViewRestrictions)
}
func (us *UserService) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersFromProfiles(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersEtag(restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForAllProfiles(), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, error) {
allowFromCache := options.ViewRestrictions == nil
users, err := us.store.GetProfileByIds(context.Background(), userIDs, options, allowFromCache)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, options.IsAdmin), nil
}
func (us *UserService) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetProfiles(options)
}
func (us *UserService) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
return us.store.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
}
func (us *UserService) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersInTeam(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
users, err := us.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfiles(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfilesNotInTeam(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersWithoutTeam(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
users, err := us.store.GetProfilesWithoutTeam(options)
if err != nil {
return nil, err
}
return users, nil
}
func (us *UserService) UpdateUser(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
return us.store.Update(user, allowRoleUpdate)
}
func (us *UserService) UpdateUserNotifyProps(userID string, props map[string]string) error {
return us.store.UpdateNotifyProps(userID, props)
}
func (us *UserService) DeactivateAllGuests() ([]string, error) {
users, err := us.store.DeactivateGuests()
if err != nil {
return nil, err
}
return users, nil
}
func (us *UserService) InvalidateCacheForUser(userID string) {
us.store.InvalidateProfilesInChannelCacheByUser(userID)
us.store.InvalidateProfileCacheForUser(userID)
if us.cluster != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUser,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
us.cluster.SendClusterMessage(msg)
}
}
func (us *UserService) GenerateMfaSecret(user *model.User) (*model.MfaSecret, error) {
secret, img, err := mfa.New(us.store).GenerateSecret(*us.config().ServiceSettings.SiteURL, user.Email, user.Id)
if err != nil {
return nil, err
}
// Make sure the old secret is not cached on any cluster nodes.
us.InvalidateCacheForUser(user.Id)
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: base64.StdEncoding.EncodeToString(img)}
return mfaSecret, nil
}
func (us *UserService) ActivateMfa(user *model.User, token string) error {
return mfa.New(us.store).Activate(user.MfaSecret, user.Id, token)
}
func (us *UserService) DeactivateMfa(user *model.User) error {
return mfa.New(us.store).Deactivate(user.Id)
}
func (us *UserService) PromoteGuestToUser(user *model.User) error {
return us.store.PromoteGuestToUser(user.Id)
}
func (us *UserService) DemoteUserToGuest(user *model.User) (*model.User, error) {
return us.store.DemoteUserToGuest(user.Id)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// CheckUserDomain checks that a user's email domain matches a list of space-delimited domains as a string.
func CheckUserDomain(user *model.User, domains string) bool {
return CheckEmailDomain(user.Email, domains)
}
// CheckEmailDomain checks that an email domain matches a list of space-delimited domains as a string.
func CheckEmailDomain(email string, domains string) bool {
if domains == "" {
return true
}
domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
for _, d := range domainArray {
if strings.HasSuffix(strings.ToLower(email), "@"+d) {
return true
}
}
return false
}
func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
for _, u := range users {
us.SanitizeProfile(u, asAdmin)
}
return users
}
func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) {
options := us.GetSanitizeOptions(asAdmin)
user.SanitizeProfile(options)
}
func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool {
options := us.config().GetSanitizeOptions()
if asAdmin {
options["email"] = true
options["fullname"] = true
options["authservice"] = true
}
return options
}
// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid.
func (us *UserService) IsUsernameTaken(name string) bool {
if !model.IsValidUsername(name) {
return false
}
if _, err := us.store.GetByUsername(name); err != nil {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
// PopulateWebConnConfig checks if the connection id already exists in the hub,
// and if so, accordingly populates the other fields of the webconn.
func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) {
return a.Srv().Platform().PopulateWebConnConfig(s, cfg, seqVal)
}
// NewWebConn returns a new WebConn instance.
func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
return a.Srv().Platform().NewWebConn(cfg, a, a.ch)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
func (a *App) TotalWebsocketConnections() int {
return a.Srv().Platform().TotalWebsocketConnections()
}
func (a *App) GetHubForUserId(userID string) *platform.Hub {
return a.Srv().Platform().GetHubForUserId(userID)
}
// HubRegister registers a connection to a hub.
func (a *App) HubRegister(webConn *platform.WebConn) {
a.Srv().Platform().HubRegister(webConn)
}
// HubUnregister unregisters a connection from a hub.
func (a *App) HubUnregister(webConn *platform.WebConn) {
a.Srv().Platform().HubUnregister(webConn)
}
func (a *App) Publish(message *model.WebSocketEvent) {
a.Srv().Platform().Publish(message)
}
func (ch *Channels) Publish(message *model.WebSocketEvent) {
ch.srv.Platform().Publish(message)
}
func (a *App) invalidateCacheForChannelMembers(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelMembers(channelID)
}
func (a *App) invalidateCacheForChannelMembersNotifyProps(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (a *App) invalidateCacheForChannelPosts(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelPosts(channelID)
}
func (a *App) InvalidateCacheForUser(userID string) {
a.Srv().Platform().InvalidateCacheForUser(userID)
}
func (a *App) invalidateCacheForUserTeams(userID string) {
a.Srv().Platform().InvalidateCacheForUserTeams(userID)
}
// UpdateWebConnUserActivity sets the LastUserActivityAt of the hub for the given session.
func (a *App) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
a.Srv().Platform().UpdateWebConnUserActivity(session, activityAt)
}
// SessionIsRegistered determines if a specific session has been registered
func (a *App) SessionIsRegistered(session model.Session) bool {
return a.Srv().Platform().SessionIsRegistered(session)
}
func (a *App) CheckWebConn(userID, connectionID string) *platform.CheckConnResult {
return a.Srv().Platform().CheckWebConn(userID, connectionID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"regexp"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TriggerwordsExactMatch = 0
TriggerwordsStartsWith = 1
MaxIntegrationResponseSize = 1024 * 1024 // Posts can be <100KB at most, so this is likely more than enough
)
func (a *App) handleWebhookEvents(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil
}
if channel.Type != model.ChannelTypeOpen {
return nil
}
hooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(team.Id, -1, -1)
if err != nil {
return model.NewAppError("handleWebhookEvents", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(hooks) == 0 {
return nil
}
var firstWord, triggerWord string
splitWords := strings.Fields(post.Message)
if len(splitWords) > 0 {
firstWord = splitWords[0]
}
relevantHooks := []*model.OutgoingWebhook{}
for _, hook := range hooks {
if hook.ChannelId == post.ChannelId || hook.ChannelId == "" {
if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
relevantHooks = append(relevantHooks, hook)
triggerWord = ""
} else if hook.TriggerWhen == TriggerwordsExactMatch && hook.TriggerWordExactMatch(firstWord) {
relevantHooks = append(relevantHooks, hook)
triggerWord = hook.GetTriggerWord(firstWord, true)
} else if hook.TriggerWhen == TriggerwordsStartsWith && hook.TriggerWordStartsWith(firstWord) {
relevantHooks = append(relevantHooks, hook)
triggerWord = hook.GetTriggerWord(firstWord, false)
}
}
}
for _, hook := range relevantHooks {
payload := &model.OutgoingWebhookPayload{
Token: hook.Token,
TeamId: hook.TeamId,
TeamDomain: team.Name,
ChannelId: post.ChannelId,
ChannelName: channel.Name,
Timestamp: post.CreateAt,
UserId: post.UserId,
UserName: user.Username,
PostId: post.Id,
Text: post.Message,
TriggerWord: triggerWord,
FileIds: strings.Join(post.FileIds, ","),
}
a.Srv().Go(func(hook *model.OutgoingWebhook) func() {
return func() {
a.TriggerWebhook(c, payload, hook, post, channel)
}
}(hook))
}
return nil
}
func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
var body io.Reader
var contentType string
if hook.ContentType == "application/json" {
js, err := json.Marshal(payload)
if err != nil {
c.Logger().Warn("Failed to encode to JSON", mlog.Err(err))
}
body = bytes.NewReader(js)
contentType = "application/json"
} else {
body = strings.NewReader(payload.ToFormValues())
contentType = "application/x-www-form-urlencoded"
}
for i := range hook.CallbackURLs {
// Get the callback URL by index to properly capture it for the go func
url := hook.CallbackURLs[i]
a.Srv().Go(func() {
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType)
if err != nil {
c.Logger().Error("Event POST failed.", mlog.Err(err))
return
}
if webhookResp != nil && (webhookResp.Text != nil || len(webhookResp.Attachments) > 0) {
postRootId := ""
if webhookResp.ResponseType == model.OutgoingHookResponseTypeComment {
postRootId = post.Id
}
if len(webhookResp.Props) == 0 {
webhookResp.Props = make(model.StringInterface)
}
webhookResp.Props["webhook_display_name"] = hook.DisplayName
text := ""
if webhookResp.Text != nil {
text = a.ProcessSlackText(*webhookResp.Text)
}
webhookResp.Attachments = a.ProcessSlackAttachments(webhookResp.Attachments)
// attachments is in here for slack compatibility
if len(webhookResp.Attachments) > 0 {
webhookResp.Props["attachments"] = webhookResp.Attachments
}
if *a.Config().ServiceSettings.EnablePostUsernameOverride && hook.Username != "" && webhookResp.Username == "" {
webhookResp.Username = hook.Username
}
if *a.Config().ServiceSettings.EnablePostIconOverride && hook.IconURL != "" && webhookResp.IconURL == "" {
webhookResp.IconURL = hook.IconURL
}
if _, err := a.CreateWebhookPost(c, hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, "", webhookResp.Props, webhookResp.Type, postRootId); err != nil {
c.Logger().Error("Failed to create response post.", mlog.Err(err))
}
}
})
}
}
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string) (*model.OutgoingWebhookResponse, error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(false).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var hookResp model.OutgoingWebhookResponse
if jsonErr := json.NewDecoder(io.LimitReader(resp.Body, MaxIntegrationResponseSize)).Decode(&hookResp); jsonErr != nil {
if jsonErr == io.EOF {
return nil, nil
}
return nil, model.NewAppError("doOutgoingWebhookRequest", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &hookResp, nil
}
func SplitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model.AppError) {
splits := make([]*model.Post, 0)
remainingText := post.Message
base := post.Clone()
base.Message = ""
base.SetProps(make(map[string]any))
for k, v := range post.GetProps() {
if k != "attachments" {
base.AddProp(k, v)
}
}
if utf8.RuneCountInString(model.StringInterfaceToJSON(base.GetProps())) > model.PostPropsMaxUserRunes {
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", http.StatusBadRequest)
}
for utf8.RuneCountInString(remainingText) > maxPostSize {
split := base.Clone()
x := 0
for index := range remainingText {
x++
if x > maxPostSize {
split.Message = remainingText[:index]
remainingText = remainingText[index:]
break
}
}
splits = append(splits, split)
}
split := base.Clone()
split.Message = remainingText
splits = append(splits, split)
attachments, _ := post.GetProp("attachments").([]*model.SlackAttachment)
for _, attachment := range attachments {
newAttachment := *attachment
for {
lastSplit := splits[len(splits)-1]
newProps := make(map[string]any)
for k, v := range lastSplit.GetProps() {
newProps[k] = v
}
origAttachments, _ := newProps["attachments"].([]*model.SlackAttachment)
newProps["attachments"] = append(origAttachments, &newAttachment)
newPropsString := model.StringInterfaceToJSON(newProps)
runeCount := utf8.RuneCountInString(newPropsString)
if runeCount <= model.PostPropsMaxUserRunes {
lastSplit.SetProps(newProps)
break
}
if len(origAttachments) > 0 {
newSplit := base.Clone()
splits = append(splits, newSplit)
continue
}
truncationNeeded := runeCount - model.PostPropsMaxUserRunes
textRuneCount := utf8.RuneCountInString(attachment.Text)
if textRuneCount < truncationNeeded {
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", http.StatusBadRequest)
}
x := 0
for index := range attachment.Text {
x++
if x > textRuneCount-truncationNeeded {
newAttachment.Text = newAttachment.Text[:index]
break
}
}
lastSplit.SetProps(newProps)
break
}
}
return splits, nil
}
func (a *App) CreateWebhookPost(c request.CTX, userID string, channel *model.Channel, text, overrideUsername, overrideIconURL, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
// parse links into Markdown format
linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`)
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
post := &model.Post{UserId: userID, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId}
post.AddProp("from_webhook", "true")
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreateWebhookPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if metrics := a.Metrics(); metrics != nil {
metrics.IncrementWebhookPost()
}
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
if overrideUsername != "" {
post.AddProp("override_username", overrideUsername)
} else {
post.AddProp("override_username", model.DefaultWebhookUsername)
}
}
if *a.Config().ServiceSettings.EnablePostIconOverride {
if overrideIconURL != "" {
post.AddProp("override_icon_url", overrideIconURL)
}
if overrideIconEmoji != "" {
post.AddProp("override_icon_emoji", overrideIconEmoji)
}
}
if len(props) > 0 {
for key, val := range props {
if key == "attachments" {
if attachments, success := val.([]*model.SlackAttachment); success {
model.ParseSlackAttachment(post, attachments)
}
} else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
post.AddProp(key, val)
}
}
}
splits, err := SplitWebhookPost(post, a.MaxPostSize())
if err != nil {
return nil, err
}
for _, split := range splits {
if _, err = a.CreatePostMissingChannel(c, split, false, false); err != nil {
return nil, model.NewAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
}
}
return splits[0], nil
}
func (a *App) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hook.UserId = creatorId
hook.TeamId = channel.TeamId
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
hook.Username = ""
}
if !*a.Config().ServiceSettings.EnablePostIconOverride {
hook.IconURL = ""
}
if hook.Username != "" && !model.IsValidUsername(hook.Username) {
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
}
webhook, err := a.Srv().Store().Webhook().SaveIncoming(hook)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) UpdateIncomingWebhook(oldHook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
updatedHook.Username = oldHook.Username
}
if !*a.Config().ServiceSettings.EnablePostIconOverride {
updatedHook.IconURL = oldHook.IconURL
}
if updatedHook.Username != "" && !model.IsValidUsername(updatedHook.Username) {
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
}
updatedHook.Id = oldHook.Id
updatedHook.UserId = oldHook.UserId
updatedHook.CreateAt = oldHook.CreateAt
updatedHook.UpdateAt = model.GetMillis()
updatedHook.TeamId = oldHook.TeamId
updatedHook.DeleteAt = oldHook.DeleteAt
newWebhook, err := a.Srv().Store().Webhook().UpdateIncoming(updatedHook)
if err != nil {
return nil, model.NewAppError("UpdateIncomingWebhook", "app.webhooks.update_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForWebhook(oldHook.Id)
return newWebhook, nil
}
func (a *App) DeleteIncomingWebhook(hookID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return model.NewAppError("DeleteIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().Webhook().DeleteIncoming(hookID, model.GetMillis()); err != nil {
return model.NewAppError("DeleteIncomingWebhook", "app.webhooks.delete_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForWebhook(hookID)
return nil
}
func (a *App) GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhook, err := a.Srv().Store().Webhook().GetIncoming(hookID, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) GetIncomingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
return a.GetIncomingWebhooksForTeamPageByUser(teamID, "", page, perPage)
}
func (a *App) GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetIncomingByTeamByUser(teamID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "app.webhooks.get_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetIncomingWebhooksPageByUser(userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetIncomingListByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "app.webhooks.get_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
return a.GetIncomingWebhooksPageByUser("", page, perPage)
}
func (a *App) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if hook.ChannelId != "" {
channel, errCh := a.Srv().Store().Channel().Get(hook.ChannelId, true)
if errCh != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(errCh, &nfErr):
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(errCh)
default:
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(errCh)
}
}
if channel.Type != model.ChannelTypeOpen {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusForbidden)
}
if channel.Type != model.ChannelTypeOpen || channel.TeamId != hook.TeamId {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
}
} else if len(hook.TriggerWords) == 0 {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusBadRequest)
}
allHooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(hook.TeamId, -1, -1)
if err != nil {
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingOutHook := range allHooks {
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, hook.CallbackURLs)
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, hook.TriggerWords)
if existingOutHook.ChannelId == hook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.intersect.app_error", nil, "", http.StatusInternalServerError)
}
}
webhook, err := a.Srv().Store().Webhook().SaveOutgoing(hook)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.override.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) UpdateOutgoingWebhook(c request.CTX, oldHook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if updatedHook.ChannelId != "" {
channel, err := a.GetChannel(c, updatedHook.ChannelId)
if err != nil {
return nil, err
}
if channel.Type != model.ChannelTypeOpen {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.not_open.app_error", nil, "", http.StatusForbidden)
}
if channel.TeamId != oldHook.TeamId {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
}
} else if len(updatedHook.TriggerWords) == 0 {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusInternalServerError)
}
allHooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(oldHook.TeamId, -1, -1)
if err != nil {
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingOutHook := range allHooks {
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, updatedHook.CallbackURLs)
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, updatedHook.TriggerWords)
if existingOutHook.ChannelId == updatedHook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 && existingOutHook.Id != updatedHook.Id {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.update_outgoing.intersect.app_error", nil, "", http.StatusBadRequest)
}
}
updatedHook.CreatorId = oldHook.CreatorId
updatedHook.CreateAt = oldHook.CreateAt
updatedHook.DeleteAt = oldHook.DeleteAt
updatedHook.TeamId = oldHook.TeamId
updatedHook.UpdateAt = model.GetMillis()
webhook, err := a.Srv().Store().Webhook().UpdateOutgoing(updatedHook)
if err != nil {
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.update_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhook, nil
}
func (a *App) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhook, err := a.Srv().Store().Webhook().GetOutgoing(hookID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) GetOutgoingWebhooksPage(page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
return a.GetOutgoingWebhooksPageByUser("", page, perPage)
}
func (a *App) GetOutgoingWebhooksPageByUser(userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingListByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingByChannelByUser(channelID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
return a.GetOutgoingWebhooksForTeamPageByUser(teamID, "", page, perPage)
}
func (a *App) GetOutgoingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingByTeamByUser(teamID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) DeleteOutgoingWebhook(hookID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return model.NewAppError("DeleteOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().Webhook().DeleteOutgoing(hookID, model.GetMillis()); err != nil {
return model.NewAppError("DeleteOutgoingWebhook", "app.webhooks.delete_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("RegenOutgoingWebhookToken", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hook.Token = model.NewId()
webhook, err := a.Srv().Store().Webhook().UpdateOutgoing(hook)
if err != nil {
return nil, model.NewAppError("RegenOutgoingWebhookToken", "app.webhooks.update_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhook, nil
}
func (a *App) HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hchan := make(chan store.StoreResult, 1)
go func() {
webhook, err := a.Srv().Store().Webhook().GetIncoming(hookID, true)
hchan <- store.StoreResult{Data: webhook, NErr: err}
close(hchan)
}()
if req == nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.parse.app_error", nil, "", http.StatusBadRequest)
}
text := req.Text
if text == "" && req.Attachments == nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.text.app_error", nil, "", http.StatusBadRequest)
}
channelName := req.ChannelName
webhookType := req.Type
var hook *model.IncomingWebhook
result := <-hchan
if result.NErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "", http.StatusBadRequest).Wrap(result.NErr)
}
hook = result.Data.(*model.IncomingWebhook)
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), hook.UserId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
if len(req.Props) == 0 {
req.Props = make(model.StringInterface)
}
req.Props["webhook_display_name"] = hook.DisplayName
text = a.ProcessSlackText(text)
req.Attachments = a.ProcessSlackAttachments(req.Attachments)
// attachments is in here for slack compatibility
if len(req.Attachments) > 0 {
req.Props["attachments"] = req.Attachments
webhookType = model.PostTypeSlackAttachment
}
var channel *model.Channel
var cchan chan store.StoreResult
if channelName != "" {
if channelName[0] == '@' {
result, nErr := a.Srv().Store().User().GetByUsername(channelName[1:])
if nErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
ch, err := a.GetOrCreateDirectChannel(c, hook.UserId, result.Id)
if err != nil {
return err
}
channel = ch
} else if channelName[0] == '#' {
cchan = make(chan store.StoreResult, 1)
go func() {
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName[1:], true)
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
close(cchan)
}()
} else {
cchan = make(chan store.StoreResult, 1)
go func() {
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName, true)
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
close(cchan)
}()
}
} else {
var err error
channel, err = a.Srv().Store().Channel().Get(hook.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if channel == nil {
result2 := <-cchan
if result2.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result2.NErr, &nfErr):
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "", http.StatusNotFound).Wrap(result2.NErr)
default:
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(result2.NErr)
}
}
channel = result2.Data.(*model.Channel)
}
if hook.ChannelLocked && hook.ChannelId != channel.Id {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel_locked.app_error", nil, "", http.StatusForbidden)
}
result = <-uchan
if result.NErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "", http.StatusForbidden).Wrap(result.NErr)
}
if channel.Type != model.ChannelTypeOpen && !a.HasPermissionToChannel(c, hook.UserId, channel.Id, model.PermissionReadChannel) {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
}
overrideUsername := hook.Username
if req.Username != "" {
overrideUsername = req.Username
}
overrideIconURL := hook.IconURL
if req.IconURL != "" {
overrideIconURL = req.IconURL
}
_, err := a.CreateWebhookPost(c, hook.UserId, channel, text, overrideUsername, overrideIconURL, req.IconEmoji, req.Props, webhookType, "")
return err
}
func (a *App) CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) {
hook := &model.CommandWebhook{
CommandId: commandID,
UserId: args.UserId,
ChannelId: args.ChannelId,
RootId: args.RootId,
}
savedHook, err := a.Srv().Store().CommandWebhook().Save(hook)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.existing", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return savedHook, nil
}
func (a *App) HandleCommandWebhook(c *request.Context, hookID string, response *model.CommandResponse) *model.AppError {
if response == nil {
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.handle_command_webhook.parse", nil, "", http.StatusBadRequest)
}
hook, nErr := a.Srv().Store().CommandWebhook().Get(hookID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.missing", map[string]any{"hook_id": hookID}, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
cmd, cmdErr := a.Srv().Store().Command().Get(hook.CommandId)
if cmdErr != nil {
var appErr *model.AppError
switch {
case errors.As(cmdErr, &appErr):
return appErr
default:
return model.NewAppError("HandleCommandWebhook", "web.command_webhook.command.app_error", nil, "", http.StatusBadRequest).Wrap(cmdErr)
}
}
args := &model.CommandArgs{
UserId: hook.UserId,
ChannelId: hook.ChannelId,
TeamId: cmd.TeamId,
RootId: hook.RootId,
}
if nErr := a.Srv().Store().CommandWebhook().TryUse(hook.Id, 5); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.invalid", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
_, err := a.HandleCommandResponse(c, cmd, args, response, false)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type WorkTemplateExecutor interface {
CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error)
CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error)
CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error)
InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error
}
type appWorkTemplateExecutor struct {
app *App
}
func (e *appWorkTemplateExecutor) CreatePlaybook(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
playbook *model.WorkTemplatePlaybook,
channel model.WorkTemplateChannel) (string, error) {
// determine playbook name
name := playbook.Name
if wtcr.Name != "" {
name += " " + wtcr.Name
}
// get the correct playbook pbTemplate
pbTemplate, err := wtcr.FindPlaybookTemplate(playbook.Template)
if err != nil {
return "", fmt.Errorf("unable to find playbook template: %w", err)
}
pbTemplate.TeamID = wtcr.TeamID
pbTemplate.Title = name
pbTemplate.Public = wtcr.Visibility == model.WorkTemplateVisibilityPublic
pbTemplate.CreatePublicPlaybookRun = wtcr.Visibility == model.WorkTemplateVisibilityPublic
data, err := json.Marshal(pbTemplate)
if err != nil {
return "", fmt.Errorf("unable to marshal playbook template: %w", err)
}
resp, appErr := e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/playbooks", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook: %w", appErr)
}
defer resp.Body.Close()
pbcResp := playbookCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbcResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook create response: %w", err)
}
runName := channel.Name
if wtcr.Name != "" {
runName = wtcr.Name
}
data, err = json.Marshal(pbclient.PlaybookRunCreateOptions{
Name: runName,
OwnerUserID: c.Session().UserId,
TeamID: wtcr.TeamID,
PlaybookID: pbcResp.ID,
})
if err != nil {
return "", fmt.Errorf("unable to marshal playbook run create request: %w", err)
}
resp, appErr = e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/runs", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook run: %w", appErr)
}
defer resp.Body.Close()
pbrResp := playbookRunCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbrResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook run create response: %w", err)
}
// using pbrResp.ChannelID, update the channel to add metadata
dbChannel, err := e.app.Srv().Store().Channel().Get(pbrResp.ChannelID, false)
if err != nil {
return "", fmt.Errorf("unable to find channel: %w", err)
}
if dbChannel == nil {
return "", fmt.Errorf("channel not found")
}
dbChannel.AddProp(model.WorkTemplateIDChannelProp, wtcr.WorkTemplate.ID)
_, err = e.app.Srv().Store().Channel().Update(dbChannel)
if err != nil {
e.app.Srv().Log().Error("Failed to update playbook channel metadata", mlog.Err(err))
}
return pbrResp.ChannelID, nil
}
func (e *appWorkTemplateExecutor) CreateChannel(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cChannel *model.WorkTemplateChannel,
) (string, error) {
channelID := ""
channelDisplayName := cChannel.Name
if wtcr.Name != "" {
channelDisplayName = wtcr.Name
}
var channelCreationAppErr *model.AppError = &model.AppError{}
cleanChannelName := cleanChannelName(channelDisplayName)
channelName := cleanChannelName
if len(channelName) > model.ChannelNameMaxLength {
channelName = channelName[:model.ChannelNameMaxLength]
}
// Mostly because of the "quick use" feature, we might try to create channel that have the exact same "Name"
// This loop ensures that if the original name is taken, we try again by adding a suffix to the Name
for channelCreationAppErr != nil {
// create channel
var newChan *model.Channel
newChan, channelCreationAppErr = e.app.CreateChannelWithUser(c, &model.Channel{
TeamId: wtcr.TeamID,
Name: channelName,
DisplayName: channelDisplayName,
Type: model.ChannelTypeOpen,
Purpose: cChannel.Purpose,
Props: map[string]any{
model.WorkTemplateIDChannelProp: wtcr.WorkTemplate.ID,
},
}, c.Session().UserId)
if channelCreationAppErr != nil {
if channelCreationAppErr.Id == store.ChannelExistsError {
// compute a new unique name
suffix := fmt.Sprintf("-%s", model.NewId()[0:4])
channelName = cleanChannelName
if len(cleanChannelName)+len(suffix) > model.ChannelNameMaxLength {
channelName = cleanChannelName[:model.ChannelNameMaxLength-len(suffix)]
}
channelName = channelName + suffix
continue
}
return "", fmt.Errorf("error while creating channel: %w", channelCreationAppErr)
}
channelID = newChan.Id
}
return channelID, nil
}
func (e *appWorkTemplateExecutor) CreateBoard(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cBoard *model.WorkTemplateBoard,
linkToChannelID string,
) (string, error) {
boardService := e.app.Srv().services[product.BoardsKey].(product.BoardsService)
templates, err := boardService.GetTemplates("0", c.Session().UserId)
if err != nil {
return "", fmt.Errorf("error while getting templates: %w", err)
}
var template *fb_model.Board = nil
for _, t := range templates {
v, ok := t.Properties["trackingTemplateId"]
if ok && v == cBoard.Template {
template = t
break
}
}
if template == nil {
return "", fmt.Errorf("template not found")
}
title := cBoard.Name
if wtcr.Name != "" {
title += " " + wtcr.Name
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, c.Session().UserId, wtcr.TeamID, false)
if err != nil {
return "", fmt.Errorf("failed to create new board from template: %w", err)
}
if len(boardsAndBlocks.Boards) != 1 {
return "", fmt.Errorf("only one board was expected, found %d", len(boardsAndBlocks.Boards))
}
// Apply patch for the title and linked channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
Title: &title,
ChannelID: &linkToChannelID,
}, boardsAndBlocks.Boards[0].ID, c.Session().UserId)
if err != nil {
return "", fmt.Errorf("failed to patch board: %w", err)
}
return patchedBoard.ID, nil
}
func (e *appWorkTemplateExecutor) InstallPlugin(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cIntegration *model.WorkTemplateIntegration,
sendToChannelID string,
) error {
// check if this plugin is already installed
pluginID := cIntegration.ID
_, appErr := e.app.GetPluginStatus(pluginID)
if appErr != nil {
if appErr.Id == "app.plugin.not_installed.app_error" {
// we install them in the background as we don't want user to wait for this
manifest, installAppErr := e.app.Channels().InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: "",
})
if installAppErr != nil {
return fmt.Errorf("unable to install plugin: %w", installAppErr)
}
if sendToChannelID != "" {
e.app.SendEphemeralPost(c, c.Session().UserId, &model.Post{
ChannelId: sendToChannelID,
Message: fmt.Sprintf("plugin %s has been installed", manifest.Name),
CreateAt: model.GetMillis(),
})
}
} else {
return fmt.Errorf("unable to get plugin status: %w", appErr)
}
}
// get plugin state
if err := e.app.EnablePlugin(pluginID); err != nil {
return fmt.Errorf("unable to enable plugin: %w", err)
}
return nil
}
type playbookCreateResponse struct {
ID string `json:"id"`
}
type playbookRunCreateResponse struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
}
// cleaning channel name code bellow comes from the playbook repository.
var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`)
func cleanChannelName(channelName string) string {
// Lower case only
channelName = strings.ToLower(channelName)
// Trim spaces
channelName = strings.TrimSpace(channelName)
// Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes
channelName = strings.ReplaceAll(channelName, "-", " ")
channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "")
channelName = strings.ReplaceAll(channelName, " ", "-")
// Remove all leading and trailing dashes
channelName = strings.Trim(channelName, "-")
return channelName
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func (a *App) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
categories, err := worktemplates.ListCategories()
if err != nil {
return nil, model.NewAppError("GetWorkTemplateCategories", "app.worktemplates.get_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
modelCategories := make([]*model.WorkTemplateCategory, len(categories))
for i := range categories {
modelCategories[i] = &model.WorkTemplateCategory{
ID: categories[i].ID,
Name: t(categories[i].Name),
}
}
return modelCategories, nil
}
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
templates, err := worktemplates.ListByCategory(category)
if err != nil {
return nil, model.NewAppError("GetWorkTemplates", "app.worktemplates.get_templates.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// filter out templates that are not enabled by feature Flag
enabledTemplates := []*model.WorkTemplate{}
for _, template := range templates {
mTemplate := template.ToModelWorkTemplate(t)
if template.FeatureFlag == nil {
enabledTemplates = append(enabledTemplates, mTemplate)
continue
}
if featureFlags[template.FeatureFlag.Name] == template.FeatureFlag.Value {
enabledTemplates = append(enabledTemplates, mTemplate)
}
}
return enabledTemplates, nil
}
func (a *App) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*WorkTemplateExecutionResult, *model.AppError) {
e := &appWorkTemplateExecutor{app: a}
return a.executeWorkTemplate(c, wtcr, e, installPlugins)
}
func (a *App) executeWorkTemplate(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
e WorkTemplateExecutor,
installPlugins bool,
) (*WorkTemplateExecutionResult, *model.AppError) {
res := &WorkTemplateExecutionResult{
ChannelWithPlaybookIDs: []string{},
ChannelIDs: []string{},
}
if wtcr.Name != "" {
if len(wtcr.Name) > model.ChannelNameMaxLength {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.name_too_long", nil, "", http.StatusBadRequest)
}
}
contentByType := map[string][]model.WorkTemplateContent{
"channel": {},
"board": {},
"playbook": {},
"integration": {},
}
for _, content := range wtcr.WorkTemplate.Content {
if content.Channel != nil {
contentByType["channel"] = append(contentByType["channel"], content)
}
if content.Board != nil {
contentByType["board"] = append(contentByType["board"], content)
}
if content.Playbook != nil {
contentByType["playbook"] = append(contentByType["playbook"], content)
}
if content.Integration != nil {
contentByType["integration"] = append(contentByType["integration"], content)
}
}
firstChannelId := ""
channelIDByWorkTemplateID := map[string]string{}
for _, pbContent := range contentByType["playbook"] {
cPlaybook := pbContent.Playbook
// find associated channel
var associatedChannel *model.WorkTemplateChannel
for _, channelContent := range contentByType["channel"] {
if channelContent.Channel.Playbook == cPlaybook.ID {
associatedChannel = channelContent.Channel
break
}
}
if associatedChannel == nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.find_channel_error", nil, "no associated channel found for playbook", http.StatusInternalServerError)
}
channelID, err := e.CreatePlaybook(c, wtcr, cPlaybook, *associatedChannel)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if firstChannelId == "" {
firstChannelId = channelID
}
res.ChannelWithPlaybookIDs = append(res.ChannelWithPlaybookIDs, channelID)
channelIDByWorkTemplateID[associatedChannel.ID] = channelID
}
// loop through all channels
for _, channelContent := range contentByType["channel"] {
cChannel := channelContent.Channel
// we only need to create a channel if there's no playbook
if cChannel.Playbook == "" {
chanID, err := e.CreateChannel(c, wtcr, cChannel)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.channels.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if firstChannelId == "" {
firstChannelId = chanID
}
res.ChannelIDs = append(res.ChannelIDs, chanID)
channelIDByWorkTemplateID[cChannel.ID] = chanID
}
}
for _, boardContent := range contentByType["board"] {
cBoard := boardContent.Board
channelID := ""
if cBoard.Channel != "" {
channel, ok := channelIDByWorkTemplateID[cBoard.Channel]
if !ok {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.app_error", nil, "no associated channel found for board", http.StatusInternalServerError)
}
channelID = channel
}
_, err := e.CreateBoard(c, wtcr, cBoard, channelID)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.boards.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if installPlugins {
for _, integrationContent := range contentByType["integration"] {
cIntegration := integrationContent.Integration
// this can take a long time so we just start those as background tasks
go e.InstallPlugin(c, wtcr, cIntegration, firstChannelId)
}
}
for _, ch := range res.ChannelWithPlaybookIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
message.Add("channel_id", ch)
message.Add("team_id", wtcr.TeamID)
a.Publish(message)
}
for _, ch := range res.ChannelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
message.Add("channel_id", ch)
message.Add("team_id", wtcr.TeamID)
a.Publish(message)
}
return res, nil
}
type WorkTemplateExecutionResult struct {
ChannelWithPlaybookIDs []string `json:"channel_with_playbook_ids"`
ChannelIDs []string `json:"channel_ids"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"errors"
"net/http"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
"github.com/mattermost/mattermost-server/v6/model"
)
type ExecutionRequest struct {
TeamID string `json:"team_id"`
Name string `json:"name"`
Visibility string `json:"visibility"`
WorkTemplate model.WorkTemplate `json:"work_template"`
PlaybookTemplates []*PlaybookTemplate `json:"playbook_templates"`
foundPlaybookTemplates map[string]*pbclient.PlaybookCreateOptions
}
type PermissionSet struct {
License *model.License
// channels
CanCreatePublicChannel bool
CanCreatePrivateChannel bool
// playbooks
CanCreatePublicPlaybook bool
CanCreatePrivatePlaybook bool
// boards
CanCreatePublicBoard bool
CanCreatePrivateBoard bool
}
func (r *ExecutionRequest) CanBeExecuted(p PermissionSet) *model.AppError {
public := r.Visibility == model.WorkTemplateVisibilityPublic
for _, c := range r.WorkTemplate.Content {
if c.Channel != nil {
if public && !p.CanCreatePublicChannel {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_channel", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivateChannel {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_channel", nil, "", http.StatusForbidden)
}
continue
}
if c.Board != nil {
if public && !p.CanCreatePublicBoard {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_board", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivateBoard {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_board", nil, "", http.StatusForbidden)
}
continue
}
if c.Playbook != nil {
if public && !p.CanCreatePublicPlaybook {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_playbook", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivatePlaybook {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_playbook", nil, "", http.StatusForbidden)
}
// private playbook is an E20/Enterprise feature
if !public && (p.License == nil || (p.License.SkuShortName != model.LicenseShortSkuE20 && p.License.SkuShortName != model.LicenseShortSkuEnterprise)) {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.license_cannot_create_private_playbook", nil, "", http.StatusForbidden)
}
continue
}
}
return nil
}
// FindPlaybookTemplate returns the playbook template with the given title.
// it also feed a cache to avoid looking for the same template twice.
func (r *ExecutionRequest) FindPlaybookTemplate(templateTitle string) (*pbclient.PlaybookCreateOptions, error) {
if r.foundPlaybookTemplates == nil {
r.foundPlaybookTemplates = make(map[string]*pbclient.PlaybookCreateOptions)
}
if pt, ok := r.foundPlaybookTemplates[templateTitle]; ok {
if pt == nil {
return nil, errors.New("playbook template not found")
}
return pt, nil
}
for _, pt := range r.PlaybookTemplates {
if pt.Title == templateTitle {
r.foundPlaybookTemplates[templateTitle] = &pt.Template
return &pt.Template, nil
}
}
r.foundPlaybookTemplates[templateTitle] = nil
return nil, errors.New("playbook template not found")
}
type PlaybookTemplate struct {
Title string `json:"title"`
Template pbclient.PlaybookCreateOptions `json:"template"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type WorkTemplateCategory struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
}
type WorkTemplate struct {
ID string `yaml:"id"`
Category string `yaml:"category"`
UseCase string `yaml:"useCase"`
Illustration string `yaml:"illustration"`
Visibility string `yaml:"visibility"`
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
Description Description `yaml:"description"`
Content []Content `yaml:"content"`
}
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
mwt := &model.WorkTemplate{
ID: wt.ID,
Category: wt.Category,
UseCase: wt.UseCase,
Illustration: wt.Illustration,
Visibility: wt.Visibility,
}
if wt.FeatureFlag != nil {
mwt.FeatureFlag = &model.WorkTemplateFeatureFlag{
Name: wt.FeatureFlag.Name,
Value: wt.FeatureFlag.Value,
}
}
if wt.Description.Channel != nil {
mwt.Description.Channel = &model.DescriptionContent{
Message: wt.Description.Channel.Translate(t),
Illustration: wt.Description.Channel.Illustration,
}
}
if wt.Description.Board != nil {
mwt.Description.Board = &model.DescriptionContent{
Message: wt.Description.Board.Translate(t),
Illustration: wt.Description.Board.Illustration,
}
}
if wt.Description.Playbook != nil {
mwt.Description.Playbook = &model.DescriptionContent{
Message: wt.Description.Playbook.Translate(t),
Illustration: wt.Description.Playbook.Illustration,
}
}
if wt.Description.Integration != nil {
mwt.Description.Integration = &model.DescriptionContent{
Message: wt.Description.Integration.Translate(t),
Illustration: wt.Description.Integration.Illustration,
}
}
for _, content := range wt.Content {
if content.Channel != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Channel: &model.WorkTemplateChannel{
ID: content.Channel.ID,
Name: content.Channel.Name,
Purpose: content.Channel.Purpose,
Playbook: content.Channel.Playbook,
Illustration: content.Channel.Illustration,
},
})
}
if content.Board != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Board: &model.WorkTemplateBoard{
ID: content.Board.ID,
Name: content.Board.Name,
Template: content.Board.Template,
Channel: content.Board.Channel,
Illustration: content.Board.Illustration,
},
})
}
if content.Playbook != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Playbook: &model.WorkTemplatePlaybook{
ID: content.Playbook.ID,
Name: content.Playbook.Name,
Template: content.Playbook.Template,
Illustration: content.Playbook.Illustration,
},
})
}
if content.Integration != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Integration: &model.WorkTemplateIntegration{
ID: content.Integration.ID,
},
})
}
}
return mwt
}
func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
if wt.ID == "" {
return errors.New("id is required")
}
if wt.Category == "" {
return errors.New("category is required")
}
if _, ok := categoryIds[wt.Category]; !ok {
return fmt.Errorf("category %s does not exist", wt.Category)
}
if wt.UseCase == "" {
return errors.New("useCase is required")
}
if wt.Illustration == "" {
return errors.New("illustration is required")
}
if wt.Visibility == "" {
return errors.New("visibility is required")
}
hasChannel := false
hasBoard := false
hasPlaybook := false
hasIntegration := false
foundChannels := map[string]struct{}{}
foundPlaybooks := map[string]struct{}{}
foundBoards := map[string]struct{}{}
foundIntegrations := map[string]struct{}{}
mustHaveChannels := []string{}
mustHavePlaybooks := []string{}
currentIdx := 0
for _, content := range wt.Content {
if content.Channel != nil {
hasChannel = true
if cErr := content.Channel.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundChannels[content.Channel.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate channel %s found", content.Channel.ID), currentIdx)
}
foundChannels[content.Channel.ID] = struct{}{}
if content.Channel.Playbook != "" {
mustHavePlaybooks = append(mustHavePlaybooks, content.Channel.Playbook)
}
}
if content.Board != nil {
hasBoard = true
if cErr := content.Board.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundBoards[content.Board.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate board %s found", content.Board.ID), currentIdx)
}
foundBoards[content.Board.ID] = struct{}{}
if content.Board.Channel != "" {
mustHaveChannels = append(mustHaveChannels, content.Board.Channel)
}
}
if content.Playbook != nil {
hasPlaybook = true
if cErr := content.Playbook.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundPlaybooks[content.Playbook.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate playbook %s found", content.Playbook.ID), currentIdx)
}
foundPlaybooks[content.Playbook.ID] = struct{}{}
}
if content.Integration != nil {
hasIntegration = true
if cErr := content.Integration.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundIntegrations[content.Integration.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate integration %s found", content.Integration.ID), currentIdx)
}
foundIntegrations[content.Integration.ID] = struct{}{}
}
}
if hasChannel && wt.Description.Channel == nil {
return errors.New("description.channel is required")
}
if hasBoard && wt.Description.Board == nil {
return errors.New("description.board is required")
}
if hasPlaybook && wt.Description.Playbook == nil {
return errors.New("description.playbook is required")
}
if hasIntegration && wt.Description.Integration == nil {
return errors.New("description.integration is required")
}
for _, channel := range mustHaveChannels {
if _, ok := foundChannels[channel]; !ok {
return fmt.Errorf("channel %s is required", channel)
}
}
for _, playbook := range mustHavePlaybooks {
if _, ok := foundPlaybooks[playbook]; !ok {
return fmt.Errorf("playbook %s is required", playbook)
}
}
return nil
}
type FeatureFlag struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
type TranslatableString struct {
ID string `yaml:"id"`
DefaultMessage string `yaml:"defaultMessage"`
Illustration string `yaml:"illustration"`
}
func (ts TranslatableString) Translate(t i18n.TranslateFunc) string {
if ts.ID != "" {
msg := t(ts.ID)
if msg != ts.ID && msg != "" {
return msg
}
}
return ts.DefaultMessage
}
type Description struct {
Channel *TranslatableString `yaml:"channel"`
Board *TranslatableString `yaml:"board"`
Playbook *TranslatableString `yaml:"playbook"`
Integration *TranslatableString `yaml:"integration"`
}
type Channel struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Purpose string `yaml:"purpose"`
Playbook string `yaml:"playbook"`
Illustration string `yaml:"illustration"`
}
func (c *Channel) Validate() error {
if c.ID == "" {
return errors.New("id is required")
}
if c.Name == "" {
return errors.New("name is required")
}
return nil
}
type Board struct {
ID string `yaml:"id"`
Template string `yaml:"template"`
Name string `yaml:"name"`
Channel string `yaml:"channel"`
Illustration string `yaml:"illustration"`
}
func (b Board) Validate() error {
if b.ID == "" {
return errors.New("id is required")
}
if b.Template == "" {
return errors.New("template is required")
}
if b.Name == "" {
return errors.New("name is required")
}
return nil
}
type Playbook struct {
Template string `yaml:"template"`
Name string `yaml:"name"`
ID string `yaml:"id"`
Illustration string `yaml:"illustration"`
}
func (p *Playbook) Validate() error {
if p.ID == "" {
return errors.New("id is required")
}
if p.Template == "" {
return errors.New("template is required")
}
if p.Name == "" {
return errors.New("name is required")
}
return nil
}
type Integration struct {
ID string `yaml:"id"`
}
func (i *Integration) Validate() error {
if i.ID == "" {
return errors.New("id is required")
}
return nil
}
type Content struct {
Channel *Channel `yaml:"channel,omitempty"`
Board *Board `yaml:"board,omitempty"`
Playbook *Playbook `yaml:"playbook,omitempty"`
Integration *Integration `yaml:"integration,omitempty"`
}
func wrapContentError(err error, index int) error {
return errors.Wrapf(err, "content #%d validation failed", index)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make generate-worktemplates"
// DO NOT EDIT
package worktemplates
func init() {
registerWorkTemplateCategory("product_teams", wtc846b565cd80043537945134a54812e07)
registerWorkTemplateCategory("devops", wtca21c218df41f6d7fd032535fe20394e2)
registerWorkTemplateCategory("companywide", wtca6def90c2edac0c33650ac8ebee1e094)
registerWorkTemplateCategory("leadership", wtce9b74766edff1096ba7c67999ca259b6)
registerWorkTemplate("product_teams/feature_release:v1", wt00a1b44a5831c0a3acb14787b3fdd352)
registerWorkTemplate("product_teams/goals_and_okrs:v1", wt5baa68055bf9ea423273662e01ccc575)
registerWorkTemplate("product_teams/bug_bash:v1", wtfeb56bc6a8f277c47b503bd1c92d830e)
registerWorkTemplate("product_teams/sprint_planning:v1", wt8d2ef53deac5517eb349dc5de6150196)
registerWorkTemplate("product_teams/product_roadmap:v1", wt00ab91a945627f4a624957dd80490bb2)
registerWorkTemplate("devops/incident_resolution:v1", wtce19b9352a59d6a5d26f292d83e84377)
registerWorkTemplate("devops/product_release:v1", wt37406285a41c18bcdeb881189f7acde0)
registerWorkTemplate("companywide/goals_and_okrs:v1", wtf7b846d35810f8272eeb9a1a562025b5)
registerWorkTemplate("companywide/create_project:v1", wtb9ab412890c2410c7b49eec8f12e7edc)
registerWorkTemplate("leadership/goals_and_okrs:v1", wt32ab773bfe021e3d4913931041552559)
// Register categories strings
_ = T("worktemplate.category.product_teams")
_ = T("worktemplate.category.devops")
_ = T("worktemplate.category.companywide")
_ = T("worktemplate.category.leadership")
// Register translation strings
_ = T("worktemplate.product_teams.feature_release.description.channel")
_ = T("worktemplate.product_teams.feature_release.description.board")
_ = T("worktemplate.product_teams.feature_release.description.playbook")
_ = T("worktemplate.product_teams.feature_release.description.integration")
_ = T("worktemplate.product_teams.goals_and_okrs.channel")
_ = T("worktemplate.product_teams.goals_and_okrs.board")
_ = T("worktemplate.product_teams.goals_and_okrs.integration")
_ = T("worktemplate.product_teams.bug_bash.channel")
_ = T("worktemplate.product_teams.bug_bash.board")
_ = T("worktemplate.product_teams.bug_bash.playbook")
_ = T("worktemplate.product_teams.bug_bash.integration")
_ = T("worktemplate.product_teams.sprint_planning.channel")
_ = T("worktemplate.product_teams.sprint_planning.board")
_ = T("worktemplate.product_teams.sprint_planning.integration")
_ = T("worktemplate.product_teams.product_roadmap.channel")
_ = T("worktemplate.product_teams.product_roadmap.board")
_ = T("worktemplate.devops.incident_resolution.description.channel")
_ = T("worktemplate.devops.incident_resolution.description.board")
_ = T("worktemplate.devops.incident_resolution.description.playbook")
_ = T("worktemplate.devops.product_release.channel")
_ = T("worktemplate.devops.product_release.board")
_ = T("worktemplate.devops.product_release.playbook")
_ = T("worktemplate.companywide.goals_and_okrs.channel")
_ = T("worktemplate.companywide.goals_and_okrs.board")
_ = T("worktemplate.companywide.goals_and_okrs.integration")
_ = T("worktemplate.companywide.create_project.channel")
_ = T("worktemplate.companywide.create_project.board")
_ = T("worktemplate.companywide.create_project.integration")
_ = T("worktemplate.leadership.goals_and_okrs.channel")
_ = T("worktemplate.leadership.goals_and_okrs.board")
_ = T("worktemplate.leadership.goals_and_okrs.integration")
}
var wtc846b565cd80043537945134a54812e07 = &WorkTemplateCategory{
ID: "product_teams",
Name: "worktemplate.category.product_teams",
}
var wtca21c218df41f6d7fd032535fe20394e2 = &WorkTemplateCategory{
ID: "devops",
Name: "worktemplate.category.devops",
}
var wtca6def90c2edac0c33650ac8ebee1e094 = &WorkTemplateCategory{
ID: "companywide",
Name: "worktemplate.category.companywide",
}
var wtce9b74766edff1096ba7c67999ca259b6 = &WorkTemplateCategory{
ID: "leadership",
Name: "worktemplate.category.leadership",
}
var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
ID: "product_teams/feature_release:v1",
Category: "product_teams",
UseCase: "Manage feature release",
Illustration: "/static/worktemplates/product_teams/feature_release/feature_release.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.channel",
DefaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.board",
DefaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.playbook",
DefaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.integration",
DefaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "feature-release",
Name: "Feature Release",
Purpose: "",
Playbook: "product-release-playbook",
Illustration: "/static/worktemplates/product_teams/feature_release/channel.png",
},
},
{
Board: &Board{
ID: "board-meeting-agenda",
Template: "54fcf9c610f0ac5e4c522c0657c90602",
Name: "Meeting Agenda",
Channel: "feature-release",
Illustration: "/static/worktemplates/boards/meeting_agenda.png",
},
},
{
Board: &Board{
ID: "board-project-task",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Project Task",
Channel: "feature-release",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
{
Playbook: &Playbook{
Template: "Product Release",
Name: "Feature release",
ID: "product-release-playbook",
Illustration: "/static/worktemplates/playbooks/product_release.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
{
Integration: &Integration{
ID: "github",
},
},
},
}
var wt5baa68055bf9ea423273662e01ccc575 = &WorkTemplate{
ID: "product_teams/goals_and_okrs:v1",
Category: "product_teams",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/product_teams/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Board: &Board{
ID: "board-1674845175528",
Template: "54fcf9c610f0ac5e4c522c0657c90602",
Name: "Meeting Agenda",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/meeting_agenda.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wtfeb56bc6a8f277c47b503bd1c92d830e = &WorkTemplate{
ID: "product_teams/bug_bash:v1",
Category: "product_teams",
UseCase: "Run a bug bash",
Illustration: "/static/worktemplates/product_teams/bug_bash/bug_bash.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.channel",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.board",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.playbook",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.integration",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Bug Bash",
Name: "Bug Bash",
ID: "playbook-1674844017943",
Illustration: "/static/worktemplates/playbooks/bug_bash.png",
},
},
{
Channel: &Channel{
ID: "channel-1674844017943",
Name: "Bug Bash",
Purpose: "",
Playbook: "playbook-1674844017943",
Illustration: "/static/worktemplates/product_teams/bug_bash/channel.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
},
}
var wt8d2ef53deac5517eb349dc5de6150196 = &WorkTemplate{
ID: "product_teams/sprint_planning:v1",
Category: "product_teams",
UseCase: "Plan sprints",
Illustration: "/static/worktemplates/product_teams/sprint_planning/sprint_planning.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.channel",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.board",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.integration",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674850783500",
Name: "Sprint planning",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/sprint_planning/channel.png",
},
},
{
Board: &Board{
ID: "board-1674850783973",
Template: "99b74e26d2f5d0a9b346d43c0a7bfb09",
Name: "Sprint planning",
Channel: "channel-1674850783500",
Illustration: "/static/worktemplates/boards/sprint_planner.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wt00ab91a945627f4a624957dd80490bb2 = &WorkTemplate{
ID: "product_teams/product_roadmap:v1",
Category: "product_teams",
UseCase: "Create a product roadmap",
Illustration: "/static/worktemplates/product_teams/product_roadmap/product_roadmap.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.product_roadmap.channel",
DefaultMessage: "Description of why the channel(s) are needed",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.product_roadmap.board",
DefaultMessage: "Description of why the board(s) are needed",
Illustration: "",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674851139450",
Name: "Product Roadmap",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/product_roadmap/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851139759",
Template: "b728c6ca730e2cfc229741c5a4712b65",
Name: "Product Roadmap",
Channel: "channel-1674851139450",
Illustration: "/static/worktemplates/boards/roadmap.png",
},
},
},
}
var wtce19b9352a59d6a5d26f292d83e84377 = &WorkTemplate{
ID: "devops/incident_resolution:v1",
Category: "devops",
UseCase: "Resolve incidents",
Illustration: "/static/worktemplates/devops/incident_resolution/incident_resolution.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.channel",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.board",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.playbook",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Incident Resolution",
Name: "Incident Resolution",
ID: "irpb",
Illustration: "/static/worktemplates/playbooks/incident_resolution.png",
},
},
{
Channel: &Channel{
ID: "irc",
Name: "Incident Resolution",
Purpose: "",
Playbook: "irpb",
Illustration: "/static/worktemplates/devops/incident_resolution/channel.png",
},
},
{
Board: &Board{
ID: "irb",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Incident Resolution",
Channel: "irc",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
},
}
var wt37406285a41c18bcdeb881189f7acde0 = &WorkTemplate{
ID: "devops/product_release:v1",
Category: "devops",
UseCase: "Prepare a product release",
Illustration: "/static/worktemplates/devops/product_release/product_release.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.devops.product_release.channel",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.devops.product_release.board",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.devops.product_release.playbook",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Product Release",
Name: "Product Release",
ID: "playbook-1674851385983",
Illustration: "/static/worktemplates/playbooks/product_release.png",
},
},
{
Channel: &Channel{
ID: "channel-1674851385983",
Name: "Product Release",
Purpose: "",
Playbook: "playbook-1674851385983",
Illustration: "/static/worktemplates/devops/product_release/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851386432",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Product Release",
Channel: "channel-1674851385983",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
},
}
var wtf7b846d35810f8272eeb9a1a562025b5 = &WorkTemplate{
ID: "companywide/goals_and_okrs:v1",
Category: "companywide",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/companywide/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/companywide/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wtb9ab412890c2410c7b49eec8f12e7edc = &WorkTemplate{
ID: "companywide/create_project:v1",
Category: "companywide",
UseCase: "Create a project",
Illustration: "/static/worktemplates/companywide/create_project/create_project.svg",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.companywide.create_project.channel",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.companywide.create_project.board",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.companywide.create_project.integration",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674851940114",
Name: "Create Project",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/companywide/create_project/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851940548",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Create Project",
Channel: "channel-1674851940114",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
{
Integration: &Integration{
ID: "github",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wt32ab773bfe021e3d4913931041552559 = &WorkTemplate{
ID: "leadership/goals_and_okrs:v1",
Category: "leadership",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/leadership/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/leadership/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate go run generator/main.go
package worktemplates
var OrderedWorkTemplates = []*WorkTemplate{}
var OrderedWorkTemplateCategories = []*WorkTemplateCategory{}
// T is a placeholder to allow the translation tool to register the strings
func T(id string) string {
return id
}
func registerWorkTemplate(id string, wt *WorkTemplate) {
OrderedWorkTemplates = append(OrderedWorkTemplates, wt)
}
func registerWorkTemplateCategory(id string, wtc *WorkTemplateCategory) {
OrderedWorkTemplateCategories = append(OrderedWorkTemplateCategories, wtc)
}
func ListCategories() ([]*WorkTemplateCategory, error) {
return OrderedWorkTemplateCategories, nil
}
func ListByCategory(category string) ([]*WorkTemplate, error) {
wts := []*WorkTemplate{}
for i := range OrderedWorkTemplates {
if OrderedWorkTemplates[i].Category == category {
wts = append(wts, OrderedWorkTemplates[i])
}
}
return wts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Audit struct {
logger *mlog.Logger
// OnQueueFull is called on an attempt to add an audit record to a full queue.
// Return true to drop record, or false to block until there is room in queue.
OnQueueFull func(qname string, maxQueueSize int) bool
// OnError is called when an error occurs while writing an audit record.
OnError func(err error)
}
func (a *Audit) Init(maxQueueSize int) {
a.logger, _ = mlog.NewLogger(
mlog.MaxQueueSize(maxQueueSize),
mlog.OnLoggerError(a.onLoggerError),
mlog.OnQueueFull(a.onQueueFull),
mlog.OnTargetQueueFull(a.onTargetQueueFull),
)
}
// LogRecord emits an audit record with complete info.
func (a *Audit) LogRecord(level mlog.Level, rec Record) {
flds := []mlog.Field{
mlog.String(KeyEventName, rec.EventName),
mlog.String(KeyStatus, rec.Status),
mlog.Any(KeyActor, rec.Actor),
mlog.Any(KeyEvent, rec.EventData),
mlog.Any(KeyMeta, rec.Meta),
mlog.Any(KeyError, rec.Error),
}
a.logger.Log(level, "", flds...)
}
// Configure sets zero or more target to output audit logs to.
func (a *Audit) Configure(cfg mlog.LoggerConfiguration) error {
return a.logger.ConfigureTargets(cfg, nil)
}
// Flush attempts to write all queued audit records to all targets.
func (a *Audit) Flush() error {
err := a.logger.Flush()
if err != nil {
a.onLoggerError(err)
}
return err
}
// Shutdown cleanly stops the audit engine after making best efforts to flush all targets.
func (a *Audit) Shutdown() error {
err := a.logger.Shutdown()
if err != nil {
a.onLoggerError(err)
}
return err
}
func (a *Audit) onQueueFull(rec *mlog.LogRec, maxQueueSize int) bool {
if a.OnQueueFull != nil {
return a.OnQueueFull("main", maxQueueSize)
}
mlog.Error("Audit logging queue full, dropping record.", mlog.Int("queueSize", maxQueueSize))
return true
}
func (a *Audit) onTargetQueueFull(target mlog.Target, rec *mlog.LogRec, maxQueueSize int) bool {
if a.OnQueueFull != nil {
return a.OnQueueFull(fmt.Sprintf("%v", target), maxQueueSize)
}
mlog.Error("Audit logging queue full for target, dropping record.", mlog.Any("target", target), mlog.Int("queueSize", maxQueueSize))
return true
}
func (a *Audit) onLoggerError(err error) {
if a.OnError != nil {
a.OnError(err)
return
}
mlog.Error("Auditing error", mlog.Err(err))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
// Record provides a consistent set of fields used for all audit logging.
type Record struct {
EventName string `json:"event_name"`
Status string `json:"status"`
EventData EventData `json:"event"`
Actor EventActor `json:"actor"`
Meta map[string]interface{} `json:"meta"`
Error EventError `json:"error,omitempty"`
}
// EventData contains all event specific data about the modified entity
type EventData struct {
Parameters map[string]interface{} `json:"parameters"` // Payload and parameters being processed as part of the request
PriorState map[string]interface{} `json:"prior_state"` // Prior state of the object being modified, nil if no prior state
ResultState map[string]interface{} `json:"resulting_state"` // Resulting object after creating or modifying it
ObjectType string `json:"object_type"` // String representation of the object type. eg. "post"
}
// EventActor is the subject triggering the event
type EventActor struct {
UserId string `json:"user_id"`
SessionId string `json:"session_id"`
Client string `json:"client"`
IpAddress string `json:"ip_address"`
}
// EventMeta is a key-value store to store related information to the event that is not directly related to the modified entity
type EventMeta struct {
ApiPath string `json:"api_path"`
ClusterId string `json:"cluster_id"`
}
// EventError contains error information in case of failure of the event
type EventError struct {
Description string `json:"description,omitempty"`
Code int `json:"status_code,omitempty"`
}
// Auditable for sensitive object classes, consider implementing Auditable and include whatever the
// AuditableObject returns. For example: it's likely OK to write a user object to the
// audit logs, but not the user password in cleartext or hashed form
type Auditable interface {
Auditable() map[string]interface{}
}
// Success marks the audit record status as successful.
func (rec *Record) Success() {
rec.Status = Success
}
// Fail marks the audit record status as failed.
func (rec *Record) Fail() {
rec.Status = Fail
}
// AddEventParameter adds a parameter, e.g. query or post body, to the event
func AddEventParameter[T string | bool | int | int64 | []string | map[string]string](rec *Record, key string, val T) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
rec.EventData.Parameters[key] = val
}
// AddEventParameterAuditable adds an object that is of type Auditable to the event
func AddEventParameterAuditable(rec *Record, key string, val Auditable) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
rec.EventData.Parameters[key] = val.Auditable()
}
// AddEventParameterAuditableArray adds an array of objects of type Auditable to the event
func AddEventParameterAuditableArray[T Auditable](rec *Record, key string, val []T) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
processedAuditables := make([]map[string]interface{}, 0, len(val))
for _, auditableVal := range val {
processedAuditables = append(processedAuditables, auditableVal.Auditable())
}
rec.EventData.Parameters[key] = processedAuditables
}
// AddEventPriorState adds the prior state of the modified object to the audit record
func (rec *Record) AddEventPriorState(object Auditable) {
rec.EventData.PriorState = object.Auditable()
}
// AddEventResultState adds the result state of the modified object to the audit record
func (rec *Record) AddEventResultState(object Auditable) {
rec.EventData.ResultState = object.Auditable()
}
// AddEventObjectType adds the object type of the modified object to the audit record
func (rec *Record) AddEventObjectType(objectType string) {
rec.EventData.ObjectType = objectType
}
// AddMeta adds a key/value entry to the audit record that can be used for related information not directly related to
// the modified object, e.g. authentication method
func (rec *Record) AddMeta(name string, val interface{}) {
rec.Meta[name] = val
}
// AddErrorCode adds the error code for a failed event to the audit record
func (rec *Record) AddErrorCode(code int) {
rec.Error.Code = code
}
// AddErrorDesc adds the error description for a failed event to the audit record
func (rec *Record) AddErrorDesc(description string) {
rec.Error.Description = description
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package db
import "embed"
//go:embed migrations
var assets embed.FS
func Assets() embed.FS {
return assets
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package einterfaces
import (
"io"
"github.com/mattermost/mattermost-server/v6/model"
)
type OAuthProvider interface {
GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error)
GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error)
GetUserFromIdToken(idToken string) (*model.User, error)
IsSameUser(dbUser, oAuthUser *model.User) bool
}
var oauthProviders = make(map[string]OAuthProvider)
func RegisterOAuthProvider(name string, newProvider OAuthProvider) {
oauthProviders[name] = newProvider
}
func GetOAuthProvider(name string) OAuthProvider {
provider, ok := oauthProviders[name]
if ok {
return provider
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package active_users
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 10 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.Enable
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeActiveUsers, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package active_users
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
JobName = "ActiveUsers"
)
func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() einterfaces.MetricsInterface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.Enable
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
count, err := store.User().Count(model.UserCountOptions{IncludeDeleted: false})
if err != nil {
return err
}
if getMetrics() != nil {
getMetrics().ObserveEnabledUsers(count)
}
return nil
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"crypto/rand"
"math/big"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
type PeriodicScheduler struct {
jobs *JobServer
period time.Duration
jobType string
enabledFunc func(cfg *model.Config) bool
}
func NewPeriodicScheduler(jobs *JobServer, jobType string, period time.Duration, enabledFunc func(cfg *model.Config) bool) *PeriodicScheduler {
return &PeriodicScheduler{
period: period,
jobType: jobType,
enabledFunc: enabledFunc,
jobs: jobs,
}
}
func (scheduler *PeriodicScheduler) Enabled(cfg *model.Config) bool {
return scheduler.enabledFunc(cfg)
}
func (scheduler *PeriodicScheduler) NextScheduleTime(_ *model.Config, _ time.Time /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) *time.Time {
nextTime := time.Now().Add(getRandomDelay(jitterRange)).Add(scheduler.period)
return &nextTime
}
func (scheduler *PeriodicScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) {
return scheduler.jobs.CreateJob(scheduler.jobType, nil)
}
type DailyScheduler struct {
jobs *JobServer
startTimeFunc func(cfg *model.Config) *time.Time
jobType string
enabledFunc func(cfg *model.Config) bool
}
func NewDailyScheduler(jobs *JobServer, jobType string, startTimeFunc func(cfg *model.Config) *time.Time, enabledFunc func(cfg *model.Config) bool) *DailyScheduler {
return &DailyScheduler{
startTimeFunc: startTimeFunc,
jobType: jobType,
enabledFunc: enabledFunc,
jobs: jobs,
}
}
func (scheduler *DailyScheduler) Enabled(cfg *model.Config) bool {
return scheduler.enabledFunc(cfg)
}
func (scheduler *DailyScheduler) NextScheduleTime(cfg *model.Config, now time.Time /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) *time.Time {
scheduledTime := scheduler.startTimeFunc(cfg)
if scheduledTime == nil {
return nil
}
return GenerateNextStartDateTime(now, *scheduledTime)
}
func (scheduler *DailyScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) {
return scheduler.jobs.CreateJob(scheduler.jobType, nil)
}
const jitterRange = 2000 // milliseconds
func getRandomDelay(limit int64) time.Duration {
num, err := rand.Int(rand.Reader, big.NewInt(limit))
if err != nil {
return time.Millisecond
}
return time.Millisecond * time.Duration(num.Int64())
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SimpleWorker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *JobServer
execute func(job *model.Job) error
isEnabled func(cfg *model.Config) bool
}
func NewSimpleWorker(name string, jobServer *JobServer, execute func(job *model.Job) error, isEnabled func(cfg *model.Config) bool) *SimpleWorker {
worker := SimpleWorker{
name: name,
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
execute: execute,
isEnabled: isEnabled,
}
return &worker
}
func (worker *SimpleWorker) Run() {
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *SimpleWorker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
worker.stop <- true
<-worker.stopped
}
func (worker *SimpleWorker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *SimpleWorker) IsEnabled(cfg *model.Config) bool {
return worker.isEnabled(cfg)
}
func (worker *SimpleWorker) DoJob(job *model.Job) {
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Warn("SimpleWorker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.Err(err))
return
} else if !claimed {
return
}
var appErr *model.AppError
// We get the job again because ClaimJob changes the job status.
job, appErr = worker.jobServer.GetJob(job.Id)
if appErr != nil {
mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr))
worker.setJobError(job, appErr)
}
err := worker.execute(job)
if err != nil {
mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, model.NewAppError("DoJob", "app.job.error", nil, "", http.StatusInternalServerError).Wrap(err))
return
}
mlog.Info("SimpleWorker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
}
func (worker *SimpleWorker) setJobSuccess(job *model.Job) {
if err := worker.jobServer.SetJobProgress(job, 100); err != nil {
mlog.Error("Worker: Failed to update progress for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, err)
}
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("SimpleWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, err)
}
}
func (worker *SimpleWorker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("SimpleWorker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package expirynotify
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 10 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ServiceSettings.ExtendSessionLengthWithActivity
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeExpiryNotify, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package expirynotify
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "ExpiryNotify"
)
func MakeWorker(jobServer *jobs.JobServer, notifySessionsExpired func() error) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ServiceSettings.ExtendSessionLengthWithActivity
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return notifySessionsExpired()
}
return jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_delete
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeExportDelete, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_delete
import (
"path/filepath"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ExportDelete"
type AppIface interface {
configservice.ConfigService
ListDirectory(path string) ([]string, *model.AppError)
FileModTime(path string) (time.Time, *model.AppError)
RemoveFile(path string) *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
exportPath := *app.Config().ExportSettings.Directory
retentionTime := time.Duration(*app.Config().ExportSettings.RetentionDays) * 24 * time.Hour
exports, appErr := app.ListDirectory(exportPath)
if appErr != nil {
return appErr
}
errors := merror.New()
for i := range exports {
filename := filepath.Base(exports[i])
modTime, appErr := app.FileModTime(filepath.Join(exportPath, filename))
if appErr != nil {
mlog.Debug("Worker: Failed to get file modification time",
mlog.Err(appErr), mlog.String("export", exports[i]))
errors.Append(appErr)
continue
}
if time.Now().After(modTime.Add(retentionTime)) {
// remove file data from storage.
if appErr := app.RemoveFile(exports[i]); appErr != nil {
mlog.Debug("Worker: Failed to remove file",
mlog.Err(appErr), mlog.String("export", exports[i]))
errors.Append(appErr)
continue
}
}
}
if err := errors.ErrorOrNil(); err != nil {
mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_process
import (
"context"
"io"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ExportProcess"
type AppIface interface {
configservice.ConfigService
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError)
BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError
Log() *mlog.Logger
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool { return true }
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
opts := model.BulkExportOpts{
CreateArchive: true,
}
includeAttachments, ok := job.Data["include_attachments"]
if ok && includeAttachments == "true" {
opts.IncludeAttachments = true
}
outPath := *app.Config().ExportSettings.Directory
exportFilename := job.Id + "_export.zip"
rd, wr := io.Pipe()
go func() {
_, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename))
if appErr != nil {
// we close the reader here to prevent a deadlock when the bulk exporter tries to
// write into the pipe while app.WriteFile has already returned. The error will be
// returned by the writer part of the pipe when app.BulkExport tries to call
// wr.Write() on it.
rd.CloseWithError(appErr) // CloseWithError never returns an error
}
}()
logger := app.Log().With(mlog.String("job_id", job.Id))
appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, job, opts)
wr.Close() // Close never returns an error
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package extract_content
import (
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ignoredFiles = map[string]bool{
"png": true, "jpg": true, "jpeg": true, "gif": true, "wmv": true,
"mpg": true, "mpeg": true, "mp3": true, "mp4": true, "ogg": true,
"ogv": true, "mov": true, "apk": true, "svg": true, "webm": true,
"mkv": true,
}
const jobName = "ExtractContent"
type AppIface interface {
ExtractContentFromFileInfo(fileInfo *model.FileInfo) error
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
jobServer.HandleJobPanic(job)
var err error
var fromTS int64 = 0
var toTS int64 = model.GetMillis()
if fromStr, ok := job.Data["from"]; ok {
if fromTS, err = strconv.ParseInt(fromStr, 10, 64); err != nil {
return err
}
fromTS *= 1000
}
if toStr, ok := job.Data["to"]; ok {
if toTS, err = strconv.ParseInt(toStr, 10, 64); err != nil {
return err
}
toTS *= 1000
}
var nFiles int
var nErrs int
for {
opts := model.GetFileInfosOptions{
Since: fromTS,
SortBy: model.FileinfoSortByCreated,
IncludeDeleted: false,
}
fileInfos, err := store.FileInfo().GetWithOptions(0, 1000, &opts)
if err != nil {
return err
}
if len(fileInfos) == 0 {
break
}
for _, fileInfo := range fileInfos {
if !ignoredFiles[fileInfo.Extension] {
mlog.Debug("extracting file", mlog.String("filename", fileInfo.Name), mlog.String("filepath", fileInfo.Path))
err = app.ExtractContentFromFileInfo(fileInfo)
if err != nil {
mlog.Warn("Failed to extract file content", mlog.Err(err), mlog.String("file_info_id", fileInfo.Id))
nErrs++
}
nFiles++
}
}
lastFileInfo := fileInfos[len(fileInfos)-1]
if lastFileInfo.CreateAt > toTS {
break
}
fromTS = lastFileInfo.CreateAt + 1
}
job.Data["errors"] = strconv.Itoa(nErrs)
job.Data["processed"] = strconv.Itoa(nFiles)
if err := jobServer.UpdateInProgressJobData(job); err != nil {
mlog.Error("Worker: Failed to update job data", mlog.String("worker", model.JobTypeExtractContent), mlog.String("job_id", job.Id), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package hosted_purchase_screening
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return model.BuildEnterpriseReady == "true" && license == nil
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeHostedPurchaseScreening, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package hosted_purchase_screening
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "HostedPurchaseScreening"
// 3 days matches the expecation given in portal purchase flow.
waitForScreeningDuration = 3 * 24 * time.Hour
)
type ScreenTimeStore interface {
GetByName(string) (*model.System, error)
PermanentDeleteByName(name string) (*model.System, error)
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, screenTimeStore ScreenTimeStore) model.Worker {
isEnabled := func(_ *model.Config) bool {
return !license.IsCloud()
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
now := time.Now()
screenTimeValue, err := screenTimeStore.GetByName(model.SystemHostedPurchaseNeedsScreening)
if err != nil {
return err
}
screenTime, err := strconv.ParseInt(screenTimeValue.Value, 10, 64)
if err != nil {
return err
}
if now.After(time.UnixMilli(screenTime).Add(waitForScreeningDuration)) {
screenTimeStore.PermanentDeleteByName(model.SystemHostedPurchaseNeedsScreening)
}
return nil
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_delete
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeImportDelete, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_delete
import (
"errors"
"path/filepath"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ImportDelete"
type AppIface interface {
configservice.ConfigService
ListDirectory(path string) ([]string, *model.AppError)
FileModTime(path string) (time.Time, *model.AppError)
RemoveFile(path string) *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
importPath := *app.Config().ImportSettings.Directory
retentionTime := time.Duration(*app.Config().ImportSettings.RetentionDays) * 24 * time.Hour
imports, appErr := app.ListDirectory(importPath)
if appErr != nil {
return appErr
}
multipleErrors := merror.New()
for i := range imports {
filename := filepath.Base(imports[i])
modTime, appErr := app.FileModTime(filepath.Join(importPath, filename))
if appErr != nil {
mlog.Debug("Worker: Failed to get file modification time",
mlog.Err(appErr), mlog.String("import", imports[i]))
multipleErrors.Append(appErr)
continue
}
if time.Now().After(modTime.Add(retentionTime)) {
// expected format if uploaded through the API is
// ${uploadID}_${filename}${model.IncompleteUploadSuffix}
minLen := 26 + 1 + len(model.IncompleteUploadSuffix)
// check if it's an incomplete upload and attempt to delete its session.
if len(filename) > minLen && filepath.Ext(filename) == model.IncompleteUploadSuffix {
uploadID := filename[:26]
if storeErr := s.UploadSession().Delete(uploadID); storeErr != nil {
mlog.Debug("Worker: Failed to delete UploadSession",
mlog.Err(storeErr), mlog.String("upload_id", uploadID))
multipleErrors.Append(storeErr)
continue
}
} else {
// check if fileinfo exists and if so delete it.
filePath := filepath.Join(imports[i])
info, storeErr := s.FileInfo().GetByPath(filePath)
var nfErr *store.ErrNotFound
if storeErr != nil && !errors.As(storeErr, &nfErr) {
mlog.Debug("Worker: Failed to get FileInfo",
mlog.Err(storeErr), mlog.String("path", filePath))
multipleErrors.Append(storeErr)
continue
} else if storeErr == nil {
if storeErr = s.FileInfo().PermanentDelete(info.Id); storeErr != nil {
mlog.Debug("Worker: Failed to delete FileInfo",
mlog.Err(storeErr), mlog.String("file_id", info.Id))
multipleErrors.Append(storeErr)
continue
}
}
}
// remove file data from storage.
if appErr := app.RemoveFile(imports[i]); appErr != nil {
mlog.Debug("Worker: Failed to remove file",
mlog.Err(appErr), mlog.String("import", imports[i]))
multipleErrors.Append(appErr)
continue
}
}
}
if err := multipleErrors.ErrorOrNil(); err != nil {
mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_process
import (
"archive/zip"
"io"
"net/http"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ImportProcess"
type AppIface interface {
configservice.ConfigService
RemoveFile(path string) *model.AppError
FileExists(path string) (bool, *model.AppError)
FileSize(path string) (int64, *model.AppError)
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int)
Log() *mlog.Logger
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
appContext := request.EmptyContext(app.Log())
isEnabled := func(cfg *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
importFileName, ok := job.Data["import_file"]
if !ok {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_file", nil, "", http.StatusBadRequest)
}
importFilePath := filepath.Join(*app.Config().ImportSettings.Directory, importFileName)
if ok, err := app.FileExists(importFilePath); err != nil {
return err
} else if !ok {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.file_exists", nil, "", http.StatusBadRequest)
}
importFileSize, appErr := app.FileSize(importFilePath)
if appErr != nil {
return appErr
}
importFile, appErr := app.FileReader(importFilePath)
if appErr != nil {
return appErr
}
defer importFile.Close()
// The import is a long running operation, try to cancel any timeouts attached to the reader.
type TimeoutCanceler interface{ CancelTimeout() bool }
if tc, ok := importFile.(TimeoutCanceler); ok {
if !tc.CancelTimeout() {
appContext.Logger().Warn("Could not cancel the timeout for the file reader. The import may fail due to a timeout.")
}
}
importZipReader, err := zip.NewReader(importFile.(io.ReaderAt), importFileSize)
if err != nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err)
}
// find JSONL import file.
var jsonFile io.ReadCloser
for _, f := range importZipReader.File {
if filepath.Ext(f.Name) != ".jsonl" {
continue
}
// avoid "zip slip"
if strings.Contains(f.Name, "..") {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "jsonFilePath contains path traversal", http.StatusForbidden)
}
jsonFile, err = f.Open()
if err != nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer jsonFile.Close()
break
}
if jsonFile == nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_jsonl", nil, "jsonFile was nil", http.StatusBadRequest)
}
// do the actual import.
appErr, lineNumber := app.BulkImportWithPath(appContext, jsonFile, importZipReader, false, runtime.NumCPU(), model.ExportDataDir)
if appErr != nil {
job.Data["line_number"] = strconv.Itoa(lineNumber)
return appErr
}
// remove import file when done.
if appErr := app.RemoveFile(importFilePath); appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"context"
"errors"
"fmt"
"net/http"
"runtime/pprof"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
CancelWatcherPollingInterval = 5000
)
func (srv *JobServer) CreateJob(jobType string, jobData map[string]string) (*model.Job, *model.AppError) {
job := model.Job{
Id: model.NewId(),
Type: jobType,
CreateAt: model.GetMillis(),
Status: model.JobStatusPending,
Data: jobData,
}
if err := job.IsValid(); err != nil {
return nil, err
}
if srv.workers.Get(job.Type) == nil {
return nil, model.NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+job.Id, http.StatusBadRequest)
}
if _, err := srv.Store.Job().Save(&job); err != nil {
return nil, model.NewAppError("CreateJob", "app.job.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &job, nil
}
func (srv *JobServer) GetJob(id string) (*model.Job, *model.AppError) {
job, err := srv.Store.Job().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return job, nil
}
func (srv *JobServer) ClaimJob(job *model.Job) (bool, *model.AppError) {
updated, err := srv.Store.Job().UpdateStatusOptimistically(job.Id, model.JobStatusPending, model.JobStatusInProgress)
if err != nil {
return false, model.NewAppError("ClaimJob", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated && srv.metrics != nil {
srv.metrics.IncrementJobActive(job.Type)
}
return updated, nil
}
func (srv *JobServer) SetJobProgress(job *model.Job, progress int64) *model.AppError {
job.Status = model.JobStatusInProgress
job.Progress = progress
if _, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err != nil {
return model.NewAppError("SetJobProgress", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (srv *JobServer) SetJobWarning(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusWarning); err != nil {
return model.NewAppError("SetJobWarning", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (srv *JobServer) SetJobSuccess(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusSuccess); err != nil {
return model.NewAppError("SetJobSuccess", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) SetJobError(job *model.Job, jobError *model.AppError) *model.AppError {
if jobError == nil {
_, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusError)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
job.Status = model.JobStatusError
job.Progress = -1
if job.Data == nil {
job.Data = make(map[string]string)
}
job.Data["error"] = jobError.Message
if jobError.DetailedError != "" {
job.Data["error"] += " — " + jobError.DetailedError
}
if wrapped := jobError.Unwrap(); wrapped != nil {
job.Data["error"] += " — " + wrapped.Error()
}
updated, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated && srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
if !updated {
updated, err = srv.Store.Job().UpdateOptimistically(job, model.JobStatusCancelRequested)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !updated {
return model.NewAppError("SetJobError", "jobs.set_job_error.update.error", nil, "id="+job.Id, http.StatusInternalServerError)
}
}
return nil
}
func (srv *JobServer) SetJobCanceled(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusCanceled); err != nil {
return model.NewAppError("SetJobCanceled", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) SetJobPending(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusPending); err != nil {
return model.NewAppError("SetJobPending", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) UpdateInProgressJobData(job *model.Job) *model.AppError {
job.Status = model.JobStatusInProgress
job.LastActivityAt = model.GetMillis()
if _, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err != nil {
return model.NewAppError("UpdateInProgressJobData", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// HandleJobPanic is used to handle panics during the execution of a job. It logs the panic and sets the status for the job.
// After handling, the method repanics! This method is supposed to be `defer`'d at the start of the job.
func (srv *JobServer) HandleJobPanic(job *model.Job) {
r := recover()
if r == nil {
return
}
sb := &strings.Builder{}
pprof.Lookup("goroutine").WriteTo(sb, 2)
mlog.Error("Unhandled panic in job", mlog.Any("panic", r), mlog.Any("job", job), mlog.String("stack", sb.String()))
rerr, ok := r.(error)
if !ok {
rerr = fmt.Errorf("job panic: %v", r)
}
appErr := srv.SetJobError(job, model.NewAppError("HandleJobPanic", "app.job.update.app_error", nil, "", http.StatusInternalServerError)).Wrap(rerr)
if appErr != nil {
mlog.Error("Failed to set the job status to 'failed'", mlog.Err(appErr), mlog.Any("job", job))
}
panic(r)
}
func (srv *JobServer) RequestCancellation(jobId string) *model.AppError {
updated, err := srv.Store.Job().UpdateStatusOptimistically(jobId, model.JobStatusPending, model.JobStatusCanceled)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated {
if srv.metrics != nil {
job, err := srv.GetJob(jobId)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
updated, err = srv.Store.Job().UpdateStatusOptimistically(jobId, model.JobStatusInProgress, model.JobStatusCancelRequested)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated {
return nil
}
return model.NewAppError("RequestCancellation", "jobs.request_cancellation.status.error", nil, "id="+jobId, http.StatusInternalServerError)
}
func (srv *JobServer) CancellationWatcher(ctx context.Context, jobId string, cancelChan chan struct{}) {
for {
select {
case <-ctx.Done():
mlog.Debug("CancellationWatcher for Job Aborting as job has finished.", mlog.String("job_id", jobId))
return
case <-time.After(CancelWatcherPollingInterval * time.Millisecond):
mlog.Debug("CancellationWatcher for Job started polling.", mlog.String("job_id", jobId))
jobStatus, err := srv.Store.Job().Get(jobId)
if err != nil {
mlog.Warn("Error getting job", mlog.String("job_id", jobId), mlog.Err(err))
continue
}
if jobStatus.Status == model.JobStatusCancelRequested {
close(cancelChan)
return
}
}
}
}
func GenerateNextStartDateTime(now time.Time, nextStartTime time.Time) *time.Time {
nextTime := time.Date(now.Year(), now.Month(), now.Day(), nextStartTime.Hour(), nextStartTime.Minute(), 0, 0, time.Local)
if !now.Before(nextTime) {
nextTime = nextTime.AddDate(0, 0, 1)
}
return &nextTime
}
func (srv *JobServer) CheckForPendingJobsByType(jobType string) (bool, *model.AppError) {
count, err := srv.Store.Job().GetCountByStatusAndType(model.JobStatusPending, jobType)
if err != nil {
return false, model.NewAppError("CheckForPendingJobsByType", "app.job.get_count_by_status_and_type.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count > 0, nil
}
func (srv *JobServer) GetJobsByTypeAndStatus(jobType string, status string) ([]*model.Job, *model.AppError) {
jobs, err := srv.Store.Job().GetAllByTypeAndStatus(jobType, status)
if err != nil {
return nil, model.NewAppError("GetJobsByTypeAndStatus", "app.job.get_all_jobs_by_type_and_status.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (srv *JobServer) GetLastSuccessfulJobByType(jobType string) (*model.Job, *model.AppError) {
statuses := []string{model.JobStatusSuccess}
if jobType == model.JobTypeMessageExport {
statuses = []string{model.JobStatusWarning, model.JobStatusSuccess}
}
job, err := srv.Store.Job().GetNewestJobByStatusesAndType(statuses, jobType)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return nil, model.NewAppError("GetLastSuccessfulJobByType", "app.job.get_newest_job_by_status_and_type.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return job, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"math/rand"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Default polling interval for jobs termination.
// (Defining as `var` rather than `const` allows tests to lower the interval.)
var DefaultWatcherPollingInterval = 15000
type Watcher struct {
srv *JobServer
workers *Workers
stop chan struct{}
stopped chan struct{}
pollingInterval int
}
func (srv *JobServer) MakeWatcher(workers *Workers, pollingInterval int) *Watcher {
return &Watcher{
stop: make(chan struct{}),
stopped: make(chan struct{}),
pollingInterval: pollingInterval,
workers: workers,
srv: srv,
}
}
func (watcher *Watcher) Start() {
mlog.Debug("Watcher Started")
// Delay for some random number of milliseconds before starting to ensure that multiple
// instances of the jobserver don't poll at a time too close to each other.
rand.Seed(time.Now().UTC().UnixNano())
<-time.After(time.Duration(rand.Intn(watcher.pollingInterval)) * time.Millisecond)
defer func() {
mlog.Debug("Watcher Finished")
close(watcher.stopped)
}()
for {
select {
case <-watcher.stop:
mlog.Debug("Watcher: Received stop signal")
return
case <-time.After(time.Duration(watcher.pollingInterval) * time.Millisecond):
watcher.PollAndNotify()
}
}
}
func (watcher *Watcher) Stop() {
mlog.Debug("Watcher Stopping")
close(watcher.stop)
<-watcher.stopped
watcher.stop = make(chan struct{})
watcher.stopped = make(chan struct{})
}
func (watcher *Watcher) PollAndNotify() {
jobs, err := watcher.srv.Store.Job().GetAllByStatus(model.JobStatusPending)
if err != nil {
mlog.Error("Error occurred getting all pending statuses.", mlog.Err(err))
return
}
for _, job := range jobs {
worker := watcher.workers.Get(job.Type)
if worker != nil {
select {
case worker.JobChannel() <- *job:
default:
}
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_file
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 2 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessibleFile))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeLastAccessibleFile, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_file
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "LastAccessibleFile"
)
type AppIface interface {
ComputeLastAccessibleFileTime() error
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return app.ComputeLastAccessibleFileTime()
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_post
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 30 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessiblePost))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeLastAccessiblePost, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_post
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "LastAccessiblePost"
)
type AppIface interface {
ComputeLastAccessiblePostTime() error
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return app.ComputeLastAccessiblePostTime()
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AdvancedPermissionsPhase2Progress struct {
CurrentTable string `json:"current_table"`
LastTeamId string `json:"last_team_id"`
LastChannelId string `json:"last_channel_id"`
LastUserId string `json:"last_user"`
}
func (p *AdvancedPermissionsPhase2Progress) ToJSON() string {
b, _ := json.Marshal(p)
return string(b)
}
func AdvancedPermissionsPhase2ProgressFromJSON(data io.Reader) *AdvancedPermissionsPhase2Progress {
var o *AdvancedPermissionsPhase2Progress
err := json.NewDecoder(data).Decode(&o)
if err != nil {
mlog.Warn("Error decoding advanced permissions phase 2 progress", mlog.Err(err))
}
return o
}
func (p *AdvancedPermissionsPhase2Progress) IsValid() bool {
if !model.IsValidId(p.LastChannelId) {
return false
}
if !model.IsValidId(p.LastTeamId) {
return false
}
if !model.IsValidId(p.LastUserId) {
return false
}
switch p.CurrentTable {
case "TeamMembers":
case "ChannelMembers":
default:
return false
}
return true
}
func (worker *Worker) runAdvancedPermissionsPhase2Migration(lastDone string) (bool, string, *model.AppError) {
var progress *AdvancedPermissionsPhase2Progress
if lastDone == "" {
// Haven't started the migration yet.
progress = &AdvancedPermissionsPhase2Progress{
CurrentTable: "TeamMembers",
LastChannelId: strings.Repeat("0", 26),
LastTeamId: strings.Repeat("0", 26),
LastUserId: strings.Repeat("0", 26),
}
} else {
err := json.NewDecoder(strings.NewReader(lastDone)).Decode(&progress)
if err != nil {
return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]any{"lastDone": lastDone}, "", http.StatusInternalServerError).Wrap(err)
}
if !progress.IsValid() {
return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]any{"progress": progress.ToJSON()}, "", http.StatusInternalServerError)
}
}
if progress.CurrentTable == "TeamMembers" {
// Run a TeamMembers migration batch.
result, err := worker.store.Team().MigrateTeamMembers(progress.LastTeamId, progress.LastUserId)
if err != nil {
return false, progress.ToJSON(), model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "app.team.migrate_team_members.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if result == nil {
// We haven't progressed. That means that we've reached the end of this stage of the migration, and should now advance to the next stage.
progress.LastUserId = strings.Repeat("0", 26)
progress.CurrentTable = "ChannelMembers"
return false, progress.ToJSON(), nil
}
progress.LastTeamId = result["TeamId"]
progress.LastUserId = result["UserId"]
} else if progress.CurrentTable == "ChannelMembers" {
// Run a ChannelMembers migration batch.
data, err := worker.store.Channel().MigrateChannelMembers(progress.LastChannelId, progress.LastUserId)
if err != nil {
return false, progress.ToJSON(), model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "app.channel.migrate_channel_members.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if data == nil {
// We haven't progressed. That means we've reached the end of this final stage of the migration.
return true, progress.ToJSON(), nil
}
progress.LastChannelId = data["ChannelId"]
progress.LastUserId = data["UserId"]
}
return false, progress.ToJSON(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
MigrationStateUnscheduled = "unscheduled"
MigrationStateInProgress = "in_progress"
MigrationStateCompleted = "completed"
JobDataKeyMigration = "migration_key"
JobDataKeyMigrationLastDone = "last_done"
)
func MakeMigrationsList() []string {
return []string{
model.MigrationKeyAdvancedPermissionsPhase2,
}
}
func GetMigrationState(migration string, store store.Store) (string, *model.Job, *model.AppError) {
if _, err := store.System().GetByName(migration); err == nil {
return MigrationStateCompleted, nil, nil
}
jobs, err := store.Job().GetAllByType(model.JobTypeMigrations)
if err != nil {
return "", nil, model.NewAppError("GetMigrationState", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, job := range jobs {
if key, ok := job.Data[JobDataKeyMigration]; ok {
if key != migration {
continue
}
switch job.Status {
case model.JobStatusInProgress, model.JobStatusPending:
return MigrationStateInProgress, job, nil
default:
return MigrationStateUnscheduled, job, nil
}
}
}
return MigrationStateUnscheduled, nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MigrationJobWedgedTimeoutMilliseconds = 3600000 // 1 hour
)
type Scheduler struct {
jobServer *jobs.JobServer
store store.Store
allMigrationsCompleted bool
}
func MakeScheduler(jobServer *jobs.JobServer, store store.Store) model.Scheduler {
return &Scheduler{jobServer, store, false}
}
func (scheduler *Scheduler) Enabled(_ *model.Config) bool {
return true
}
//nolint:unparam
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
if scheduler.allMigrationsCompleted {
return nil
}
nextTime := time.Now().Add(60 * time.Second)
return &nextTime
}
//nolint:unparam
func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) {
mlog.Debug("Scheduling Job", mlog.String("scheduler", model.JobTypeMigrations))
// Work through the list of migrations in order. Schedule the first one that isn't done (assuming it isn't in progress already).
for _, key := range MakeMigrationsList() {
state, job, err := GetMigrationState(key, scheduler.store)
if err != nil {
mlog.Error("Failed to determine status of migration: ", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key), mlog.Err(err))
return nil, nil
}
if state == MigrationStateInProgress {
// Check the migration job isn't wedged.
if job != nil && job.LastActivityAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds && job.CreateAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds {
mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key))
if err := scheduler.jobServer.SetJobError(job, nil); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("job_id", job.Id), mlog.Err(err))
}
return scheduler.createJob(key, job)
}
return nil, nil
}
if state == MigrationStateCompleted {
// This migration is done. Continue to check the next.
continue
}
if state == MigrationStateUnscheduled {
mlog.Debug("Scheduling a new job for migration.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key))
return scheduler.createJob(key, job)
}
mlog.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state))
return nil, nil
}
// If we reached here, then there aren't any migrations left to run.
scheduler.allMigrationsCompleted = true
mlog.Debug("All migrations are complete.", mlog.String("scheduler", model.JobTypeMigrations))
return nil, nil
}
func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job) (*model.Job, *model.AppError) {
var lastDone string
if lastJob != nil {
lastDone = lastJob.Data[JobDataKeyMigrationLastDone]
}
data := map[string]string{
JobDataKeyMigration: migrationKey,
JobDataKeyMigrationLastDone: lastDone,
}
job, err := scheduler.jobServer.CreateJob(model.JobTypeMigrations, data)
if err != nil {
return nil, err
}
return job, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"context"
"net/http"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TimeBetweenBatches = 100
)
type Worker struct {
name string
stop chan struct{}
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
store store.Store
closed int32
}
func MakeWorker(jobServer *jobs.JobServer, store store.Store) model.Worker {
worker := Worker{
name: "Migrations",
stop: make(chan struct{}),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
store: store,
}
return &worker
}
func (worker *Worker) Run() {
// Set to open if closed before. We are not bothered about multiple opens.
if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) {
worker.stop = make(chan struct{})
}
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *Worker) Stop() {
// Set to close, and if already closed before, then return.
if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) {
return
}
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
close(worker.stop)
<-worker.stopped
}
func (worker *Worker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *Worker) IsEnabled(_ *model.Config) bool {
return true
}
func (worker *Worker) DoJob(job *model.Job) {
defer worker.jobServer.HandleJobPanic(job)
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Info("Worker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.String("error", err.Error()))
return
} else if !claimed {
return
}
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
cancelWatcherChan := make(chan struct{}, 1)
go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
defer cancelCancelWatcher()
for {
select {
case <-cancelWatcherChan:
mlog.Debug("Worker: Job has been canceled via CancellationWatcher", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-worker.stop:
mlog.Debug("Worker: Job has been canceled via Worker Stop", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-time.After(TimeBetweenBatches * time.Millisecond):
done, progress, err := worker.runMigration(job.Data[JobDataKeyMigration], job.Data[JobDataKeyMigrationLastDone])
if err != nil {
mlog.Error("Worker: Failed to run migration", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
} else if done {
mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
return
} else {
job.Data[JobDataKeyMigrationLastDone] = progress
if err := worker.jobServer.UpdateInProgressJobData(job); err != nil {
mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
}
}
}
}
}
func (worker *Worker) setJobSuccess(job *model.Job) {
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
}
}
func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
func (worker *Worker) setJobCanceled(job *model.Job) {
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
// Return parameters:
// - whether the migration is completed on this run (true) or still incomplete (false).
// - the updated lastDone string for the migration.
// - any error which may have occurred while running the migration.
func (worker *Worker) runMigration(key string, lastDone string) (bool, string, *model.AppError) {
var done bool
var progress string
var err *model.AppError
switch key {
case model.MigrationKeyAdvancedPermissionsPhase2:
done, progress, err = worker.runAdvancedPermissionsPhase2Migration(lastDone)
default:
return false, "", model.NewAppError("MigrationsWorker.runMigration", "migrations.worker.run_migration.unknown_key", map[string]any{"key": key}, "", http.StatusInternalServerError)
}
if done {
if nErr := worker.store.System().Save(&model.System{Name: key, Value: "true"}); nErr != nil {
return false, "", model.NewAppError("runMigration", "migrations.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return done, progress, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const installPluginSchedFreq = 24 * time.Hour
func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := jobType == model.JobTypeInstallPluginNotifyAdmin
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, jobType, installPluginSchedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, jobType, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
UpgradeNotifyJobName = "UpgradeNotifyAdmin"
TrialNotifyJobName = "TrialNotifyAdmin"
InstallNotifyJobName = "InstallNotifyAdmin"
)
type AppIface interface {
DoCheckForAdminNotifications(trial bool) *model.AppError
}
func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(false)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(UpgradeNotifyJobName, jobServer, execute, isEnabled)
return worker
}
func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(true)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(TrialNotifyJobName, jobServer, execute, isEnabled)
return worker
}
func MakeInstallPluginNotifyWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(false)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(InstallNotifyJobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product_notices
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
type Scheduler struct {
*jobs.PeriodicScheduler
}
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
nextTime := time.Now().Add(time.Duration(*cfg.AnnouncementSettings.NoticesFetchFrequency) * time.Second)
return &nextTime
}
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled
}
return &Scheduler{PeriodicScheduler: jobs.NewPeriodicScheduler(jobServer, model.JobTypeProductNotices, 0, isEnabled)}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product_notices
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ProductNotices"
type AppIface interface {
UpdateProductNotices() *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
if err := app.UpdateProductNotices(); err != nil {
mlog.Error("Worker: Failed to fetch product notices", mlog.String("worker", model.JobTypeProductNotices), mlog.String("job_id", job.Id), mlog.Err(err))
return err
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package resend_invitation_email
import (
"encoding/json"
"net/http"
"os"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const FourtyEightHoursInMillis int64 = 172800000
type AppIface interface {
configservice.ConfigService
GetUserByEmail(email string) (*model.User, *model.AppError)
GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError)
InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
}
type ResendInvitationEmailWorker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
app AppIface
store store.Store
telemetryService *telemetry.TelemetryService
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store, telemetryService *telemetry.TelemetryService) model.Worker {
worker := ResendInvitationEmailWorker{
name: model.JobTypeResendInvitationEmail,
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
app: app,
store: store,
telemetryService: telemetryService,
}
return &worker
}
func (rseworker *ResendInvitationEmailWorker) Run() {
mlog.Debug("Worker started", mlog.String("worker", rseworker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", rseworker.name))
rseworker.stopped <- true
}()
for {
select {
case <-rseworker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", rseworker.name))
return
case job := <-rseworker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", rseworker.name))
rseworker.DoJob(&job)
}
}
}
func (rseworker *ResendInvitationEmailWorker) IsEnabled(cfg *model.Config) bool {
return *cfg.ServiceSettings.EnableEmailInvitations
}
func (rseworker *ResendInvitationEmailWorker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", rseworker.name))
rseworker.stop <- true
<-rseworker.stopped
}
func (rseworker *ResendInvitationEmailWorker) JobChannel() chan<- model.Job {
return rseworker.jobs
}
func (rseworker *ResendInvitationEmailWorker) DoJob(job *model.Job) {
defer rseworker.jobServer.HandleJobPanic(job)
elapsedTimeSinceSchedule, DurationInMillis := rseworker.GetDurations(job)
if elapsedTimeSinceSchedule > DurationInMillis {
rseworker.ResendEmails(job, "48")
rseworker.TearDown(job)
}
}
func (rseworker *ResendInvitationEmailWorker) setJobSuccess(job *model.Job) {
if err := rseworker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
rseworker.setJobError(job, err)
}
}
func (rseworker *ResendInvitationEmailWorker) setJobError(job *model.Job, appError *model.AppError) {
if err := rseworker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
func (rseworker *ResendInvitationEmailWorker) cleanEmailData(emailStringData string) ([]string, error) {
// emailStringData looks like this ["user1@gmail.com","user2@gmail.com"]
emails := []string{}
err := json.Unmarshal([]byte(emailStringData), &emails)
if err != nil {
return nil, err
}
return emails, nil
}
func (rseworker *ResendInvitationEmailWorker) cleanChannelsData(channelStringData string) ([]string, error) {
// channelStringData looks like this ["uuuiiiiidddd","uuuiiiiidddd"]
channels := []string{}
err := json.Unmarshal([]byte(channelStringData), &channels)
if err != nil {
return nil, err
}
return channels, nil
}
func (rseworker *ResendInvitationEmailWorker) removeAlreadyJoined(teamID string, emailList []string) []string {
var notJoinedYet []string
for _, email := range emailList {
// check if the user with this email is on the system already
user, appErr := rseworker.app.GetUserByEmail(email)
if appErr != nil {
notJoinedYet = append(notJoinedYet, email)
continue
}
// now we check if they are part of the team already
userID := []string{user.Id}
members, appErr := rseworker.app.GetTeamMembersByIds(teamID, userID, nil)
if len(members) == 0 || appErr != nil {
notJoinedYet = append(notJoinedYet, email)
}
}
return notJoinedYet
}
func (rseworker *ResendInvitationEmailWorker) GetDurations(job *model.Job) (int64, int64) {
scheduledAt, _ := strconv.ParseInt(job.Data["scheduledAt"], 10, 64)
now := model.GetMillis()
elapsedTimeSinceSchedule := now - scheduledAt
duration := os.Getenv("MM_RESEND_INVITATION_EMAIL_JOB_DURATION")
DurationInMillis, parseError := strconv.ParseInt(duration, 10, 64)
if parseError != nil {
// default to 48 hours
DurationInMillis = FourtyEightHoursInMillis
}
return elapsedTimeSinceSchedule, DurationInMillis
}
func (rseworker *ResendInvitationEmailWorker) TearDown(job *model.Job) {
rseworker.store.System().PermanentDeleteByName(job.Id)
rseworker.setJobSuccess(job)
}
func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, interval string) {
teamID := job.Data["teamID"]
emailListData := job.Data["emailList"]
channelListData := job.Data["channelList"]
emailList, err := rseworker.cleanEmailData(emailListData)
if err != nil {
appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err)
mlog.Error("Worker: Failed to clean emails string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
channelList, err := rseworker.cleanChannelsData(channelListData)
if err != nil {
appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err)
mlog.Error("Worker: Failed to clean channel string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
emailList = rseworker.removeAlreadyJoined(teamID, emailList)
memberInvite := model.MemberInvite{
Emails: emailList,
}
if len(channelList) > 0 {
memberInvite.ChannelIds = channelList
}
_, appErr := rseworker.app.InviteNewUsersToTeamGracefully(&memberInvite, teamID, job.Data["senderID"], interval)
if appErr != nil {
mlog.Error("Worker: Failed to send emails", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
rseworker.telemetryService.SendTelemetry("track_invite_email_resend", map[string]any{interval: interval})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"errors"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Schedulers struct {
stop chan bool
stopped chan bool
configChanged chan *model.Config
clusterLeaderChanged chan bool
listenerId string
jobs *JobServer
isLeader bool
running bool
schedulers map[string]model.Scheduler
nextRunTimes map[string]*time.Time
}
var (
ErrSchedulersNotRunning = errors.New("job schedulers are not running")
ErrSchedulersRunning = errors.New("job schedulers are running")
ErrSchedulersUninitialized = errors.New("job schedulers are not initialized")
)
func (schedulers *Schedulers) AddScheduler(name string, scheduler model.Scheduler) {
schedulers.schedulers[name] = scheduler
}
// Start starts the schedulers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (schedulers *Schedulers) Start() {
schedulers.stop = make(chan bool)
schedulers.stopped = make(chan bool)
schedulers.listenerId = schedulers.jobs.ConfigService.AddConfigListener(schedulers.handleConfigChange)
go func() {
mlog.Info("Starting schedulers.")
defer func() {
mlog.Info("Schedulers stopped.")
close(schedulers.stopped)
}()
now := time.Now()
for name, scheduler := range schedulers.schedulers {
if !scheduler.Enabled(schedulers.jobs.Config()) {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(schedulers.jobs.Config(), name, now, false)
}
}
for {
timer := time.NewTimer(1 * time.Minute)
select {
case <-schedulers.stop:
mlog.Debug("Schedulers received stop signal.")
timer.Stop()
return
case now = <-timer.C:
cfg := schedulers.jobs.Config()
for name, nextTime := range schedulers.nextRunTimes {
if nextTime == nil {
continue
}
if time.Now().After(*nextTime) {
scheduler := schedulers.schedulers[name]
if scheduler == nil || !schedulers.isLeader || !scheduler.Enabled(cfg) {
continue
}
if _, err := schedulers.scheduleJob(cfg, name, scheduler); err != nil {
mlog.Error("Failed to schedule job", mlog.String("scheduler", name), mlog.Err(err))
continue
}
schedulers.setNextRunTime(cfg, name, now, true)
}
}
case newCfg := <-schedulers.configChanged:
for name, scheduler := range schedulers.schedulers {
if !schedulers.isLeader || !scheduler.Enabled(newCfg) {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(newCfg, name, now, false)
}
}
case isLeader := <-schedulers.clusterLeaderChanged:
for name := range schedulers.schedulers {
schedulers.isLeader = isLeader
if !isLeader {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(schedulers.jobs.Config(), name, now, false)
}
}
}
timer.Stop()
}
}()
schedulers.running = true
}
// Stop stops the schedulers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (schedulers *Schedulers) Stop() {
mlog.Info("Stopping schedulers.")
close(schedulers.stop)
<-schedulers.stopped
schedulers.jobs.ConfigService.RemoveConfigListener(schedulers.listenerId)
schedulers.listenerId = ""
schedulers.running = false
}
func (schedulers *Schedulers) setNextRunTime(cfg *model.Config, name string, now time.Time, pendingJobs bool) {
scheduler := schedulers.schedulers[name]
if !pendingJobs {
pj, err := schedulers.jobs.CheckForPendingJobsByType(name)
if err != nil {
mlog.Error("Failed to set next job run time", mlog.Err(err))
schedulers.nextRunTimes[name] = nil
return
}
pendingJobs = pj
}
lastSuccessfulJob, err := schedulers.jobs.GetLastSuccessfulJobByType(name)
if err != nil {
mlog.Error("Failed to set next job run time", mlog.Err(err))
schedulers.nextRunTimes[name] = nil
return
}
schedulers.nextRunTimes[name] = scheduler.NextScheduleTime(cfg, now, pendingJobs, lastSuccessfulJob)
mlog.Debug("Next run time for scheduler", mlog.String("scheduler_name", name), mlog.String("next_runtime", fmt.Sprintf("%v", schedulers.nextRunTimes[name])))
}
func (schedulers *Schedulers) scheduleJob(cfg *model.Config, name string, scheduler model.Scheduler) (*model.Job, *model.AppError) {
pendingJobs, err := schedulers.jobs.CheckForPendingJobsByType(name)
if err != nil {
return nil, err
}
lastSuccessfulJob, err2 := schedulers.jobs.GetLastSuccessfulJobByType(name)
if err2 != nil {
return nil, err
}
return scheduler.ScheduleJob(cfg, pendingJobs, lastSuccessfulJob)
}
func (schedulers *Schedulers) handleConfigChange(_, newConfig *model.Config) {
mlog.Debug("Schedulers received config change.")
select {
case schedulers.configChanged <- newConfig:
case <-schedulers.stop:
}
}
func (schedulers *Schedulers) handleClusterLeaderChange(isLeader bool) {
select {
case schedulers.clusterLeaderChanged <- isLeader:
default:
mlog.Debug("Sending cluster leader change message to schedulers failed.")
// Drain the buffered channel to make room for the latest change.
select {
case <-schedulers.clusterLeaderChanged:
default:
}
// Enqueue the latest change. This operation is safe due to this method
// being called under lock.
schedulers.clusterLeaderChanged <- isLeader
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
)
type JobServer struct {
ConfigService configservice.ConfigService
Store store.Store
metrics einterfaces.MetricsInterface
// mut is used to protect the following fields from concurrent access.
mut sync.Mutex
workers *Workers
schedulers *Schedulers
}
func NewJobServer(configService configservice.ConfigService, store store.Store, metrics einterfaces.MetricsInterface) *JobServer {
srv := &JobServer{
ConfigService: configService,
Store: store,
metrics: metrics,
}
srv.initWorkers()
srv.initSchedulers()
return srv
}
func (srv *JobServer) initWorkers() {
workers := NewWorkers(srv.ConfigService)
workers.Watcher = srv.MakeWatcher(workers, DefaultWatcherPollingInterval)
srv.workers = workers
}
func (srv *JobServer) initSchedulers() {
schedulers := &Schedulers{
configChanged: make(chan *model.Config),
clusterLeaderChanged: make(chan bool, 1),
jobs: srv,
isLeader: true,
schedulers: make(map[string]model.Scheduler),
nextRunTimes: make(map[string]*time.Time),
}
srv.schedulers = schedulers
}
func (srv *JobServer) Config() *model.Config {
return srv.ConfigService.Config()
}
func (srv *JobServer) RegisterJobType(name string, worker model.Worker, scheduler model.Scheduler) {
srv.mut.Lock()
defer srv.mut.Unlock()
if worker != nil {
srv.workers.AddWorker(name, worker)
}
if scheduler != nil {
srv.schedulers.AddScheduler(name, scheduler)
}
}
func (srv *JobServer) StartWorkers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.workers == nil {
return ErrWorkersUninitialized
} else if srv.workers.running {
return ErrWorkersRunning
}
srv.workers.Start()
return nil
}
func (srv *JobServer) StartSchedulers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers == nil {
return ErrSchedulersUninitialized
} else if srv.schedulers.running {
return ErrSchedulersRunning
}
srv.schedulers.Start()
return nil
}
func (srv *JobServer) StopWorkers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.workers == nil {
return ErrWorkersUninitialized
} else if !srv.workers.running {
return ErrWorkersNotRunning
}
srv.workers.Stop()
return nil
}
func (srv *JobServer) StopSchedulers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers == nil {
return ErrSchedulersUninitialized
} else if !srv.schedulers.running {
return ErrSchedulersNotRunning
}
srv.schedulers.Stop()
return nil
}
func (srv *JobServer) HandleClusterLeaderChange(isLeader bool) {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers != nil {
srv.schedulers.handleClusterLeaderChange(isLeader)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Workers struct {
ConfigService configservice.ConfigService
Watcher *Watcher
workers map[string]model.Worker
listenerId string
running bool
}
var (
ErrWorkersNotRunning = errors.New("job workers are not running")
ErrWorkersRunning = errors.New("job workers are running")
ErrWorkersUninitialized = errors.New("job workers are not initialized")
)
func NewWorkers(configService configservice.ConfigService) *Workers {
return &Workers{
ConfigService: configService,
workers: make(map[string]model.Worker),
}
}
func (workers *Workers) AddWorker(name string, worker model.Worker) {
workers.workers[name] = worker
}
func (workers *Workers) Get(name string) model.Worker {
return workers.workers[name]
}
// Start starts the workers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (workers *Workers) Start() {
mlog.Info("Starting workers")
for _, w := range workers.workers {
if w.IsEnabled(workers.ConfigService.Config()) {
go w.Run()
}
}
go workers.Watcher.Start()
workers.listenerId = workers.ConfigService.AddConfigListener(workers.handleConfigChange)
workers.running = true
}
func (workers *Workers) handleConfigChange(oldConfig *model.Config, newConfig *model.Config) {
mlog.Debug("Workers received config change.")
for _, w := range workers.workers {
if w.IsEnabled(oldConfig) && !w.IsEnabled(newConfig) {
w.Stop()
}
if !w.IsEnabled(oldConfig) && w.IsEnabled(newConfig) {
go w.Run()
}
}
}
// Stop stops the workers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (workers *Workers) Stop() {
workers.ConfigService.RemoveConfigListener(workers.listenerId)
workers.Watcher.Stop()
for _, w := range workers.workers {
if w.IsEnabled(workers.ConfigService.Config()) {
w.Stop()
}
}
workers.running = false
mlog.Info("Stopped workers")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package manualtesting
import (
"errors"
"hash/fnv"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/slashcommands"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// TestEnvironment is a helper struct used for tests in manualtesting.
type TestEnvironment struct {
Params map[string][]string
Client *model.Client4
CreatedTeamID string
CreatedUserID string
Context *web.Context
Writer http.ResponseWriter
Request *http.Request
}
// Init adds manualtest endpoint to the API.
func Init(api4 *api4.API) {
api4.BaseRoutes.Root.Handle("/manualtest", api4.APIHandler(manualTest)).Methods("GET")
}
func manualTest(c *web.Context, w http.ResponseWriter, r *http.Request) {
// Let the world know
mlog.Info("Setting up for manual test...")
// URL Parameters
params, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
c.Err = model.NewAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "", http.StatusBadRequest)
return
}
// Grab a uuid (if available) to seed the random number generator so we don't get conflicts.
uid, ok := params["uid"]
if ok {
hasher := fnv.New32a()
hasher.Write([]byte(uid[0] + strconv.Itoa(int(time.Now().UTC().UnixNano()))))
hash := hasher.Sum32()
rand.Seed(int64(hash))
} else {
mlog.Debug("No uid in URL")
}
// Create a client for tests to use
client := model.NewAPIv4Client("http://localhost" + *c.App.Config().ServiceSettings.ListenAddress)
// Check for username parameter and create a user if present
username, ok1 := params["username"]
teamDisplayName, ok2 := params["teamname"]
var teamID string
var userID string
if ok1 && ok2 {
mlog.Info("Creating user and team")
// Create team for testing
team := &model.Team{
DisplayName: teamDisplayName[0],
Name: "zz" + utils.RandomName(utils.Range{Begin: 20, End: 20}, utils.LOWERCASE),
Email: "success+" + model.NewId() + "simulator.amazonses.com",
Type: model.TeamOpen,
}
createdTeam, err := c.App.Srv().Store().Team().Save(team)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
c.Err = model.NewAppError("manualTest", "app.team.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
c.Err = appErr
default:
c.Err = model.NewAppError("manualTest", "app.team.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.ChannelTypeOpen, TeamId: createdTeam.Id}
if _, err := c.App.CreateChannel(c.AppContext, channel, false); err != nil {
c.Err = err
return
}
teamID = createdTeam.Id
// Create user for testing
user := &model.User{
Email: "success+" + model.NewId() + "simulator.amazonses.com",
Nickname: username[0],
Password: slashcommands.UserPassword}
user, _, err = client.CreateUser(user)
if err != nil {
var appErr *model.AppError
ok = errors.As(err, &appErr)
if ok {
c.Err = appErr
} else {
c.Err = model.NewAppError("manualTest", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
c.App.Srv().Store().User().VerifyEmail(user.Id, user.Email)
c.App.Srv().Store().Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, *c.App.Config().TeamSettings.MaxUsersPerTeam)
userID = user.Id
// Login as user to generate auth token
_, _, err = client.LoginById(user.Id, slashcommands.UserPassword)
if err != nil {
var appErr *model.AppError
ok = errors.As(err, &appErr)
if ok {
c.Err = appErr
} else {
c.Err = model.NewAppError("manualTest", "api.user.login.bot_login_forbidden.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
// Respond with an auth token this can be overridden by a specific test as required
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: client.AuthToken,
Path: "/",
MaxAge: *c.App.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60,
HttpOnly: true,
}
http.SetCookie(w, sessionCookie)
http.Redirect(w, r, "/channels/town-square", http.StatusTemporaryRedirect)
}
// Setup test environment
env := TestEnvironment{
Params: params,
Client: client,
CreatedTeamID: teamID,
CreatedUserID: userID,
Context: c,
Writer: w,
Request: r,
}
// Grab the test ID and pick the test
testname, ok := params["test"]
if !ok {
c.Err = model.NewAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "", http.StatusBadRequest)
return
}
switch testname[0] {
case "autolink":
c.Err = testAutoLink(env)
// ADD YOUR NEW TEST HERE!
case "general":
}
}
func getChannelID(a app.AppIface, channelname string, teamid string, userid string) (string, bool) {
// Grab all the channels
channels, err := a.Srv().Store().Channel().GetChannels(teamid, userid, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if err != nil {
mlog.Debug("Unable to get channels")
return "", false
}
for _, channel := range channels {
if channel.Name == channelname {
return channel.Id, true
}
}
mlog.Debug("Could not find channel", mlog.String("Channel name", channelname), mlog.Int("Possibilities searched", len(channels)))
return "", false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package manualtesting
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const linkPostText = `
Some Links:
https://spinpunch.atlassian.net/issues/?filter=10101&jql=resolution%20in%20(Fixed%2C%20%22Won't%20Fix%22%2C%20Duplicate%2C%20%22Cannot%20Reproduce%22)%20AND%20Resolution%20%3D%20Fixed%20AND%20updated%20%3E%3D%20-7d%20ORDER%20BY%20updatedDate%20DESC
https://www.google.com.pk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&uact=8&ved=0CCUQFjAB&url=https%3A%2F%2Fdevelopers.google.com%2Fmaps%2Fdocumentation%2Fios%2Furlscheme&ei=HBFbVdSBN-WcygOG4oHIBw&usg=AFQjCNGI0Jg92Y7qNmyIpQyvYPut7vx5-Q&bvm=bv.93564037,d.bGg
http://www.google.com.pk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&cad=rja&uact=8&ved=0CC8QFjAD&url=http%3A%2F%2Fwww.quora.com%2FHow-long-will-a-Google-shortened-URL-be-available&ei=XRBbVbPLGYKcsAGqiIDQAw&usg=AFQjCNHY0Xi-GG4hgbrPUY_8Kg-55_-DNQ&bvm=bv.93564037,d.bGg
https://medium.com/@slackhq/11-useful-tips-for-getting-the-most-of-slack-5dfb3d1af77
`
func testAutoLink(env TestEnvironment) *model.AppError {
mlog.Info("Manual Auto Link Test")
channelID, ok := getChannelID(env.Context.App, model.DefaultChannelName, env.CreatedTeamID, env.CreatedUserID)
if !ok {
return model.NewAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "", http.StatusInternalServerError)
}
post := &model.Post{
ChannelId: channelID,
Message: linkPostText}
_, _, err := env.Client.CreatePost(post)
var appErr *model.AppError
if ok = errors.As(err, &appErr); !ok {
appErr = model.NewAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "", http.StatusInternalServerError)
}
return appErr
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
type HooksManager struct {
registeredProducts sync.Map
metrics einterfaces.MetricsInterface
}
func NewHooksManager(metrics einterfaces.MetricsInterface) *HooksManager {
return &HooksManager{
metrics: metrics,
}
}
func (m *HooksManager) AddProduct(productID string, hooks any) error {
prod, err := plugin.NewAdapter(hooks)
if err != nil {
return err
}
rp := &plugin.RegisteredProduct{
ProductID: productID,
Adapter: prod,
}
m.registeredProducts.Store(productID, rp)
return nil
}
func (m *HooksManager) RemoveProduct(productID string) {
m.registeredProducts.Delete(productID)
}
func (m *HooksManager) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
startTime := time.Now()
m.registeredProducts.Range(func(key, value any) bool {
rp := value.(*plugin.RegisteredProduct)
if !rp.Implements(hookId) {
return true
}
hookStartTime := time.Now()
result := hookRunnerFunc(rp.Adapter)
if m.metrics != nil {
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
m.metrics.ObservePluginMultiHookIterationDuration(rp.ProductID, elapsedTime)
}
return result
})
if m.metrics != nil {
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
m.metrics.ObservePluginMultiHookDuration(elapsedTime)
}
}
func (m *HooksManager) HooksForProduct(id string) plugin.Hooks {
if value, ok := m.registeredProducts.Load(id); ok {
rp := value.(*plugin.RegisteredProduct)
return rp.Adapter
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
type Product interface {
Start() error
Stop() error
}
type Manifest struct {
Initializer func(map[ServiceKey]any) (Product, error)
Dependencies map[ServiceKey]struct{}
}
var products = make(map[string]Manifest)
func RegisterProduct(name string, m Manifest) {
products[name] = m
}
func GetProducts() map[string]Manifest {
return products
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package store
import (
"fmt"
"strings"
)
// ErrInvalidInput indicates an error that has occurred due to an invalid input.
type ErrInvalidInput struct {
Entity string // The entity which was sent as the input.
Field string // The field of the entity which was invalid.
Value any // The actual value of the field.
wrapped error // The original error
}
func NewErrInvalidInput(entity, field string, value any) *ErrInvalidInput {
return &ErrInvalidInput{
Entity: entity,
Field: field,
Value: value,
}
}
func (e *ErrInvalidInput) Error() string {
if e.wrapped != nil {
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s error: %s", e.Entity, e.Field, e.Value, e.wrapped)
}
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s", e.Entity, e.Field, e.Value)
}
func (e *ErrInvalidInput) Wrap(err error) *ErrInvalidInput {
e.wrapped = err
return e
}
func (e *ErrInvalidInput) Unwrap() error {
return e.wrapped
}
func (e *ErrInvalidInput) InvalidInputInfo() (entity string, field string, value any) {
entity = e.Entity
field = e.Field
value = e.Value
return
}
// ErrLimitExceeded indicates an error that has occurred because some value exceeded a limit.
type ErrLimitExceeded struct {
What string // What was the object that exceeded.
Count int // The value of the object.
meta string // Any additional metadata.
}
func NewErrLimitExceeded(what string, count int, meta string) *ErrLimitExceeded {
return &ErrLimitExceeded{
What: what,
Count: count,
meta: meta,
}
}
func (e *ErrLimitExceeded) Error() string {
return fmt.Sprintf("limit exceeded: what: %s count: %d metadata: %s", e.What, e.Count, e.meta)
}
// ErrConflict indicates a conflict that occurred.
type ErrConflict struct {
Resource string // The resource which created the conflict.
err error // Internal error.
meta string // Any additional metadata.
}
func NewErrConflict(resource string, err error, meta string) *ErrConflict {
return &ErrConflict{
Resource: resource,
err: err,
meta: meta,
}
}
func (e *ErrConflict) Error() string {
msg := e.Resource + "exists " + e.meta
if e.err != nil {
msg += " " + e.err.Error()
}
return msg
}
func (e *ErrConflict) Unwrap() error {
return e.err
}
// IsErrConflict allows easy type assertion without adding store as a dependency.
func (e *ErrConflict) IsErrConflict() bool {
return true
}
// ErrNotFound indicates that a resource was not found
type ErrNotFound struct {
resource string
ID string
wrapped error
}
func NewErrNotFound(resource, id string) *ErrNotFound {
return &ErrNotFound{
resource: resource,
ID: id,
}
}
func (e *ErrNotFound) Wrap(err error) *ErrNotFound {
e.wrapped = err
return e
}
func (e *ErrNotFound) Error() string {
if e.wrapped != nil {
return fmt.Sprintf("resource: %s id: %s error: %s", e.resource, e.ID, e.wrapped)
}
return fmt.Sprintf("resource: %s id: %s", e.resource, e.ID)
}
// IsErrNotFound allows easy type assertion without adding store as a dependency.
func (e *ErrNotFound) IsErrNotFound() bool {
return true
}
// ErrOutOfBounds indicates that the requested total numbers of rows
// was greater than the allowed limit.
type ErrOutOfBounds struct {
value int
}
func (e *ErrOutOfBounds) Error() string {
return fmt.Sprintf("invalid limit parameter: %d", e.value)
}
func NewErrOutOfBounds(value int) *ErrOutOfBounds {
return &ErrOutOfBounds{value: value}
}
// ErrNotImplemented indicates that some feature or requirement is not implemented yet.
type ErrNotImplemented struct {
detail string
}
func (e *ErrNotImplemented) Error() string {
return e.detail
}
func NewErrNotImplemented(detail string) *ErrNotImplemented {
return &ErrNotImplemented{detail: detail}
}
type ErrUniqueConstraint struct {
Columns []string
}
// NewErrUniqueConstraint creates a uniqueness constraint error for the given column(s).
//
// Examples:
//
// store.NewErrUniqueConstraint("DisplayName") // single column constraint
// store.NewErrUniqueConstraint("Name", "Source") // multi-column constraint
func NewErrUniqueConstraint(columns ...string) *ErrUniqueConstraint {
return &ErrUniqueConstraint{
Columns: columns,
}
}
func (e *ErrUniqueConstraint) Error() string {
var tmpl string
if len(e.Columns) > 1 {
tmpl = "unique constraint: (%s)"
} else {
tmpl = "unique constraint: %s"
}
return fmt.Sprintf(tmpl, strings.Join(e.Columns, ","))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheChannelStore struct {
store.ChannelStore
rootStore *LocalCacheStore
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelMemberCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelMemberCountsCache.Purge()
} else {
s.rootStore.channelMemberCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelPinnedPostCount(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelPinnedPostCountsCache.Purge()
} else {
s.rootStore.channelPinnedPostCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelGuestCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelGuestCountCache.Purge()
} else {
s.rootStore.channelGuestCountCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelByIdCache.Purge()
} else {
s.rootStore.channelByIdCache.Remove(string(msg.Data))
}
}
func (s LocalCacheChannelStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.channelMemberCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelPinnedPostCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelGuestCountCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
s.ChannelStore.ClearCaches()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guest Count - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge")
}
}
func (s LocalCacheChannelStore) InvalidatePinnedPostCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelPinnedPostCountsCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) InvalidateMemberCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMemberCountsCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) InvalidateGuestCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelGuestCountCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guests Count - Remove by channelId")
}
}
func (s LocalCacheChannelStore) InvalidateChannel(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByIdCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetMemberCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelGuestCountCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetGuestCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelGuestCountCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetMemberCountFromCache(channelId string) int64 {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count
}
count, err := s.GetMemberCount(channelId, true)
if err != nil {
return 0
}
return count
}
func (s LocalCacheChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelPinnedPostCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetPinnedPostCount(channelId, allowFromCache)
if err != nil {
return 0, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelPinnedPostCountsCache, channelId, count)
}
return count, nil
}
func (s LocalCacheChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
if allowFromCache {
var cacheItem *model.Channel
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
ch, err := s.ChannelStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, id, ch)
}
return ch, err
}
func (s LocalCacheChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
var foundChannels []*model.Channel
var channelsToQuery []string
if allowFromCache {
for _, id := range ids {
var ch *model.Channel
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &ch); err == nil {
foundChannels = append(foundChannels, ch)
} else {
channelsToQuery = append(channelsToQuery, id)
}
}
}
if channelsToQuery == nil {
return foundChannels, nil
}
channels, err := s.ChannelStore.GetMany(channelsToQuery, allowFromCache)
if err != nil {
return nil, err
}
for _, ch := range channels {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, ch.Id, ch)
}
return append(foundChannels, channels...), nil
}
func (s LocalCacheChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.SaveMember(member)
if err != nil {
return nil, err
}
s.InvalidateMemberCount(member.ChannelId)
return member, nil
}
func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.SaveMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
s.InvalidateMemberCount(member.ChannelId)
}
return members, nil
}
func (s LocalCacheChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.UpdateMember(member)
if err != nil {
return nil, err
}
s.InvalidateMemberCount(member.ChannelId)
return member, nil
}
func (s LocalCacheChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.UpdateMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
s.InvalidateMemberCount(member.ChannelId)
}
return members, nil
}
func (s LocalCacheChannelStore) RemoveMember(channelId, userId string) error {
err := s.ChannelStore.RemoveMember(channelId, userId)
if err != nil {
return err
}
s.InvalidateMemberCount(channelId)
return nil
}
func (s LocalCacheChannelStore) RemoveMembers(channelId string, userIds []string) error {
err := s.ChannelStore.RemoveMembers(channelId, userIds)
if err != nil {
return err
}
s.InvalidateMemberCount(channelId)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type LocalCacheEmojiStore struct {
store.EmojiStore
rootStore *LocalCacheStore
emojiByIdMut sync.Mutex
emojiByIdInvalidations map[string]bool
emojiByNameMut sync.Mutex
emojiByNameInvalidations map[string]bool
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiCacheById.Purge()
} else {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[string(msg.Data)] = true
es.emojiByIdMut.Unlock()
es.rootStore.emojiCacheById.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiIdByName(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiIdCacheByName.Purge()
} else {
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[string(msg.Data)] = true
es.emojiByNameMut.Unlock()
es.rootStore.emojiIdCacheByName.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
if allowFromCache {
if emoji, ok := es.getFromCacheById(id); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByIdMut.Lock()
if es.emojiByIdInvalidations[id] {
// And then remove the key from the map.
ctx = sqlstore.WithMaster(ctx)
delete(es.emojiByIdInvalidations, id)
}
es.emojiByIdMut.Unlock()
emoji, err := es.EmojiStore.Get(ctx, id, allowFromCache)
if allowFromCache && err == nil {
es.addToCache(emoji)
}
return emoji, err
}
func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
if id, ok := model.GetSystemEmojiId(name); ok {
return es.Get(ctx, id, allowFromCache)
}
if allowFromCache {
if emoji, ok := es.getFromCacheByName(name); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByNameMut.Lock()
if es.emojiByNameInvalidations[name] {
ctx = sqlstore.WithMaster(ctx)
// And then remove the key from the map.
delete(es.emojiByNameInvalidations, name)
}
es.emojiByNameMut.Unlock()
emoji, err := es.EmojiStore.GetByName(ctx, name, allowFromCache)
if allowFromCache && err == nil {
es.addToCache(emoji)
}
return emoji, err
}
func (es *LocalCacheEmojiStore) Delete(emoji *model.Emoji, time int64) error {
err := es.EmojiStore.Delete(emoji, time)
if err == nil {
es.removeFromCache(emoji)
}
return err
}
func (es *LocalCacheEmojiStore) addToCache(emoji *model.Emoji) {
es.rootStore.doStandardAddToCache(es.rootStore.emojiCacheById, emoji.Id, emoji)
es.rootStore.doStandardAddToCache(es.rootStore.emojiIdCacheByName, emoji.Name, emoji.Id)
}
func (es *LocalCacheEmojiStore) getFromCacheById(id string) (*model.Emoji, bool) {
var emoji *model.Emoji
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiCacheById, id, &emoji); err == nil {
return emoji, true
}
return nil, false
}
func (es *LocalCacheEmojiStore) getFromCacheByName(name string) (*model.Emoji, bool) {
var emojiId string
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiIdCacheByName, name, &emojiId); err == nil {
return es.getFromCacheById(emojiId)
}
return nil, false
}
func (es *LocalCacheEmojiStore) removeFromCache(emoji *model.Emoji) {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[emoji.Id] = true
es.emojiByIdMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiCacheById, emoji.Id)
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[emoji.Name] = true
es.emojiByNameMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiIdCacheByName, emoji.Name)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheFileInfoStore struct {
store.FileInfoStore
rootStore *LocalCacheStore
}
func (s *LocalCacheFileInfoStore) handleClusterInvalidateFileInfo(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.fileInfoCache.Purge()
return
}
s.rootStore.fileInfoCache.Remove(string(msg.Data))
}
func (s LocalCacheFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
if !allowFromCache {
return s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
}
cacheKey := postId
if includeDeleted {
cacheKey += "_deleted"
}
var fileInfo []*model.FileInfo
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, cacheKey, &fileInfo); err == nil {
return fileInfo, nil
}
fileInfos, err := s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
if err != nil {
return nil, err
}
if len(fileInfos) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, cacheKey, fileInfos)
}
return fileInfos, nil
}
func (s LocalCacheFileInfoStore) ClearCaches() {
s.rootStore.fileInfoCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Purge")
}
}
func (s LocalCacheFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
cacheKey := postId
if deleted {
cacheKey += "_deleted"
}
s.rootStore.doInvalidateCacheCluster(s.rootStore.fileInfoCache, cacheKey)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Remove by PostId")
}
}
func (s LocalCacheFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
storageUsageKey := "storage_usage"
if includeDeleted {
storageUsageKey += "_deleted"
}
if !allowFromCache {
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}
var usage int64
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, storageUsageKey, &usage); err == nil {
return usage, nil
}
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"runtime"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
)
const (
ReactionCacheSize = 20000
ReactionCacheSec = 30 * 60
RoleCacheSize = 20000
RoleCacheSec = 30 * 60
SchemeCacheSize = 20000
SchemeCacheSec = 30 * 60
FileInfoCacheSize = 25000
FileInfoCacheSec = 30 * 60
ChannelGuestCountCacheSize = model.ChannelCacheSize
ChannelGuestCountCacheSec = 30 * 60
WebhookCacheSize = 25000
WebhookCacheSec = 15 * 60
EmojiCacheSize = 5000
EmojiCacheSec = 30 * 60
ChannelPinnedPostsCountsCacheSize = model.ChannelCacheSize
ChannelPinnedPostsCountsCacheSec = 30 * 60
ChannelMembersCountsCacheSize = model.ChannelCacheSize
ChannelMembersCountsCacheSec = 30 * 60
LastPostsCacheSize = 20000
LastPostsCacheSec = 30 * 60
PostsUsageCacheSize = 1
PostsUsageCacheSec = 30 * 60
TermsOfServiceCacheSize = 20000
TermsOfServiceCacheSec = 30 * 60
LastPostTimeCacheSize = 25000
LastPostTimeCacheSec = 15 * 60
UserProfileByIDCacheSize = 20000
UserProfileByIDSec = 30 * 60
ProfilesInChannelCacheSize = model.ChannelCacheSize
ProfilesInChannelCacheSec = 15 * 60
TeamCacheSize = 20000
TeamCacheSec = 30 * 60
ChannelCacheSec = 15 * 60 // 15 mins
)
var clearCacheMessageData = []byte("")
type LocalCacheStore struct {
store.Store
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
reaction LocalCacheReactionStore
reactionCache cache.Cache
fileInfo LocalCacheFileInfoStore
fileInfoCache cache.Cache
role LocalCacheRoleStore
roleCache cache.Cache
rolePermissionsCache cache.Cache
scheme LocalCacheSchemeStore
schemeCache cache.Cache
emoji *LocalCacheEmojiStore
emojiCacheById cache.Cache
emojiIdCacheByName cache.Cache
channel LocalCacheChannelStore
channelMemberCountsCache cache.Cache
channelGuestCountCache cache.Cache
channelPinnedPostCountsCache cache.Cache
channelByIdCache cache.Cache
webhook LocalCacheWebhookStore
webhookCache cache.Cache
post LocalCachePostStore
postLastPostsCache cache.Cache
lastPostTimeCache cache.Cache
postsUsageCache cache.Cache
user *LocalCacheUserStore
userProfileByIdsCache cache.Cache
profilesInChannelCache cache.Cache
team LocalCacheTeamStore
teamAllTeamIdsForUserCache cache.Cache
termsOfService LocalCacheTermsOfServiceStore
termsOfServiceCache cache.Cache
}
func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterface, cluster einterfaces.ClusterInterface, cacheProvider cache.Provider) (localCacheStore LocalCacheStore, err error) {
localCacheStore = LocalCacheStore{
Store: baseStore,
cluster: cluster,
metrics: metrics,
}
// Reactions
if localCacheStore.reactionCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReactionCacheSize,
Name: "Reaction",
DefaultExpiry: ReactionCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReactions,
}); err != nil {
return
}
localCacheStore.reaction = LocalCacheReactionStore{ReactionStore: baseStore.Reaction(), rootStore: &localCacheStore}
// Roles
if localCacheStore.roleCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "Role",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRoles,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
if localCacheStore.rolePermissionsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "RolePermission",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRolePermissions,
}); err != nil {
return
}
localCacheStore.role = LocalCacheRoleStore{RoleStore: baseStore.Role(), rootStore: &localCacheStore}
// Schemes
if localCacheStore.schemeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: SchemeCacheSize,
Name: "Scheme",
DefaultExpiry: SchemeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSchemes,
}); err != nil {
return
}
localCacheStore.scheme = LocalCacheSchemeStore{SchemeStore: baseStore.Scheme(), rootStore: &localCacheStore}
// FileInfo
if localCacheStore.fileInfoCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: FileInfoCacheSize,
Name: "FileInfo",
DefaultExpiry: FileInfoCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForFileInfos,
}); err != nil {
return
}
localCacheStore.fileInfo = LocalCacheFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: &localCacheStore}
// Webhooks
if localCacheStore.webhookCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: WebhookCacheSize,
Name: "Webhook",
DefaultExpiry: WebhookCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForWebhooks,
}); err != nil {
return
}
localCacheStore.webhook = LocalCacheWebhookStore{WebhookStore: baseStore.Webhook(), rootStore: &localCacheStore}
// Emojis
if localCacheStore.emojiCacheById, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiById",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisById,
}); err != nil {
return
}
if localCacheStore.emojiIdCacheByName, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiByName",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisIdByName,
}); err != nil {
return
}
localCacheStore.emoji = &LocalCacheEmojiStore{
EmojiStore: baseStore.Emoji(),
rootStore: &localCacheStore,
emojiByIdInvalidations: make(map[string]bool),
emojiByNameInvalidations: make(map[string]bool),
}
// Channels
if localCacheStore.channelPinnedPostCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelPinnedPostsCountsCacheSize,
Name: "ChannelPinnedPostsCounts",
DefaultExpiry: ChannelPinnedPostsCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts,
}); err != nil {
return
}
if localCacheStore.channelMemberCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelMembersCountsCacheSize,
Name: "ChannelMemberCounts",
DefaultExpiry: ChannelMembersCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelMemberCounts,
}); err != nil {
return
}
if localCacheStore.channelGuestCountCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelGuestCountCacheSize,
Name: "ChannelGuestsCount",
DefaultExpiry: ChannelGuestCountCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelGuestCount,
}); err != nil {
return
}
if localCacheStore.channelByIdCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: model.ChannelCacheSize,
Name: "channelById",
DefaultExpiry: ChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannel,
}); err != nil {
return
}
localCacheStore.channel = LocalCacheChannelStore{ChannelStore: baseStore.Channel(), rootStore: &localCacheStore}
// Posts
if localCacheStore.postLastPostsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostsCacheSize,
Name: "LastPost",
DefaultExpiry: LastPostsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPosts,
}); err != nil {
return
}
if localCacheStore.lastPostTimeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostTimeCacheSize,
Name: "LastPostTime",
DefaultExpiry: LastPostTimeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPostTime,
}); err != nil {
return
}
if localCacheStore.postsUsageCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: PostsUsageCacheSize,
Name: "PostsUsage",
DefaultExpiry: PostsUsageCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPostsUsage,
}); err != nil {
return
}
localCacheStore.post = LocalCachePostStore{PostStore: baseStore.Post(), rootStore: &localCacheStore}
// TOS
if localCacheStore.termsOfServiceCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TermsOfServiceCacheSize,
Name: "TermsOfService",
DefaultExpiry: TermsOfServiceCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTermsOfService,
}); err != nil {
return
}
localCacheStore.termsOfService = LocalCacheTermsOfServiceStore{TermsOfServiceStore: baseStore.TermsOfService(), rootStore: &localCacheStore}
// Users
if localCacheStore.userProfileByIdsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: UserProfileByIDCacheSize,
Name: "UserProfileByIds",
DefaultExpiry: UserProfileByIDSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileByIds,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
if localCacheStore.profilesInChannelCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ProfilesInChannelCacheSize,
Name: "ProfilesInChannel",
DefaultExpiry: ProfilesInChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileInChannel,
}); err != nil {
return
}
localCacheStore.user = &LocalCacheUserStore{
UserStore: baseStore.User(),
rootStore: &localCacheStore,
userProfileByIdsInvalidations: make(map[string]bool),
}
// Teams
if localCacheStore.teamAllTeamIdsForUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TeamCacheSize,
Name: "Team",
DefaultExpiry: TeamCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTeams,
}); err != nil {
return
}
localCacheStore.team = LocalCacheTeamStore{TeamStore: baseStore.Team(), rootStore: &localCacheStore}
if cluster != nil {
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReactions, localCacheStore.reaction.handleClusterInvalidateReaction)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRoles, localCacheStore.role.handleClusterInvalidateRole)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRolePermissions, localCacheStore.role.handleClusterInvalidateRolePermissions)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSchemes, localCacheStore.scheme.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForFileInfos, localCacheStore.fileInfo.handleClusterInvalidateFileInfo)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPostTime, localCacheStore.post.handleClusterInvalidateLastPostTime)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPostsUsage, localCacheStore.post.handleClusterInvalidatePostsUsage)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForWebhooks, localCacheStore.webhook.handleClusterInvalidateWebhook)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisById, localCacheStore.emoji.handleClusterInvalidateEmojiById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisIdByName, localCacheStore.emoji.handleClusterInvalidateEmojiIdByName)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts, localCacheStore.channel.handleClusterInvalidateChannelPinnedPostCount)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMemberCounts, localCacheStore.channel.handleClusterInvalidateChannelMemberCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelGuestCount, localCacheStore.channel.handleClusterInvalidateChannelGuestCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannel, localCacheStore.channel.handleClusterInvalidateChannelById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPosts, localCacheStore.post.handleClusterInvalidateLastPosts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTermsOfService, localCacheStore.termsOfService.handleClusterInvalidateTermsOfService)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileByIds, localCacheStore.user.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileInChannel, localCacheStore.user.handleClusterInvalidateProfilesInChannel)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTeams, localCacheStore.team.handleClusterInvalidateTeam)
}
return
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func (s LocalCacheStore) Reaction() store.ReactionStore {
return s.reaction
}
func (s LocalCacheStore) Role() store.RoleStore {
return s.role
}
func (s LocalCacheStore) Scheme() store.SchemeStore {
return s.scheme
}
func (s LocalCacheStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s LocalCacheStore) Webhook() store.WebhookStore {
return s.webhook
}
func (s LocalCacheStore) Emoji() store.EmojiStore {
return s.emoji
}
func (s LocalCacheStore) Channel() store.ChannelStore {
return s.channel
}
func (s LocalCacheStore) Post() store.PostStore {
return s.post
}
func (s LocalCacheStore) TermsOfService() store.TermsOfServiceStore {
return s.termsOfService
}
func (s LocalCacheStore) User() store.UserStore {
return s.user
}
func (s LocalCacheStore) Team() store.TeamStore {
return s.team
}
func (s LocalCacheStore) DropAllTables() {
s.Invalidate()
s.Store.DropAllTables()
}
func (s *LocalCacheStore) doInvalidateCacheCluster(cache cache.Cache, key string) {
cache.Remove(key)
if s.cluster != nil {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: []byte(key),
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) doStandardAddToCache(cache cache.Cache, key string, value any) {
cache.SetWithDefaultExpiry(key, value)
}
func (s *LocalCacheStore) doStandardReadCache(cache cache.Cache, key string, value any) error {
err := cache.Get(key, value)
if err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter(cache.Name())
}
return nil
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter(cache.Name())
}
return err
}
func (s *LocalCacheStore) doClearCacheCluster(cache cache.Cache) {
cache.Purge()
if s.cluster != nil {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: clearCacheMessageData,
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) Invalidate() {
s.doClearCacheCluster(s.reactionCache)
s.doClearCacheCluster(s.schemeCache)
s.doClearCacheCluster(s.roleCache)
s.doClearCacheCluster(s.fileInfoCache)
s.doClearCacheCluster(s.webhookCache)
s.doClearCacheCluster(s.emojiCacheById)
s.doClearCacheCluster(s.emojiIdCacheByName)
s.doClearCacheCluster(s.channelMemberCountsCache)
s.doClearCacheCluster(s.channelPinnedPostCountsCache)
s.doClearCacheCluster(s.channelGuestCountCache)
s.doClearCacheCluster(s.channelByIdCache)
s.doClearCacheCluster(s.postLastPostsCache)
s.doClearCacheCluster(s.termsOfServiceCache)
s.doClearCacheCluster(s.lastPostTimeCache)
s.doClearCacheCluster(s.userProfileByIdsCache)
s.doClearCacheCluster(s.profilesInChannelCache)
s.doClearCacheCluster(s.teamAllTeamIdsForUserCache)
s.doClearCacheCluster(s.rolePermissionsCache)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCachePostStore struct {
store.PostStore
rootStore *LocalCacheStore
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPostTime(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.lastPostTimeCache.Purge()
} else {
s.rootStore.lastPostTimeCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPosts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postLastPostsCache.Purge()
} else {
s.rootStore.postLastPostsCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidatePostsUsage(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postsUsageCache.Purge()
} else {
s.rootStore.postsUsageCache.Remove(string(msg.Data))
}
}
func (s LocalCachePostStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.lastPostTimeCache)
s.rootStore.doClearCacheCluster(s.rootStore.postLastPostsCache)
s.rootStore.doClearCacheCluster(s.rootStore.postsUsageCache)
s.PostStore.ClearCaches()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Posts Usage Cache - Purge")
}
}
func (s LocalCachePostStore) InvalidateLastPostTimeCache(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.lastPostTimeCache, channelId)
// Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"30")
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"60")
s.PostStore.InvalidateLastPostTimeCache(channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Remove by Channel Id")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Remove by Channel Id")
}
}
func (s LocalCachePostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string {
if allowFromCache {
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, channelId, &lastTime); err == nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, lastTime)
}
}
result := s.PostStore.GetEtag(channelId, allowFromCache, collapsedThreads)
splittedResult := strings.Split(result, ".")
lastTime, _ := strconv.ParseInt((splittedResult[len(splittedResult)-1]), 10, 64)
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, channelId, lastTime)
return result
}
func (s LocalCachePostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if allowFromCache {
// If the last post in the channel's time is less than or equal to the time we are getting posts since,
// we can safely return no posts.
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, options.ChannelId, &lastTime); err == nil && lastTime <= options.Time {
list := model.NewPostList()
return list, nil
}
}
list, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
latestUpdate := options.Time
if err == nil {
for _, p := range list.ToSlice() {
if latestUpdate < p.UpdateAt {
latestUpdate = p.UpdateAt
}
}
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, options.ChannelId, latestUpdate)
}
return list, err
}
func (s LocalCachePostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if !allowFromCache {
return s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
}
offset := options.PerPage * options.Page
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
var cacheItem *model.PostList
if err := s.rootStore.doStandardReadCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), &cacheItem); err == nil {
return cacheItem, nil
}
}
list, err := s.PostStore.GetPosts(options, false, sanitizeOptions)
if err != nil {
return nil, err
}
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
s.rootStore.doStandardAddToCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), list)
}
return list, err
}
// AnalyticsPostCount looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
func (s LocalCachePostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
if !options.AllowFromCache || options.MustHaveFile || options.MustHaveHashtag || !options.UsersPostsOnly || !options.ExcludeDeleted || options.TeamId != "" {
return s.PostStore.AnalyticsPostCount(options)
}
// Currently cache only for app > usage > GetPostsUsage()
// Other filter combinations can be cached if required
cacheKey := "posts_usage"
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.postsUsageCache, cacheKey, &count); err == nil {
return count, nil
}
count, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.postsUsageCache, cacheKey, count)
return count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheReactionStore struct {
store.ReactionStore
rootStore *LocalCacheStore
}
func (s *LocalCacheReactionStore) handleClusterInvalidateReaction(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.reactionCache.Purge()
} else {
s.rootStore.reactionCache.Remove(string(msg.Data))
}
}
func (s LocalCacheReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
return s.ReactionStore.Save(reaction)
}
func (s LocalCacheReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
return s.ReactionStore.Delete(reaction)
}
func (s LocalCacheReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
if !allowFromCache {
return s.ReactionStore.GetForPost(postId, false)
}
var reaction []*model.Reaction
if err := s.rootStore.doStandardReadCache(s.rootStore.reactionCache, postId, &reaction); err == nil {
return reaction, nil
}
reaction, err := s.ReactionStore.GetForPost(postId, false)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.reactionCache, postId, reaction)
return reaction, nil
}
func (s LocalCacheReactionStore) DeleteAllWithEmojiName(emojiName string) error {
// This could be improved. Right now we just clear the whole
// cache because we don't have a way find what post Ids have this emoji name.
defer s.rootStore.doClearCacheCluster(s.rootStore.reactionCache)
return s.ReactionStore.DeleteAllWithEmojiName(emojiName)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheRoleStore struct {
store.RoleStore
rootStore *LocalCacheStore
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRole(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.roleCache.Purge()
} else {
s.rootStore.roleCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRolePermissions(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.rolePermissionsCache.Purge()
} else {
s.rootStore.rolePermissionsCache.Remove(string(msg.Data))
}
}
func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, error) {
if role.Name != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return s.RoleStore.Save(role)
}
func (s LocalCacheRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, name, &role); err == nil {
return role, nil
}
role, err := s.RoleStore.GetByName(ctx, name)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, name, role)
return role, nil
}
func (s LocalCacheRoleStore) GetByNames(names []string) ([]*model.Role, error) {
var foundRoles []*model.Role
var rolesToQuery []string
for _, roleName := range names {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, roleName, &role); err == nil {
foundRoles = append(foundRoles, role)
} else {
rolesToQuery = append(rolesToQuery, roleName)
}
}
roles, err := s.RoleStore.GetByNames(rolesToQuery)
if err != nil {
return nil, err
}
for _, role := range roles {
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, role.Name, role)
}
return append(foundRoles, roles...), nil
}
func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, error) {
role, err := s.RoleStore.Delete(roleId)
if err == nil {
s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return role, err
}
func (s LocalCacheRoleStore) PermanentDeleteAll() error {
defer s.rootStore.roleCache.Purge()
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.RoleStore.PermanentDeleteAll()
}
func (s LocalCacheRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
sort.Strings(roleNames)
cacheKey := strings.Join(roleNames, "/")
var rolePermissionsMap map[string]*model.RolePermissions
if err := s.rootStore.doStandardReadCache(s.rootStore.rolePermissionsCache, cacheKey, &rolePermissionsMap); err == nil {
return rolePermissionsMap, nil
}
rolePermissionsMap, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.rolePermissionsCache, cacheKey, rolePermissionsMap)
return rolePermissionsMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheSchemeStore struct {
store.SchemeStore
rootStore *LocalCacheStore
}
func (s *LocalCacheSchemeStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.schemeCache.Purge()
} else {
s.rootStore.schemeCache.Remove(string(msg.Data))
}
}
func (s LocalCacheSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
if scheme.Id != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, scheme.Id)
}
return s.SchemeStore.Save(scheme)
}
func (s LocalCacheSchemeStore) Get(schemeId string) (*model.Scheme, error) {
var scheme *model.Scheme
if err := s.rootStore.doStandardReadCache(s.rootStore.schemeCache, schemeId, &scheme); err == nil {
return scheme, nil
}
scheme, err := s.SchemeStore.Get(schemeId)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.schemeCache, schemeId, scheme)
return scheme, nil
}
func (s LocalCacheSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, schemeId)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.SchemeStore.Delete(schemeId)
}
func (s LocalCacheSchemeStore) PermanentDeleteAll() error {
defer s.rootStore.doClearCacheCluster(s.rootStore.schemeCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.SchemeStore.PermanentDeleteAll()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheTeamStore struct {
store.TeamStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTeamStore) handleClusterInvalidateTeam(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
} else {
s.rootStore.teamAllTeamIdsForUserCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTeamStore) ClearCaches() {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Purge")
}
}
func (s LocalCacheTeamStore) InvalidateAllTeamIdsForUser(userId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.teamAllTeamIdsForUserCache, userId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Remove by UserId")
}
}
func (s LocalCacheTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
if !allowFromCache {
return s.TeamStore.GetUserTeamIds(userID, allowFromCache)
}
var userTeamIds []string
if err := s.rootStore.doStandardReadCache(s.rootStore.teamAllTeamIdsForUserCache, userID, &userTeamIds); err == nil {
return userTeamIds, nil
}
userTeamIds, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err != nil {
return nil, err
}
if len(userTeamIds) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.teamAllTeamIdsForUserCache, userID, userTeamIds)
}
return userTeamIds, nil
}
func (s LocalCacheTeamStore) Update(team *model.Team) (*model.Team, error) {
var oldTeam *model.Team
var err error
if team.DeleteAt != 0 {
oldTeam, err = s.TeamStore.Get(team.Id)
if err != nil {
return nil, err
}
}
tm, err := s.TeamStore.Update(team)
if err != nil {
return nil, err
}
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
if oldTeam != nil && oldTeam.DeleteAt == 0 {
s.rootStore.doClearCacheCluster(s.rootStore.teamAllTeamIdsForUserCache)
}
return tm, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
LatestKey = "latest"
)
type LocalCacheTermsOfServiceStore struct {
store.TermsOfServiceStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTermsOfServiceStore) handleClusterInvalidateTermsOfService(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.termsOfServiceCache.Purge()
} else {
s.rootStore.termsOfServiceCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTermsOfServiceStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.termsOfServiceCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Terms Of Service - Purge")
}
}
func (s LocalCacheTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
tos, err := s.TermsOfServiceStore.Save(termsOfService)
if err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, tos.Id, tos)
s.rootStore.doInvalidateCacheCluster(s.rootStore.termsOfServiceCache, LatestKey)
}
return tos, err
}
func (s LocalCacheTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
if len, err := s.rootStore.termsOfServiceCache.Len(); err == nil && len != 0 {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, LatestKey, &cacheItem); err == nil {
return cacheItem, nil
}
}
}
termsOfService, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, LatestKey, termsOfService)
}
return termsOfService, err
}
func (s LocalCacheTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
termsOfService, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
}
return termsOfService, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sort"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type LocalCacheUserStore struct {
store.UserStore
rootStore *LocalCacheStore
userProfileByIdsMut sync.Mutex
userProfileByIdsInvalidations map[string]bool
}
func (s *LocalCacheUserStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.userProfileByIdsCache.Purge()
} else {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[string(msg.Data)] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.userProfileByIdsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) handleClusterInvalidateProfilesInChannel(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.profilesInChannelCache.Purge()
} else {
s.rootStore.profilesInChannelCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) ClearCaches() {
s.rootStore.userProfileByIdsCache.Purge()
s.rootStore.profilesInChannelCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Purge")
}
}
func (s *LocalCacheUserStore) InvalidateProfileCacheForUser(userId string) {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[userId] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.doInvalidateCacheCluster(s.rootStore.userProfileByIdsCache, userId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Remove")
}
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
keys, err := s.rootStore.profilesInChannelCache.Keys()
if err == nil {
for _, key := range keys {
var userMap map[string]*model.User
if err = s.rootStore.profilesInChannelCache.Get(key, &userMap); err == nil {
if _, userInCache := userMap[userId]; userInCache {
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, key)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by User")
}
}
}
}
}
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCache(channelID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, channelID)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by Channel")
}
}
func (s *LocalCacheUserStore) GetAllProfilesInChannel(ctx context.Context, channelId string, allowFromCache bool) (map[string]*model.User, error) {
if allowFromCache {
var cachedMap map[string]*model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.profilesInChannelCache, channelId, &cachedMap); err == nil {
return cachedMap, nil
}
}
userMap, err := s.UserStore.GetAllProfilesInChannel(ctx, channelId, allowFromCache)
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.profilesInChannelCache, channelId, model.UserMap(userMap))
}
return userMap, nil
}
func (s *LocalCacheUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
if !allowFromCache {
return s.UserStore.GetProfileByIds(ctx, userIds, options, false)
}
if options == nil {
options = &store.UserGetByIdsOpts{}
}
users := []*model.User{}
remainingUserIds := make([]string, 0)
fromMaster := false
for _, userId := range userIds {
var cacheItem *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, userId, &cacheItem); err == nil {
if options.Since == 0 || cacheItem.UpdateAt > options.Since {
users = append(users, cacheItem)
}
} else {
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[userId] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, userId)
}
s.userProfileByIdsMut.Unlock()
remainingUserIds = append(remainingUserIds, userId)
}
}
if len(remainingUserIds) > 0 {
if fromMaster {
ctx = sqlstore.WithMaster(ctx)
}
remainingUsers, err := s.UserStore.GetProfileByIds(ctx, remainingUserIds, options, false)
if err != nil {
return nil, err
}
for _, user := range remainingUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
users = append(users, user)
}
}
return users, nil
}
// Get is a cache wrapper around the SqlStore method to get a user profile by id.
// It checks if the user entry is present in the cache, returning the entry from cache
// if it is present. Otherwise, it fetches the entry from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) Get(ctx context.Context, id string) (*model.User, error) {
var cacheItem *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cacheItem); err == nil {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
}
return cacheItem, nil
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[id] {
ctx = sqlstore.WithMaster(ctx)
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, id)
}
s.userProfileByIdsMut.Unlock()
user, err := s.UserStore.Get(ctx, id)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, id, user)
return user, nil
}
// GetMany is a cache wrapper around the SqlStore method to get a user profiles by ids.
// It checks if the user entries are present in the cache, returning the entries from cache
// if it is present. Otherwise, it fetches the entries from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
// we are doing a loop instead of caching the full set in the cache because the number of permutations that we can have
// in this func is making caching of the total set not beneficial.
var cachedUsers []*model.User
var notCachedUserIds []string
uniqIDs := dedup(ids)
fromMaster := false
for _, id := range uniqIDs {
var cachedUser *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cachedUser); err == nil {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
}
cachedUsers = append(cachedUsers, cachedUser)
} else {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[id] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, id)
}
s.userProfileByIdsMut.Unlock()
notCachedUserIds = append(notCachedUserIds, id)
}
}
if len(notCachedUserIds) > 0 {
if fromMaster {
ctx = sqlstore.WithMaster(ctx)
}
dbUsers, err := s.UserStore.GetMany(ctx, notCachedUserIds)
if err != nil {
return nil, err
}
for _, user := range dbUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
cachedUsers = append(cachedUsers, user)
}
}
return cachedUsers, nil
}
func dedup(elements []string) []string {
if len(elements) == 0 {
return elements
}
sort.Strings(elements)
j := 0
for i := 1; i < len(elements); i++ {
if elements[j] == elements[i] {
continue
}
j++
// preserve the original data
// in[i], in[j] = in[j], in[i]
// only set what is required
elements[j] = elements[i]
}
return elements[:j+1]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheWebhookStore struct {
store.WebhookStore
rootStore *LocalCacheStore
}
func (s *LocalCacheWebhookStore) handleClusterInvalidateWebhook(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.webhookCache.Purge()
} else {
s.rootStore.webhookCache.Remove(string(msg.Data))
}
}
func (s LocalCacheWebhookStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.webhookCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Purge")
}
}
func (s LocalCacheWebhookStore) InvalidateWebhookCache(webhookId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.webhookCache, webhookId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Remove by WebhookId")
}
}
func (s LocalCacheWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
if !allowFromCache {
return s.WebhookStore.GetIncoming(id, allowFromCache)
}
var incomingWebhook *model.IncomingWebhook
if err := s.rootStore.doStandardReadCache(s.rootStore.webhookCache, id, &incomingWebhook); err == nil {
return incomingWebhook, nil
}
incomingWebhook, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.webhookCache, id, incomingWebhook)
return incomingWebhook, nil
}
func (s LocalCacheWebhookStore) DeleteIncoming(webhookId string, time int64) error {
err := s.WebhookStore.DeleteIncoming(webhookId, time)
if err != nil {
return err
}
s.InvalidateWebhookCache(webhookId)
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByUser(userId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package opentracinglayer
import (
"context"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
)
type OpenTracingLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *OpenTracingLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *OpenTracingLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *OpenTracingLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *OpenTracingLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *OpenTracingLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *OpenTracingLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *OpenTracingLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *OpenTracingLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *OpenTracingLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *OpenTracingLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *OpenTracingLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *OpenTracingLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *OpenTracingLayer) Job() store.JobStore {
return s.JobStore
}
func (s *OpenTracingLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *OpenTracingLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *OpenTracingLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *OpenTracingLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *OpenTracingLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *OpenTracingLayer) Post() store.PostStore {
return s.PostStore
}
func (s *OpenTracingLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *OpenTracingLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *OpenTracingLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *OpenTracingLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *OpenTracingLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *OpenTracingLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *OpenTracingLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *OpenTracingLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *OpenTracingLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *OpenTracingLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *OpenTracingLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *OpenTracingLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *OpenTracingLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *OpenTracingLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *OpenTracingLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *OpenTracingLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *OpenTracingLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *OpenTracingLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *OpenTracingLayer) User() store.UserStore {
return s.UserStore
}
func (s *OpenTracingLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *OpenTracingLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *OpenTracingLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type OpenTracingLayerAuditStore struct {
store.AuditStore
Root *OpenTracingLayer
}
type OpenTracingLayerBotStore struct {
store.BotStore
Root *OpenTracingLayer
}
type OpenTracingLayerChannelStore struct {
store.ChannelStore
Root *OpenTracingLayer
}
type OpenTracingLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *OpenTracingLayer
}
type OpenTracingLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *OpenTracingLayer
}
type OpenTracingLayerCommandStore struct {
store.CommandStore
Root *OpenTracingLayer
}
type OpenTracingLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *OpenTracingLayer
}
type OpenTracingLayerComplianceStore struct {
store.ComplianceStore
Root *OpenTracingLayer
}
type OpenTracingLayerDraftStore struct {
store.DraftStore
Root *OpenTracingLayer
}
type OpenTracingLayerEmojiStore struct {
store.EmojiStore
Root *OpenTracingLayer
}
type OpenTracingLayerFileInfoStore struct {
store.FileInfoStore
Root *OpenTracingLayer
}
type OpenTracingLayerGroupStore struct {
store.GroupStore
Root *OpenTracingLayer
}
type OpenTracingLayerJobStore struct {
store.JobStore
Root *OpenTracingLayer
}
type OpenTracingLayerLicenseStore struct {
store.LicenseStore
Root *OpenTracingLayer
}
type OpenTracingLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *OpenTracingLayer
}
type OpenTracingLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *OpenTracingLayer
}
type OpenTracingLayerOAuthStore struct {
store.OAuthStore
Root *OpenTracingLayer
}
type OpenTracingLayerPluginStore struct {
store.PluginStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostStore struct {
store.PostStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostPriorityStore struct {
store.PostPriorityStore
Root *OpenTracingLayer
}
type OpenTracingLayerPreferenceStore struct {
store.PreferenceStore
Root *OpenTracingLayer
}
type OpenTracingLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *OpenTracingLayer
}
type OpenTracingLayerReactionStore struct {
store.ReactionStore
Root *OpenTracingLayer
}
type OpenTracingLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *OpenTracingLayer
}
type OpenTracingLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *OpenTracingLayer
}
type OpenTracingLayerRoleStore struct {
store.RoleStore
Root *OpenTracingLayer
}
type OpenTracingLayerSchemeStore struct {
store.SchemeStore
Root *OpenTracingLayer
}
type OpenTracingLayerSessionStore struct {
store.SessionStore
Root *OpenTracingLayer
}
type OpenTracingLayerSharedChannelStore struct {
store.SharedChannelStore
Root *OpenTracingLayer
}
type OpenTracingLayerStatusStore struct {
store.StatusStore
Root *OpenTracingLayer
}
type OpenTracingLayerSystemStore struct {
store.SystemStore
Root *OpenTracingLayer
}
type OpenTracingLayerTeamStore struct {
store.TeamStore
Root *OpenTracingLayer
}
type OpenTracingLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *OpenTracingLayer
}
type OpenTracingLayerThreadStore struct {
store.ThreadStore
Root *OpenTracingLayer
}
type OpenTracingLayerTokenStore struct {
store.TokenStore
Root *OpenTracingLayer
}
type OpenTracingLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *OpenTracingLayer
}
type OpenTracingLayerUploadSessionStore struct {
store.UploadSessionStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserStore struct {
store.UserStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *OpenTracingLayer
}
type OpenTracingLayerWebhookStore struct {
store.WebhookStore
Root *OpenTracingLayer
}
func (s *OpenTracingLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.AuditStore.Get(user_id, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerAuditStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.AuditStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerAuditStore) Save(audit *model.Audit) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.AuditStore.Save(audit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Get(userID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.GetAll(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) PermanentDelete(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.BotStore.PermanentDelete(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Save(bot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Update(bot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AnalyticsDeletedTypeCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AnalyticsTypeCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Autocomplete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AutocompleteInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AutocompleteInTeamForSearch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.ClearCaches()
}
func (s *OpenTracingLayerChannelStore) ClearMembersForUserCache() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearMembersForUserCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.ClearMembersForUserCache()
}
func (s *OpenTracingLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearSidebarOnTeamLeave")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CountPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CountUrgentPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateDirectChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateInitialSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) Delete(channelID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.Delete(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarChannelsByPreferences")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Get(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAll(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersById")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersById(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersNotifyPropsForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelsCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllDirectChannelsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByNameIncludeDeleted")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelCounts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelMembersForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelMembersTimezones")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelUnread")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithTeamDataByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeleted")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeletedByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetFileCount(channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetFileCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetFileCount(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCountFromCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.ChannelStore.GetMemberCountFromCache(channelID)
return result
}
func (s *OpenTracingLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCountsByGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByChannelIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithPagination")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersInfoByChannelIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMoreChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPinnedPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPinnedPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPinnedPosts(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPrivateChannelsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPublicChannelsByIdsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPublicChannelsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategoriesForTeamForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategoryOrder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamChannels(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamForChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamMembersForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GroupSyncedChannelCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GroupSyncedChannelCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IncrementMentionCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateAllChannelMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
}
func (s *OpenTracingLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateCacheForChannelMembersNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidateChannel(id string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateChannel(id)
}
func (s *OpenTracingLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateChannelByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateChannelByName(teamID, name)
}
func (s *OpenTracingLayerChannelStore) InvalidateGuestCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateGuestCount(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidateMemberCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateMemberCount(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidatePinnedPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidatePinnedPostCount(channelID)
}
func (s *OpenTracingLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IsUserInChannelUseCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
return result
}
func (s *OpenTracingLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.MigrateChannelMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) PermanentDelete(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDelete(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteMembersByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PostCountsByDuration")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveAllDeactivatedMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) RemoveMember(channelID string, userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveMember(channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveMembers(channelID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) ResetAllChannelSchemes() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ResetAllChannelSchemes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ResetAllChannelSchemes()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Restore(channelID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Restore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.Restore(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveDirectChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchAllChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchArchivedInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchForUserInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchGroupChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchMore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SetDeleteAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) SetShared(channelId string, shared bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SetShared")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.SetShared(channelId, shared)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Update(channel)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMemberNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMembersRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategoryOrder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarChannelCategoryOnMove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarChannelsByPreferences")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UserBelongsToChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetChannelsLeftSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetUsersInChannelDuring")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.LogJoinEvent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.LogLeaveEvent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Cleanup() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.Cleanup()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.Delete(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Exists")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.Exists(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.Save(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.SetLastPingAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.AnalyticsCommandCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) Delete(commandID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.Delete(commandID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) Get(id string) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.GetByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.GetByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.GetByTrigger")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.PermanentDeleteByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.PermanentDeleteByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Save(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Update(hook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) Cleanup() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.CommandWebhookStore.Cleanup()
}
func (s *OpenTracingLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandWebhookStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandWebhookStore.Save(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) TryUse(id string, limit int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.TryUse")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandWebhookStore.TryUse(id, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.ComplianceExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerComplianceStore) Get(id string) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.MessageExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Save(compliance)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Update(compliance)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.DraftStore.Delete(userID, channelID, rootID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.GetDraftsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Save(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Update(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.EmojiStore.Delete(emoji, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetList(offset, limit, sort)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetMultipleByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetMultipleByName(names)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Save(emoji)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.AttachToPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.FileInfoStore.ClearCaches()
}
func (s *OpenTracingLayerFileInfoStore) CountAll() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.CountAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.CountAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.DeleteForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.DeleteForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetByIds(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetByPath")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetByPath(path)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetFilesBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetFromMaster")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetFromMaster(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetStorageUsage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetUptoNSizeFileTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetWithOptions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.InvalidateFileInfosForPostCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
}
func (s *OpenTracingLayerFileInfoStore) PermanentDelete(fileID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.PermanentDelete(fileID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Save(info)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) SetContent(fileID string, content string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.SetContent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.SetContent(fileID, content)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Upsert")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Upsert(info)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.AdminRoleGroupsForSyncableMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersToAdd")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersToRemove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountChannelMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountGroupsByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountGroupsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountTeamMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Create")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Create(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CreateGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CreateWithUserIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CreateWithUserIds(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Delete(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Delete(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DistinctGroupMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DistinctGroupMemberCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DistinctGroupMemberCountForSource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Get(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Get(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetAllBySource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetAllBySource(groupSource)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetAllGroupSyncablesByGroupId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByIDs")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByIDs(groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByName(name, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByRemoteID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroups")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsAssociatedToChannelsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberCount(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCountWithRestrictions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsers(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersSortedPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetNonMemberUsersPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupChannelCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupChannelCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupChannelCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCountBySource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCountBySource(source)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCountWithAllowReference")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCountWithAllowReference()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupMemberCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupMemberCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupTeamCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupTeamCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.PermanentDeleteMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.PermittedSyncableAdmins")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Restore(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Restore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Restore(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersToAdd")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersToRemove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersToRemove(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Update(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpdateGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpsertMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpsertMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpsertMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.JobStore.Cleanup(expiryTime, batchSize)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerJobStore) Delete(id string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Get(id string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByStatus(status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByType(jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypeAndStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypePage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypesPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllPage(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetCountByStatusAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetNewestJobByStatusAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetNewestJobByStatusesAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Save(job *model.Job) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Save(job)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateOptimistically")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateStatus(id, status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateStatusOptimistically")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.Save(license)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LinkMetadataStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LinkMetadataStore.Get(url, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LinkMetadataStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LinkMetadataStore.Save(linkMetadata)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.DeleteBefore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.NotifyAdminStore.DeleteBefore(trial, now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.Get(trial)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.GetDataByUserIdAndFeature")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.Save(data)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) DeleteApp(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.DeleteApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.DeleteApp(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessData(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessDataByRefreshToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessDataByUserForApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetApp(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAppByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetApps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetApps(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAuthData(code)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAuthorizedApps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetPreviousAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.PermanentDeleteAuthDataByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAccessData(token string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAccessData(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAllAccessData() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAllAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAllAccessData()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAuthData(code string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAuthData(code)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAuthDataByClientId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveAccessData(accessData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveApp(app)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveAuthData(authData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.UpdateAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.UpdateAccessData(accessData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.UpdateApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.UpdateApp(app)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.CompareAndDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.CompareAndSet")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) Delete(pluginID string, key string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.Delete(pluginID, key)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) DeleteAllExpired() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.DeleteAllExpired")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.DeleteAllExpired()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.DeleteAllForPlugin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.DeleteAllForPlugin(PluginID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.Get(pluginID, key)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.List")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.List(pluginID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.SaveOrUpdate(keyVal)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.SetWithOptions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsPostCountsByDay")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsUserCountsWithPostsByDay")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.PostStore.ClearCaches()
}
func (s *OpenTracingLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.Delete(postID, timestamp, deleteByID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetDirectPostParentsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEditHistoryForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetEditHistoryForPost(postId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEtag")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
return result
}
func (s *OpenTracingLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPostsForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPostsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
span.SetTag("userID", userID)
span.SetTag("teamID", teamID)
span.SetTag("offset", offset)
span.SetTag("limit", limit)
defer span.Finish()
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetMaxPostSize() int {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetMaxPostSize")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.PostStore.GetMaxPostSize()
return result
}
func (s *OpenTracingLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetNthRecentPostTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetNthRecentPostTime(n)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetOldest() (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetOldest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetOldest()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetOldestEntityCreationTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetOldestEntityCreationTime()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetParentsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostAfterTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostIdAfterTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostIdBeforeTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostReminderMetadata")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostReminderMetadata(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostReminders")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostReminders(now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsBefore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsByIds(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsByThread")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsByThread(threadID, since)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsCreatedAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsSinceForSync")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRecentSearchesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetRecentSearchesForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRepliesForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetRepliesForExport(parentID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetSingle")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetSingle(id, inclDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetTopDMsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.InvalidateLastPostTimeCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.PostStore.InvalidateLastPostTimeCache(channelID)
}
func (s *OpenTracingLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.LogRecentSearch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Overwrite")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Overwrite(post)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.OverwriteMultiple")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Save(post *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Save(post)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SaveMultiple")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Search(teamID, userID, params)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SearchPostsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SetPostReminder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.SetPostReminder(reminder)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Update(newPost, oldPost)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPriorityStore.GetForPost(postId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPriorityStore.GetForPosts(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.CleanupFlagsBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) Delete(userID string, category string, name string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.Delete(userID, category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteCategory(userID string, category string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.DeleteCategory(userID, category)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteCategoryAndName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.Get(userID, category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetAll(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetCategory(userID, category)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetCategoryAndName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) Save(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.Save(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) Clear(notices []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.Clear")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.Clear(notices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.ClearOldNotices")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.GetViews")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ProductNoticesStore.GetViews(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerProductNoticesStore) View(userID string, notices []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.View")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.View(userID, notices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.BulkGetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.BulkGetForPosts(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.Delete(reaction)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteAllWithEmojiName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetForPostSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.Save(reaction)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Get(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.GetAll(filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Save(rc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.SetLastPingAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Update(rc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.UpdateTopics")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.AddChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.AddTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelPoliciesCountForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelPoliciesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesCountForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Patch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Patch(patch)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.RemoveChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.RemoveTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Save(policy)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.AllChannelSchemeRoles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.AllChannelSchemeRoles()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelHigherScopedPermissions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelRolesUnderTeamRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) Delete(roleID string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Delete(roleID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) Get(roleID string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Get(roleID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetAll() ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetByName(ctx, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetByNames(names)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) PermanentDeleteAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.PermanentDeleteAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RoleStore.PermanentDeleteAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Save(role)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) CountByScope(scope string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.CountByScope")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.CountByScope(scope)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.CountWithoutPermission")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Delete(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Get(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.GetByName(schemeName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) PermanentDeleteAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.PermanentDeleteAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SchemeStore.PermanentDeleteAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Save(scheme)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) AnalyticsSessionCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.AnalyticsSessionCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.AnalyticsSessionCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.Cleanup(expiryTime, batchSize)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessions(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessionsExpired")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessionsWithActiveDeviceIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.PermanentDeleteSessionsByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Remove(sessionIDOrToken string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Remove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.Remove(sessionIDOrToken)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) RemoveAllSessions() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.RemoveAllSessions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.RemoveAllSessions()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.Save(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateDeviceId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateExpiredNotify")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateExpiresAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateLastActivityAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateProps(session *model.Session) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateProps(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateRoles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.UpdateRoles(userID, roles)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Delete(channelId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Delete(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.DeleteRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Get(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAllCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAllCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemote(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemoteByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemoteForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemotes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemotes(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemotesStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetSingleUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetUsersForSync")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetUsersForSync(filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetUsersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetUsersForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.HasChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.HasChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.HasRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Save(sc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveAttachment(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveRemote(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveUser(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Update(sc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateAttachmentLastSyncAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.UpdateRemote(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateRemoteCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateUserLastSyncAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpsertAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.UpsertAttachment(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) Get(userID string) (*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.Get(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.GetByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.GetByIds(userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.GetTotalActiveUsersCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.GetTotalActiveUsersCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) ResetAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.ResetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.ResetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerStatusStore) SaveOrUpdate(status *model.Status) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.SaveOrUpdate(status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.UpdateExpiredDNDStatuses")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.UpdateLastActivityAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) Get() (model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.Get()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) GetByName(name string) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.GetByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.InsertIfExists")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.InsertIfExists(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.PermanentDeleteByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.PermanentDeleteByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) Save(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.Save(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) SaveOrUpdate(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.SaveOrUpdate(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.SaveOrUpdateWithWarnMetricHandling")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) Update(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.Update(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.AnalyticsGetTeamCountForScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.AnalyticsTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.AnalyticsTeamCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TeamStore.ClearCaches()
}
func (s *OpenTracingLayerTeamStore) Get(id string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetActiveMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAll() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllPrivateTeamListing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllPrivateTeamListing()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllTeamListing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllTeamListing()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByEmptyInviteID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByEmptyInviteID()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByInviteId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByInviteId(inviteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByName(name string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByNames(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetChannelUnreadsForAllTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetChannelUnreadsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForTwoUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMany(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMembersByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetNewTeamMembersSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamMembersForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamMembersForExport(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsByScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsByUserId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsByUserId(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsForUserWithPagination")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTotalMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetUserTeamIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GroupSyncedTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GroupSyncedTeamCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.InvalidateAllTeamIdsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
}
func (s *OpenTracingLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.MigrateTeamMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) PermanentDelete(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.PermanentDelete(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveAllMembersByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveAllMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveAllMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveAllMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveMember(teamID string, userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveMember(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveMembers(teamID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) ResetAllTeamSchemes() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ResetAllTeamSchemes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.ResetAllTeamSchemes()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Save(team)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SaveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SaveMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchAll(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAllPaged")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchOpen")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchOpen(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchPrivate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchPrivate(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Update(team)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateLastTeamIconUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UpdateMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMembersRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UpdateMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UserBelongsToTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.GetLatest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.Save(termsOfService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) Get(id string) (*model.Thread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetMembershipForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetMembershipsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTeamsUnreadForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadFollowers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadUnreadReplyCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalThreads")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadMentions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadThreads")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadUrgentMentions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MaintainMembership")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsRead")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsReadByChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsReadByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAsRead")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.UpdateMembership")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.UpdateMembership(membership)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) Cleanup(expiryTime int64) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TokenStore.Cleanup(expiryTime)
}
func (s *OpenTracingLayerTokenStore) Delete(token string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.Delete(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.GetAllTokensByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TokenStore.GetAllTokensByType(tokenType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) GetByToken(token string) (*model.Token, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.GetByToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TokenStore.GetByToken(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.RemoveAllTokensByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.RemoveAllTokensByType(tokenType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTokenStore) Save(recovery *model.Token) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.Save(recovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.CreateTrueUpReviewStatusRecord")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.GetTrueUpReviewStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UploadSessionStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.Get(ctx, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.GetForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.GetForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.Save(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Update(session *model.UploadSession) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UploadSessionStore.Update(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsActiveCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsActiveCountForPeriod")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetExternalUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetGuestCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetInactiveUsersCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetSystemAdminCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AutocompleteUsersInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.ClearCaches()
}
func (s *OpenTracingLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Count")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Count(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) DeactivateGuests() ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.DeactivateGuests")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.DeactivateGuests()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.DemoteUserToGuest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.DemoteUserToGuest(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Get(ctx, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAll() ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllNotInAuthService")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllNotInAuthService(authServices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllProfiles(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllProfilesInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllUsingAuthService")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllUsingAuthService(authService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAnyUnreadPostCountForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByAuth")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByAuth(authData, authService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByEmail(email string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByEmail")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByEmail(email)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByUsername(username string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByUsername")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByUsername(username)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetChannelGroupUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetChannelGroupUsers(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetEtagForAllProfiles() string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForAllProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForAllProfiles()
return result
}
func (s *OpenTracingLayerUserStore) GetEtagForProfiles(teamID string) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForProfiles(teamID)
return result
}
func (s *OpenTracingLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForProfilesNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForProfilesNotInTeam(teamID)
return result
}
func (s *OpenTracingLayerUserStore) GetFirstSystemAdminID() (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetFirstSystemAdminID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetFirstSystemAdminID()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetForLogin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetKnownUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetKnownUsers(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetMany(ctx, ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetNewUsersForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfileByGroupChannelIdsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfileByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfiles(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesByUsernames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannel(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannelByAdmin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannelByStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesWithoutTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesWithoutTeam(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetRecentlyActiveUsersForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetSystemAdminProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetSystemAdminProfiles()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetTeamGroupUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetTeamGroupUsers(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUnreadCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUnreadCountForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersWithInvalidEmails")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) InferSystemInstallDate() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InferSystemInstallDate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.InferSystemInstallDate()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) InsertUsers(users []*model.User) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InsertUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.InsertUsers(users)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) InvalidateProfileCacheForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfileCacheForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfileCacheForUser(userID)
}
func (s *OpenTracingLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfilesInChannelCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfilesInChannelCache(channelID)
}
func (s *OpenTracingLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfilesInChannelCacheByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
}
func (s *OpenTracingLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.IsEmpty")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.IsEmpty(excludeBots)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) PermanentDelete(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.PermanentDelete(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) PromoteGuestToUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.PromoteGuestToUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.PromoteGuestToUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ResetAuthDataToEmailForUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) ResetLastPictureUpdate(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ResetLastPictureUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.ResetLastPictureUpdate(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) Save(user *model.User) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Save(user)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Search(teamID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchInChannel(channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchInGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchInGroup(groupID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchWithoutTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchWithoutTeam(term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Update(user, allowRoleUpdate)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateFailedPasswordAttempts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateLastPictureUpdate(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateLastPictureUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateLastPictureUpdate(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateMfaActive(userID string, active bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateMfaActive")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateMfaActive(userID, active)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateMfaSecret")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateMfaSecret(userID, secret)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateNotifyProps(userID, props)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdatePassword(userID string, newPassword string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdatePassword")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdatePassword(userID, newPassword)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateUpdateAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.UpdateUpdateAt(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.VerifyEmail")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.VerifyEmail(userID, email)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Delete(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.Delete(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.DeleteAllForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Get(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetByToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Save(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Search(term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.UpdateTokenDisable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.UpdateTokenEnable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsIncomingCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsOutgoingCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.WebhookStore.ClearCaches()
}
func (s *OpenTracingLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.DeleteIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.DeleteOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByTeamByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingList(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingListByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoing(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByChannelByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByTeamByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingListByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) InvalidateWebhookCache(webhook string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.InvalidateWebhookCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.WebhookStore.InvalidateWebhookCache(webhook)
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteIncomingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteIncomingByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteOutgoingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteOutgoingByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.SaveIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.SaveIncoming(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.SaveOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.SaveOutgoing(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.UpdateIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.UpdateIncoming(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.UpdateOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.UpdateOutgoing(hook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayer) Close() {
s.Store.Close()
}
func (s *OpenTracingLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *OpenTracingLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *OpenTracingLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *OpenTracingLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *OpenTracingLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *OpenTracingLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *OpenTracingLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *OpenTracingLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore := OpenTracingLayer{
Store: childStore,
}
newStore.AuditStore = &OpenTracingLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &OpenTracingLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &OpenTracingLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &OpenTracingLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &OpenTracingLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &OpenTracingLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &OpenTracingLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &OpenTracingLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &OpenTracingLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &OpenTracingLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &OpenTracingLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &OpenTracingLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &OpenTracingLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &OpenTracingLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &OpenTracingLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &OpenTracingLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &OpenTracingLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &OpenTracingLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &OpenTracingLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &OpenTracingLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &OpenTracingLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &OpenTracingLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &OpenTracingLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &OpenTracingLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &OpenTracingLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &OpenTracingLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &OpenTracingLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &OpenTracingLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &OpenTracingLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &OpenTracingLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &OpenTracingLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &OpenTracingLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &OpenTracingLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &OpenTracingLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package retrylayer
import (
"context"
"time"
timepkg "time"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/pkg/errors"
)
const mySQLDeadlockCode = uint16(1213)
type RetryLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *RetryLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *RetryLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *RetryLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *RetryLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *RetryLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *RetryLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *RetryLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *RetryLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *RetryLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *RetryLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *RetryLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *RetryLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *RetryLayer) Job() store.JobStore {
return s.JobStore
}
func (s *RetryLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *RetryLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *RetryLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *RetryLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *RetryLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *RetryLayer) Post() store.PostStore {
return s.PostStore
}
func (s *RetryLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *RetryLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *RetryLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *RetryLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *RetryLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *RetryLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *RetryLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *RetryLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *RetryLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *RetryLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *RetryLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *RetryLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *RetryLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *RetryLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *RetryLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *RetryLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *RetryLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *RetryLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *RetryLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *RetryLayer) User() store.UserStore {
return s.UserStore
}
func (s *RetryLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *RetryLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *RetryLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type RetryLayerAuditStore struct {
store.AuditStore
Root *RetryLayer
}
type RetryLayerBotStore struct {
store.BotStore
Root *RetryLayer
}
type RetryLayerChannelStore struct {
store.ChannelStore
Root *RetryLayer
}
type RetryLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *RetryLayer
}
type RetryLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *RetryLayer
}
type RetryLayerCommandStore struct {
store.CommandStore
Root *RetryLayer
}
type RetryLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *RetryLayer
}
type RetryLayerComplianceStore struct {
store.ComplianceStore
Root *RetryLayer
}
type RetryLayerDraftStore struct {
store.DraftStore
Root *RetryLayer
}
type RetryLayerEmojiStore struct {
store.EmojiStore
Root *RetryLayer
}
type RetryLayerFileInfoStore struct {
store.FileInfoStore
Root *RetryLayer
}
type RetryLayerGroupStore struct {
store.GroupStore
Root *RetryLayer
}
type RetryLayerJobStore struct {
store.JobStore
Root *RetryLayer
}
type RetryLayerLicenseStore struct {
store.LicenseStore
Root *RetryLayer
}
type RetryLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *RetryLayer
}
type RetryLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *RetryLayer
}
type RetryLayerOAuthStore struct {
store.OAuthStore
Root *RetryLayer
}
type RetryLayerPluginStore struct {
store.PluginStore
Root *RetryLayer
}
type RetryLayerPostStore struct {
store.PostStore
Root *RetryLayer
}
type RetryLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *RetryLayer
}
type RetryLayerPostPriorityStore struct {
store.PostPriorityStore
Root *RetryLayer
}
type RetryLayerPreferenceStore struct {
store.PreferenceStore
Root *RetryLayer
}
type RetryLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *RetryLayer
}
type RetryLayerReactionStore struct {
store.ReactionStore
Root *RetryLayer
}
type RetryLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *RetryLayer
}
type RetryLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *RetryLayer
}
type RetryLayerRoleStore struct {
store.RoleStore
Root *RetryLayer
}
type RetryLayerSchemeStore struct {
store.SchemeStore
Root *RetryLayer
}
type RetryLayerSessionStore struct {
store.SessionStore
Root *RetryLayer
}
type RetryLayerSharedChannelStore struct {
store.SharedChannelStore
Root *RetryLayer
}
type RetryLayerStatusStore struct {
store.StatusStore
Root *RetryLayer
}
type RetryLayerSystemStore struct {
store.SystemStore
Root *RetryLayer
}
type RetryLayerTeamStore struct {
store.TeamStore
Root *RetryLayer
}
type RetryLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *RetryLayer
}
type RetryLayerThreadStore struct {
store.ThreadStore
Root *RetryLayer
}
type RetryLayerTokenStore struct {
store.TokenStore
Root *RetryLayer
}
type RetryLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *RetryLayer
}
type RetryLayerUploadSessionStore struct {
store.UploadSessionStore
Root *RetryLayer
}
type RetryLayerUserStore struct {
store.UserStore
Root *RetryLayer
}
type RetryLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *RetryLayer
}
type RetryLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *RetryLayer
}
type RetryLayerWebhookStore struct {
store.WebhookStore
Root *RetryLayer
}
func isRepeatableError(err error) bool {
var pqErr *pq.Error
var mysqlErr *mysql.MySQLError
switch {
case errors.As(errors.Cause(err), &pqErr):
if pqErr.Code == "40001" || pqErr.Code == "40P01" {
return true
}
case errors.As(errors.Cause(err), &mysqlErr):
if mysqlErr.Number == mySQLDeadlockCode {
return true
}
}
return false
}
func (s *RetryLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
tries := 0
for {
result, err := s.AuditStore.Get(user_id, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAuditStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.AuditStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAuditStore) Save(audit *model.Audit) error {
tries := 0
for {
err := s.AuditStore.Save(audit)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Get(userID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.GetAll(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) PermanentDelete(userID string) error {
tries := 0
for {
err := s.BotStore.PermanentDelete(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Save(bot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Update(bot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.ChannelStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ClearCaches() {
s.ChannelStore.ClearCaches()
}
func (s *RetryLayerChannelStore) ClearMembersForUserCache() {
s.ChannelStore.ClearMembersForUserCache()
}
func (s *RetryLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
tries := 0
for {
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
tries := 0
for {
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Delete(channelID string, timestamp int64) error {
tries := 0
for {
err := s.ChannelStore.Delete(channelID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
tries := 0
for {
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
tries := 0
for {
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Get(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAll(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersById(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelsCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetFileCount(channelID string) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetFileCount(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
return s.ChannelStore.GetMemberCountFromCache(channelID)
}
func (s *RetryLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPinnedPosts(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamChannels(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamForChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GroupSyncedChannelCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
tries := 0
for {
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
}
func (s *RetryLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (s *RetryLayerChannelStore) InvalidateChannel(id string) {
s.ChannelStore.InvalidateChannel(id)
}
func (s *RetryLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
s.ChannelStore.InvalidateChannelByName(teamID, name)
}
func (s *RetryLayerChannelStore) InvalidateGuestCount(channelID string) {
s.ChannelStore.InvalidateGuestCount(channelID)
}
func (s *RetryLayerChannelStore) InvalidateMemberCount(channelID string) {
s.ChannelStore.InvalidateMemberCount(channelID)
}
func (s *RetryLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
s.ChannelStore.InvalidatePinnedPostCount(channelID)
}
func (s *RetryLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
return s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
}
func (s *RetryLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
tries := 0
for {
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDelete(channelID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDelete(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
tries := 0
for {
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
tries := 0
for {
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveMember(channelID string, userID string) error {
tries := 0
for {
err := s.ChannelStore.RemoveMember(channelID, userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
tries := 0
for {
err := s.ChannelStore.RemoveMembers(channelID, userIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ResetAllChannelSchemes() error {
tries := 0
for {
err := s.ChannelStore.ResetAllChannelSchemes()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Restore(channelID string, timestamp int64) error {
tries := 0
for {
err := s.ChannelStore.Restore(channelID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
tries := 0
for {
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SetShared(channelId string, shared bool) error {
tries := 0
for {
err := s.ChannelStore.SetShared(channelId, shared)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Update(channel)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
tries := 0
for {
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
tries := 0
for {
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
tries := 0
for {
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
tries := 0
for {
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Cleanup() error {
tries := 0
for {
err := s.ClusterDiscoveryStore.Cleanup()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.Delete(discovery)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.Exists(discovery)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
tries := 0
for {
err := s.ClusterDiscoveryStore.Save(discovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
tries := 0
for {
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Delete(commandID string, timestamp int64) error {
tries := 0
for {
err := s.CommandStore.Delete(commandID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Get(id string) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.GetByTeam(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
tries := 0
for {
err := s.CommandStore.PermanentDeleteByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.CommandStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Save(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Update(hook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) Cleanup() {
s.CommandWebhookStore.Cleanup()
}
func (s *RetryLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
tries := 0
for {
result, err := s.CommandWebhookStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
tries := 0
for {
result, err := s.CommandWebhookStore.Save(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) TryUse(id string, limit int) error {
tries := 0
for {
err := s.CommandWebhookStore.TryUse(id, limit)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Get(id string) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
tries := 0
for {
result, err := s.ComplianceStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Save(compliance)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Update(compliance)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
tries := 0
for {
err := s.DraftStore.Delete(userID, channelID, rootID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Save(d)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Update(d)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
tries := 0
for {
err := s.EmojiStore.Delete(emoji, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetList(offset, limit, sort)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetMultipleByName(names)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Save(emoji)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
tries := 0
for {
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) ClearCaches() {
s.FileInfoStore.ClearCaches()
}
func (s *RetryLayerFileInfoStore) CountAll() (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.CountAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
tries := 0
for {
result, err := s.FileInfoStore.DeleteForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetByIds(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetByPath(path)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetFromMaster(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
}
func (s *RetryLayerFileInfoStore) PermanentDelete(fileID string) error {
tries := 0
for {
err := s.FileInfoStore.PermanentDelete(fileID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Save(info)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
tries := 0
for {
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) SetContent(fileID string, content string) error {
tries := 0
for {
err := s.FileInfoStore.SetContent(fileID, content)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Upsert(info)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
tries := 0
for {
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Create(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.CreateWithUserIds(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Delete(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Delete(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.DistinctGroupMemberCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Get(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Get(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetAllBySource(groupSource)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByIDs(groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByName(name, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.GetMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberCount(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsers(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupChannelCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupChannelCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCountBySource(source)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCountWithAllowReference()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupMemberCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupMemberCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupTeamCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupTeamCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
tries := 0
for {
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
tries := 0
for {
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Restore(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Restore(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersToRemove(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Update(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.UpsertMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
tries := 0
for {
err := s.JobStore.Cleanup(expiryTime, batchSize)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Delete(id string) (string, error) {
tries := 0
for {
result, err := s.JobStore.Delete(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Get(id string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByStatus(status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByType(jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllPage(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
tries := 0
for {
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Save(job *model.Job) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.Save(job)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
tries := 0
for {
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.UpdateStatus(id, status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
tries := 0
for {
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.Save(license)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
tries := 0
for {
result, err := s.LinkMetadataStore.Get(url, timestamp)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
tries := 0
for {
result, err := s.LinkMetadataStore.Save(linkMetadata)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
tries := 0
for {
err := s.NotifyAdminStore.DeleteBefore(trial, now)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.Get(trial)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.Save(data)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
tries := 0
for {
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) DeleteApp(id string) error {
tries := 0
for {
err := s.OAuthStore.DeleteApp(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessData(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetApp(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetApps(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAuthData(code)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
tries := 0
for {
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAccessData(token string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAccessData(token)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAllAccessData() error {
tries := 0
for {
err := s.OAuthStore.RemoveAllAccessData()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAuthData(code string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAuthData(code)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveAccessData(accessData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveApp(app)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveAuthData(authData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.UpdateAccessData(accessData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.UpdateApp(app)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) Delete(pluginID string, key string) error {
tries := 0
for {
err := s.PluginStore.Delete(pluginID, key)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) DeleteAllExpired() error {
tries := 0
for {
err := s.PluginStore.DeleteAllExpired()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
tries := 0
for {
err := s.PluginStore.DeleteAllForPlugin(PluginID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
tries := 0
for {
result, err := s.PluginStore.Get(pluginID, key)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
tries := 0
for {
result, err := s.PluginStore.List(pluginID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
tries := 0
for {
result, err := s.PluginStore.SaveOrUpdate(keyVal)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsPostCount(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) ClearCaches() {
s.PostStore.ClearCaches()
}
func (s *RetryLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
tries := 0
for {
err := s.PostStore.Delete(postID, timestamp, deleteByID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.PostStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetEditHistoryForPost(postId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
return s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
}
func (s *RetryLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetMaxPostSize() int {
return s.PostStore.GetMaxPostSize()
}
func (s *RetryLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
tries := 0
for {
result, err := s.PostStore.GetNthRecentPostTime(n)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetOldest() (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetOldest()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
tries := 0
for {
result, err := s.PostStore.GetOldestEntityCreationTime()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
tries := 0
for {
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
tries := 0
for {
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
tries := 0
for {
result, err := s.PostStore.GetPostReminderMetadata(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
tries := 0
for {
result, err := s.PostStore.GetPostReminders(now)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsByIds(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsByThread(threadID, since)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
tries := 0
for {
result, err := s.PostStore.GetRecentSearchesForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetRepliesForExport(parentID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetSingle(id, inclDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
tries := 0
for {
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
tries := 0
for {
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
s.PostStore.InvalidateLastPostTimeCache(channelID)
}
func (s *RetryLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
tries := 0
for {
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Overwrite(post)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteByChannel(channelID string) error {
tries := 0
for {
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.PostStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Save(post *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Save(post)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.Search(teamID, userID, params)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
tries := 0
for {
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
tries := 0
for {
err := s.PostStore.SetPostReminder(reminder)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Update(newPost, oldPost)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
tries := 0
for {
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
tries := 0
for {
result, err := s.PostPriorityStore.GetForPost(postId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
tries := 0
for {
result, err := s.PostPriorityStore.GetForPosts(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
tries := 0
for {
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Delete(userID string, category string, name string) error {
tries := 0
for {
err := s.PreferenceStore.Delete(userID, category, name)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteCategory(userID string, category string) error {
tries := 0
for {
err := s.PreferenceStore.DeleteCategory(userID, category)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
tries := 0
for {
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
tries := 0
for {
result, err := s.PreferenceStore.Get(userID, category, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetAll(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetCategory(userID, category)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.PreferenceStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Save(preferences model.Preferences) error {
tries := 0
for {
err := s.PreferenceStore.Save(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) Clear(notices []string) error {
tries := 0
for {
err := s.ProductNoticesStore.Clear(notices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
tries := 0
for {
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
tries := 0
for {
result, err := s.ProductNoticesStore.GetViews(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) View(userID string, notices []string) error {
tries := 0
for {
err := s.ProductNoticesStore.View(userID, notices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.BulkGetForPosts(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.Delete(reaction)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
tries := 0
for {
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.Save(reaction)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Get(remoteClusterId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.GetAll(filter)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Save(rc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
tries := 0
for {
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Update(rc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Delete(id string) error {
tries := 0
for {
err := s.RetentionPolicyStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetCount() (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Patch(patch)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Save(policy)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.AllChannelSchemeRoles()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
tries := 0
for {
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Delete(roleID string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Delete(roleID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Get(roleID string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Get(roleID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetAll() ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetByName(ctx, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetByNames(names)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) PermanentDeleteAll() error {
tries := 0
for {
err := s.RoleStore.PermanentDeleteAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Save(role)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) CountByScope(scope string) (int64, error) {
tries := 0
for {
result, err := s.SchemeStore.CountByScope(scope)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
tries := 0
for {
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Delete(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Get(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.GetByName(schemeName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) PermanentDeleteAll() error {
tries := 0
for {
err := s.SchemeStore.PermanentDeleteAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Save(scheme)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) AnalyticsSessionCount() (int64, error) {
tries := 0
for {
result, err := s.SessionStore.AnalyticsSessionCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
tries := 0
for {
err := s.SessionStore.Cleanup(expiryTime, batchSize)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessions(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
tries := 0
for {
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Remove(sessionIDOrToken string) error {
tries := 0
for {
err := s.SessionStore.Remove(sessionIDOrToken)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) RemoveAllSessions() error {
tries := 0
for {
err := s.SessionStore.RemoveAllSessions()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.Save(session)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
tries := 0
for {
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
tries := 0
for {
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
tries := 0
for {
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
tries := 0
for {
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateProps(session *model.Session) error {
tries := 0
for {
err := s.SessionStore.UpdateProps(session)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
tries := 0
for {
result, err := s.SessionStore.UpdateRoles(userID, roles)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Delete(channelId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Delete(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Get(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAllCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemote(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemotes(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetUsersForSync(filter)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetUsersForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.HasChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Save(sc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveAttachment(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveRemote(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveUser(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Update(sc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.UpdateRemote(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
tries := 0
for {
result, err := s.SharedChannelStore.UpsertAttachment(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) Get(userID string) (*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.Get(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.GetByIds(userIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
tries := 0
for {
result, err := s.StatusStore.GetTotalActiveUsersCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) ResetAll() error {
tries := 0
for {
err := s.StatusStore.ResetAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) SaveOrUpdate(status *model.Status) error {
tries := 0
for {
err := s.StatusStore.SaveOrUpdate(status)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
tries := 0
for {
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Get() (model.StringMap, error) {
tries := 0
for {
result, err := s.SystemStore.Get()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) GetByName(name string) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.GetByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.InsertIfExists(system)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.PermanentDeleteByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Save(system *model.System) error {
tries := 0
for {
err := s.SystemStore.Save(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) SaveOrUpdate(system *model.System) error {
tries := 0
for {
err := s.SystemStore.SaveOrUpdate(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
tries := 0
for {
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Update(system *model.System) error {
tries := 0
for {
err := s.SystemStore.Update(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.AnalyticsTeamCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.TeamStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ClearCaches() {
s.TeamStore.ClearCaches()
}
func (s *RetryLayerTeamStore) Get(id string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAll() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllPrivateTeamListing()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllTeamListing()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByEmptyInviteID()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByInviteId(inviteID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByName(name string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByNames(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
tries := 0
for {
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetMany(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
tries := 0
for {
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamMembersForExport(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsByUserId(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
tries := 0
for {
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GroupSyncedTeamCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
}
func (s *RetryLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
tries := 0
for {
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) PermanentDelete(teamID string) error {
tries := 0
for {
err := s.TeamStore.PermanentDelete(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
tries := 0
for {
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveAllMembersByUser(userID string) error {
tries := 0
for {
err := s.TeamStore.RemoveAllMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveMember(teamID string, userID string) error {
tries := 0
for {
err := s.TeamStore.RemoveMember(teamID, userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
tries := 0
for {
err := s.TeamStore.RemoveMembers(teamID, userIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ResetAllTeamSchemes() error {
tries := 0
for {
err := s.TeamStore.ResetAllTeamSchemes()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Save(team)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchAll(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
tries := 0
for {
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchOpen(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchPrivate(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Update(team)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
tries := 0
for {
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.UpdateMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
tries := 0
for {
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.UpdateMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
tries := 0
for {
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.Save(termsOfService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
tries := 0
for {
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) Get(id string) (*model.Thread, error) {
tries := 0
for {
result, err := s.ThreadStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
tries := 0
for {
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.UpdateMembership(membership)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) Cleanup(expiryTime int64) {
s.TokenStore.Cleanup(expiryTime)
}
func (s *RetryLayerTokenStore) Delete(token string) error {
tries := 0
for {
err := s.TokenStore.Delete(token)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
tries := 0
for {
result, err := s.TokenStore.GetAllTokensByType(tokenType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) GetByToken(token string) (*model.Token, error) {
tries := 0
for {
result, err := s.TokenStore.GetByToken(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
tries := 0
for {
err := s.TokenStore.RemoveAllTokensByType(tokenType)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) Save(recovery *model.Token) error {
tries := 0
for {
err := s.TokenStore.Save(recovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Delete(id string) error {
tries := 0
for {
err := s.UploadSessionStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.Get(ctx, id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.GetForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.Save(session)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Update(session *model.UploadSession) error {
tries := 0
for {
err := s.UploadSessionStore.Update(session)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetGuestCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
tries := 0
for {
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.UserStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ClearCaches() {
s.UserStore.ClearCaches()
}
func (s *RetryLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.Count(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) DeactivateGuests() ([]string, error) {
tries := 0
for {
result, err := s.UserStore.DeactivateGuests()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.DemoteUserToGuest(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Get(ctx, id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAll() ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllNotInAuthService(authServices)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllProfiles(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllUsingAuthService(authService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByAuth(authData, authService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByEmail(email string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByEmail(email)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByUsername(username string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByUsername(username)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetChannelGroupUsers(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetEtagForAllProfiles() string {
return s.UserStore.GetEtagForAllProfiles()
}
func (s *RetryLayerUserStore) GetEtagForProfiles(teamID string) string {
return s.UserStore.GetEtagForProfiles(teamID)
}
func (s *RetryLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
return s.UserStore.GetEtagForProfilesNotInTeam(teamID)
}
func (s *RetryLayerUserStore) GetFirstSystemAdminID() (string, error) {
tries := 0
for {
result, err := s.UserStore.GetFirstSystemAdminID()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
tries := 0
for {
result, err := s.UserStore.GetKnownUsers(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetMany(ctx, ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfiles(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannel(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesWithoutTeam(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetSystemAdminProfiles()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetTeamGroupUsers(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
tries := 0
for {
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InferSystemInstallDate() (int64, error) {
tries := 0
for {
result, err := s.UserStore.InferSystemInstallDate()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InsertUsers(users []*model.User) error {
tries := 0
for {
err := s.UserStore.InsertUsers(users)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InvalidateProfileCacheForUser(userID string) {
s.UserStore.InvalidateProfileCacheForUser(userID)
}
func (s *RetryLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
s.UserStore.InvalidateProfilesInChannelCache(channelID)
}
func (s *RetryLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
}
func (s *RetryLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
tries := 0
for {
result, err := s.UserStore.IsEmpty(excludeBots)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) PermanentDelete(userID string) error {
tries := 0
for {
err := s.UserStore.PermanentDelete(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) PromoteGuestToUser(userID string) error {
tries := 0
for {
err := s.UserStore.PromoteGuestToUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
tries := 0
for {
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ResetLastPictureUpdate(userID string) error {
tries := 0
for {
err := s.UserStore.ResetLastPictureUpdate(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Save(user *model.User) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Save(user)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Search(teamID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchInChannel(channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchInGroup(groupID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchWithoutTeam(term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
tries := 0
for {
result, err := s.UserStore.Update(user, allowRoleUpdate)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
tries := 0
for {
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
tries := 0
for {
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateLastPictureUpdate(userID string) error {
tries := 0
for {
err := s.UserStore.UpdateLastPictureUpdate(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateMfaActive(userID string, active bool) error {
tries := 0
for {
err := s.UserStore.UpdateMfaActive(userID, active)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
tries := 0
for {
err := s.UserStore.UpdateMfaSecret(userID, secret)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
tries := 0
for {
err := s.UserStore.UpdateNotifyProps(userID, props)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdatePassword(userID string, newPassword string) error {
tries := 0
for {
err := s.UserStore.UpdatePassword(userID, newPassword)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.UpdateUpdateAt(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
tries := 0
for {
result, err := s.UserStore.VerifyEmail(userID, email)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Delete(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.Delete(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Get(tokenID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Save(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Search(term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
tries := 0
for {
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
tries := 0
for {
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
tries := 0
for {
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) ClearCaches() {
s.WebhookStore.ClearCaches()
}
func (s *RetryLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
tries := 0
for {
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
tries := 0
for {
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingList(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoing(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) InvalidateWebhookCache(webhook string) {
s.WebhookStore.InvalidateWebhookCache(webhook)
}
func (s *RetryLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.SaveIncoming(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.SaveOutgoing(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.UpdateIncoming(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.UpdateOutgoing(hook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayer) Close() {
s.Store.Close()
}
func (s *RetryLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *RetryLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *RetryLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *RetryLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *RetryLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *RetryLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *RetryLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *RetryLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store) *RetryLayer {
newStore := RetryLayer{
Store: childStore,
}
newStore.AuditStore = &RetryLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &RetryLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &RetryLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &RetryLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &RetryLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &RetryLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &RetryLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &RetryLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &RetryLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &RetryLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &RetryLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &RetryLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &RetryLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &RetryLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &RetryLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &RetryLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &RetryLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &RetryLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &RetryLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &RetryLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &RetryLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &RetryLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &RetryLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &RetryLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &RetryLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &RetryLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &RetryLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &RetryLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &RetryLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &RetryLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &RetryLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &RetryLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &RetryLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &RetryLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchChannelStore struct {
store.ChannelStore
rootStore *SearchStore
}
func (c *SearchChannelStore) deleteChannelIndex(channel *model.Channel) {
if channel.Type == model.ChannelTypeOpen {
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannel(channel); err != nil {
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed channel from index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
}
func (c *SearchChannelStore) indexChannel(channel *model.Channel) {
var userIDs, teamMemberIDs []string
var err error
if channel.Type == model.ChannelTypePrivate {
userIDs, err = c.GetAllChannelMembersById(channel.Id)
if err != nil {
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
}
teamMemberIDs, err = c.GetTeamMembersForChannel(channel.Id)
if err != nil {
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.IndexChannel(channel, userIDs, teamMemberIDs); err != nil {
mlog.Warn("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Indexed channel in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
func (c *SearchChannelStore) Save(channel *model.Channel, maxChannels int64) (*model.Channel, error) {
newChannel, err := c.ChannelStore.Save(channel, maxChannels)
if err == nil {
c.indexChannel(newChannel)
}
return newChannel, err
}
func (c *SearchChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
updatedChannel, err := c.ChannelStore.Update(channel)
if err == nil {
c.indexChannel(updatedChannel)
}
return updatedChannel, err
}
func (c *SearchChannelStore) UpdateMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.UpdateMember(cm)
if err == nil {
c.rootStore.indexUserFromID(cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(channel)
c.rootStore.indexUserFromID(channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) SaveMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.SaveMember(cm)
if err == nil {
c.rootStore.indexUserFromID(cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(channel)
c.rootStore.indexUserFromID(channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) RemoveMember(channelID, userIdToRemove string) error {
err := c.ChannelStore.RemoveMember(channelID, userIdToRemove)
if err == nil {
c.rootStore.indexUserFromID(userIdToRemove)
}
channel, err := c.ChannelStore.Get(channelID, true)
if err == nil {
c.indexChannel(channel)
}
return err
}
func (c *SearchChannelStore) RemoveMembers(channelID string, userIds []string) error {
if err := c.ChannelStore.RemoveMembers(channelID, userIds); err != nil {
return err
}
channel, err := c.ChannelStore.Get(channelID, true)
if err == nil {
c.indexChannel(channel)
}
for _, uid := range userIds {
c.rootStore.indexUserFromID(uid)
}
return nil
}
func (c *SearchChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
channel, err := c.ChannelStore.CreateDirectChannel(user, otherUser, channelOptions...)
if err == nil {
c.rootStore.indexUserFromID(user.Id)
c.rootStore.indexUserFromID(otherUser.Id)
c.indexChannel(channel)
}
return channel, err
}
func (c *SearchChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
channel, err := c.ChannelStore.SaveDirectChannel(directchannel, member1, member2)
if err == nil {
c.rootStore.indexUserFromID(member1.UserId)
c.rootStore.indexUserFromID(member2.UserId)
c.indexChannel(channel)
}
return channel, err
}
func (c *SearchChannelStore) Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
var channelList model.ChannelListWithTeamData
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannelsAllTeams(engine, userID, term, includeDeleted, isGuest)
if err != nil {
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
mlog.Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
var channelList model.ChannelList
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannels(engine, teamID, userID, term, includeDeleted, isGuest)
if err != nil {
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
mlog.Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannels(engine searchengine.SearchEngineInterface, teamId, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
channelIds, err := engine.SearchChannels(teamId, userID, term, isGuest)
if err != nil {
return nil, err
}
channelList := model.ChannelList{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannelsAllTeams(engine searchengine.SearchEngineInterface, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
channelIds, err := engine.SearchChannels("", userID, term, isGuest)
if err != nil {
return nil, err
}
channelList := model.ChannelListWithTeamData{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) PermanentDeleteMembersByUser(userId string) error {
channels, errGetChannels := c.ChannelStore.GetChannelsByUser(userId, false, 0, -1, "")
if errGetChannels != nil {
mlog.Warn("Encountered error indexing channel after removing user", mlog.String("user_id", userId), mlog.Err(errGetChannels))
}
err := c.ChannelStore.PermanentDeleteMembersByUser(userId)
if err == nil {
c.rootStore.indexUserFromID(userId)
if errGetChannels == nil {
for _, ch := range channels {
c.indexChannel(ch)
}
}
}
return err
}
func (c *SearchChannelStore) RemoveAllDeactivatedMembers(channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
if errProfiles != nil {
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.RemoveAllDeactivatedMembers(channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
if user.DeleteAt != 0 {
c.rootStore.indexUser(user)
}
}
}
return err
}
func (c *SearchChannelStore) PermanentDeleteMembersByChannel(channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
if errProfiles != nil {
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.PermanentDeleteMembersByChannel(channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
c.rootStore.indexUser(user)
}
}
return err
}
func (c *SearchChannelStore) PermanentDelete(channelId string) error {
channel, channelErr := c.ChannelStore.Get(channelId, true)
if channelErr != nil {
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channelId), mlog.Err(channelErr))
}
err := c.ChannelStore.PermanentDelete(channelId)
if err == nil && channelErr == nil {
c.deleteChannelIndex(channel)
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchFileInfoStore struct {
store.FileInfoStore
rootStore *SearchStore
}
func (s SearchFileInfoStore) indexFile(file *model.FileInfo) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if file.PostId == "" {
return
}
post, postErr := s.rootStore.Post().GetSingle(file.PostId, false)
if postErr != nil {
mlog.Error("Couldn't get post for file for SearchEngine indexing.", mlog.String("post_id", file.PostId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("file_info_id", file.Id), mlog.Err(postErr))
return
}
if err := engineCopy.IndexFile(file, post.ChannelId); err != nil {
mlog.Error("Encountered error indexing file", mlog.String("file_info_id", file.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndex(fileID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFile(fileID); err != nil {
mlog.Error("Encountered error deleting file", mlog.String("file_info_id", fileID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexForUser(userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserFiles(userID); err != nil {
mlog.Error("Encountered error deleting files for user", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed user's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", userID))
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexForPost(postID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePostFiles(postID); err != nil {
mlog.Error("Encountered error deleting files for post", mlog.String("post_id", postID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed post's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", postID))
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexBatch(endTime, limit int64) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFilesBatch(endTime, limit); err != nil {
mlog.Error("Encountered error deleting a batch of files", mlog.Int64("limit", limit), mlog.Int64("end_time", endTime), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed batch of files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.Int64("end_time", endTime), mlog.Int64("limit", limit))
})
}
}
}
func (s SearchFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
nfile, err := s.FileInfoStore.Save(info)
if err == nil {
s.indexFile(nfile)
}
return nfile, err
}
func (s SearchFileInfoStore) SetContent(fileID, content string) error {
err := s.FileInfoStore.SetContent(fileID, content)
if err == nil {
nfile, err2 := s.FileInfoStore.GetFromMaster(fileID)
if err2 == nil {
nfile.Content = content
s.indexFile(nfile)
}
}
return err
}
func (s SearchFileInfoStore) AttachToPost(fileId, postId, channelId, creatorId string) error {
err := s.FileInfoStore.AttachToPost(fileId, postId, channelId, creatorId)
if err == nil {
nFileInfo, err2 := s.FileInfoStore.GetFromMaster(fileId)
if err2 == nil {
s.indexFile(nFileInfo)
}
}
return err
}
func (s SearchFileInfoStore) DeleteForPost(postId string) (string, error) {
result, err := s.FileInfoStore.DeleteForPost(postId)
if err == nil {
s.deleteFileIndexForPost(postId)
}
return result, err
}
func (s SearchFileInfoStore) PermanentDelete(fileId string) error {
err := s.FileInfoStore.PermanentDelete(fileId)
if err == nil {
s.deleteFileIndex(fileId)
}
return err
}
func (s SearchFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
s.deleteFileIndexBatch(endTime, limit)
}
return result, err
}
func (s SearchFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteByUser(userId)
if err == nil {
s.deleteFileIndexForUser(userId)
}
return result, err
}
func (s SearchFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if nErr != nil {
return nil, nErr
}
fileIds, appErr := engine.SearchFiles(userChannels, paramsList, page, perPage)
if appErr != nil {
mlog.Error("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(appErr))
continue
}
// Get the files
filesList := model.NewFileInfoList()
if len(fileIds) > 0 {
files, nErr := s.FileInfoStore.GetByIds(fileIds)
if nErr != nil {
return nil, nErr
}
for _, f := range files {
filesList.AddFileInfo(f)
filesList.AddOrder(f.Id)
}
}
return filesList, nil
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return model.NewFileInfoList(), nil
}
return s.FileInfoStore.Search(paramsList, userId, teamId, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"sync/atomic"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchStore struct {
store.Store
searchEngine *searchengine.Broker
user *SearchUserStore
team *SearchTeamStore
channel *SearchChannelStore
post *SearchPostStore
fileInfo *SearchFileInfoStore
configValue atomic.Value
}
func NewSearchLayer(baseStore store.Store, searchEngine *searchengine.Broker, cfg *model.Config) *SearchStore {
searchStore := &SearchStore{
Store: baseStore,
searchEngine: searchEngine,
}
searchStore.configValue.Store(cfg)
searchStore.channel = &SearchChannelStore{ChannelStore: baseStore.Channel(), rootStore: searchStore}
searchStore.post = &SearchPostStore{PostStore: baseStore.Post(), rootStore: searchStore}
searchStore.team = &SearchTeamStore{TeamStore: baseStore.Team(), rootStore: searchStore}
searchStore.user = &SearchUserStore{UserStore: baseStore.User(), rootStore: searchStore}
searchStore.fileInfo = &SearchFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: searchStore}
return searchStore
}
func (s *SearchStore) UpdateConfig(cfg *model.Config) {
s.configValue.Store(cfg)
}
func (s *SearchStore) getConfig() *model.Config {
return s.configValue.Load().(*model.Config)
}
func (s *SearchStore) Channel() store.ChannelStore {
return s.channel
}
func (s *SearchStore) Post() store.PostStore {
return s.post
}
func (s *SearchStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s *SearchStore) Team() store.TeamStore {
return s.team
}
func (s *SearchStore) User() store.UserStore {
return s.user
}
func (s *SearchStore) indexUserFromID(userId string) {
user, err := s.User().Get(context.Background(), userId)
if err != nil {
return
}
s.indexUser(user)
}
func (s *SearchStore) indexUser(user *model.User) {
for _, engine := range s.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
userTeams, nErr := s.Team().GetTeamsByUserId(user.Id)
if nErr != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(nErr))
return
}
userTeamsIds := []string{}
for _, team := range userTeams {
userTeamsIds = append(userTeamsIds, team.Id)
}
userChannelMembers, err := s.Channel().GetAllChannelMembersForUser(user.Id, false, true)
if err != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
userChannelsIds := []string{}
for channelId := range userChannelMembers {
userChannelsIds = append(userChannelsIds, channelId)
}
if err := engineCopy.IndexUser(user, userTeamsIds, userChannelsIds); err != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Indexed user in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
// Runs an indexing function synchronously or asynchronously depending on the engine
func runIndexFn(engine searchengine.SearchEngineInterface, indexFn func(searchengine.SearchEngineInterface)) {
if engine.IsIndexingSync() {
indexFn(engine)
if err := engine.RefreshIndexes(); err != nil {
mlog.Error("Encountered error refresh the indexes", mlog.Err(err))
}
} else {
go (func(engineCopy searchengine.SearchEngineInterface) {
indexFn(engineCopy)
})(engine)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchPostStore struct {
store.PostStore
rootStore *SearchStore
}
func (s SearchPostStore) indexPost(post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
channel, chanErr := s.rootStore.Channel().Get(post.ChannelId, true)
if chanErr != nil {
mlog.Error("Couldn't get channel for post for SearchEngine indexing.", mlog.String("channel_id", post.ChannelId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id), mlog.Err(chanErr))
return
}
if err := engineCopy.IndexPost(post, channel.TeamId); err != nil {
mlog.Warn("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deletePostIndex(post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePost(post); err != nil {
mlog.Warn("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deleteChannelPostsIndex(channelID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannelPosts(channelID); err != nil {
mlog.Warn("Encountered error deleting channel posts", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed all channel posts from the index in search engine", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) deleteUserPostsIndex(userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserPosts(userID); err != nil {
mlog.Warn("Encountered error deleting user posts", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed all user posts from the index in search engine", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) Update(newPost, oldPost *model.Post) (*model.Post, error) {
post, err := s.PostStore.Update(newPost, oldPost)
if err == nil {
s.indexPost(post)
}
return post, err
}
func (s *SearchPostStore) Overwrite(post *model.Post) (*model.Post, error) {
post, err := s.PostStore.Overwrite(post)
if err == nil {
s.indexPost(post)
}
return post, err
}
func (s SearchPostStore) Save(post *model.Post) (*model.Post, error) {
npost, err := s.PostStore.Save(post)
if err == nil {
s.indexPost(npost)
}
return npost, err
}
func (s SearchPostStore) Delete(postId string, date int64, deletedByID string) error {
err := s.PostStore.Delete(postId, date, deletedByID)
if err == nil {
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
postList, err2 := s.PostStore.Get(context.Background(), postId, opts, "", map[string]bool{})
if postList != nil && len(postList.Order) > 0 {
if err2 != nil {
s.deletePostIndex(postList.Posts[postList.Order[0]])
}
}
}
return err
}
func (s SearchPostStore) PermanentDeleteByUser(userID string) error {
err := s.PostStore.PermanentDeleteByUser(userID)
if err == nil {
s.deleteUserPostsIndex(userID)
}
return err
}
func (s SearchPostStore) PermanentDeleteByChannel(channelID string) error {
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err == nil {
s.deleteChannelPostsIndex(channelID)
}
return err
}
func (s SearchPostStore) searchPostsForUserByEngine(engine searchengine.SearchEngineInterface, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
// We only allow the user to search in channels they are a member of.
userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId,
&model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if err2 != nil {
return nil, errors.Wrap(err2, "error getting channel for user")
}
postIds, matches, err := engine.SearchPosts(userChannels, paramsList, page, perPage)
if err != nil {
return nil, err
}
// Get the posts
postList := model.NewPostList()
if len(postIds) > 0 {
posts, err := s.PostStore.GetPostsByIds(postIds)
if err != nil {
return nil, err
}
for _, p := range posts {
if p.DeleteAt == 0 {
postList.AddPost(p)
postList.AddOrder(p.Id)
}
}
}
return model.MakePostSearchResults(postList, matches), nil
}
func (s SearchPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
results, err := s.searchPostsForUserByEngine(engine, paramsList, userId, teamId, page, perPage)
if err != nil {
mlog.Warn("Encountered error on SearchPostsInTeamForUser.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
return results, err
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return &model.PostSearchResults{PostList: model.NewPostList(), Matches: model.PostSearchMatches{}}, nil
}
return s.PostStore.SearchPostsForUser(paramsList, userId, teamId, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
model "github.com/mattermost/mattermost-server/v6/model"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SearchTeamStore struct {
store.TeamStore
rootStore *SearchStore
}
func (s SearchTeamStore) SaveMember(teamMember *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
member, err := s.TeamStore.SaveMember(teamMember, maxUsersPerTeam)
if err == nil {
s.rootStore.indexUserFromID(member.UserId)
}
return member, err
}
func (s SearchTeamStore) UpdateMember(teamMember *model.TeamMember) (*model.TeamMember, error) {
member, err := s.TeamStore.UpdateMember(teamMember)
if err == nil {
s.rootStore.indexUserFromID(member.UserId)
}
return member, err
}
func (s SearchTeamStore) RemoveMember(teamId string, userId string) error {
err := s.TeamStore.RemoveMember(teamId, userId)
if err == nil {
s.rootStore.indexUserFromID(userId)
}
return err
}
func (s SearchTeamStore) RemoveAllMembersByUser(userId string) error {
err := s.TeamStore.RemoveAllMembersByUser(userId)
if err == nil {
s.rootStore.indexUserFromID(userId)
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchUserStore struct {
store.UserStore
rootStore *SearchStore
}
func (s *SearchUserStore) deleteUserIndex(user *model.User) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUser(user); err != nil {
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed user from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return []*model.User{}, nil
}
sanitizedTerm := sanitizeSearchTerm(term)
usersIds, err := engine.SearchUsersInTeam(teamId, listOfAllowedChannels, sanitizedTerm, options)
if err != nil {
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
users, nErr := s.UserStore.GetProfileByIds(context.Background(), usersIds, nil, false)
if nErr != nil {
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return users, nil
}
}
mlog.Debug("Using database search because no other search engine is available")
return s.UserStore.Search(teamId, term, options)
}
func (s *SearchUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
userUpdate, err := s.UserStore.Update(user, trustedUpdateData)
if err == nil {
s.rootStore.indexUser(userUpdate.New)
}
return userUpdate, err
}
func (s *SearchUserStore) Save(user *model.User) (*model.User, error) {
nuser, err := s.UserStore.Save(user)
if err == nil {
s.rootStore.indexUser(nuser)
}
return nuser, err
}
func (s *SearchUserStore) PermanentDelete(userId string) error {
user, userErr := s.UserStore.Get(context.Background(), userId)
if userErr != nil {
mlog.Warn("Encountered error deleting user", mlog.String("user_id", userId), mlog.Err(userErr))
}
err := s.UserStore.PermanentDelete(userId)
if err == nil && userErr == nil {
s.deleteUserIndex(user)
}
return err
}
func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine.SearchEngineInterface, teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
var err *model.AppError
uchanIds := []string{}
nuchanIds := []string{}
sanitizedTerm := sanitizeSearchTerm(term)
if channelId != "" && options.ListOfAllowedChannels != nil && !strings.Contains(strings.Join(options.ListOfAllowedChannels, "."), channelId) {
nuchanIds, err = engine.SearchUsersInTeam(teamId, options.ListOfAllowedChannels, sanitizedTerm, options)
} else {
uchanIds, nuchanIds, err = engine.SearchUsersInChannel(teamId, channelId, options.ListOfAllowedChannels, sanitizedTerm, options)
}
if err != nil {
return nil, err
}
uchan := make(chan store.StoreResult, 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(context.Background(), uchanIds, nil, false)
uchan <- store.StoreResult{Data: users, NErr: nErr}
close(uchan)
}()
nuchan := make(chan store.StoreResult, 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(context.Background(), nuchanIds, nil, false)
nuchan <- store.StoreResult{Data: users, NErr: nErr}
close(nuchan)
}()
autocomplete := &model.UserAutocompleteInChannel{}
result := <-uchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
inUsers := result.Data.([]*model.User)
autocomplete.InChannel = inUsers
result = <-nuchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
outUsers := result.Data.([]*model.User)
autocomplete.OutOfChannel = outUsers
return autocomplete, nil
}
// getListOfAllowedChannels return the list of allowed channels to search user based on the
//
// next scenarios:
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
// channels are allowed (nil return)
// - If we receive a team Id and either we don't have view restrictions or the provided team id is included in the
// list of restricted teams, then we return all the team channels
// - If we don't receive team id or the provided team id is not in the list of allowed teams to search of and we
// don't have channel restrictions then we return an empty result because we cannot get channels
// - If we receive channels restrictions we get:
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
// - If we have a team id then we only return those restricted channels that belongs to that team
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
var listOfAllowedChannels []string
if viewRestrictions == nil && teamId == "" {
// nil return without error means all channels are allowed
return nil, nil
}
if teamId != "" && (viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId)) {
channels, err := s.rootStore.Channel().GetTeamChannels(teamId)
if err != nil {
return nil, errors.Wrap(err, "failed to get team channels")
}
for _, channel := range channels {
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
}
if channelId != "" {
ch, err := s.rootStore.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
}
// Check if DM/GM channel, and add to the list.
// This is because GetTeamChannels does not return DM/GM channels.
// And since the channelId is passed from the API layer, it is already
// auth checked to confirm that the user has permission.
if ch.IsGroupOrDirect() {
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
}
}
return listOfAllowedChannels, nil
}
if len(viewRestrictions.Channels) > 0 {
channels, err := s.rootStore.Channel().GetChannelsByIds(viewRestrictions.Channels, false)
if err != nil {
return nil, errors.Wrap(err, "failed to get channels by ids")
}
for _, c := range channels {
if teamId == "" || (teamId != "" && c.TeamId == teamId) {
listOfAllowedChannels = append(listOfAllowedChannels, c.Id)
}
}
return listOfAllowedChannels, nil
}
return []string{}, nil
}
func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return &model.UserAutocompleteInChannel{}, nil
}
options.ListOfAllowedChannels = listOfAllowedChannels
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return autocomplete, nil
}
}
mlog.Debug("Using database search because no other search engine is available")
return s.UserStore.AutocompleteUsersInChannel(teamId, channelId, term, options)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"strings"
)
func sanitizeSearchTerm(term string) string {
return strings.TrimLeft(term, "@")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"database/sql/driver"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type jsonArray []string
func (a jsonArray) Value() (driver.Value, error) {
var out bytes.Buffer
if err := out.WriteByte('['); err != nil {
return nil, err
}
for i, item := range a {
if _, err := out.WriteString(strconv.Quote(item)); err != nil {
return nil, err
}
// Skip the last element.
if i < len(a)-1 {
if err := out.WriteByte(','); err != nil {
return nil, err
}
}
}
err := out.WriteByte(']')
return out.Bytes(), err
}
type jsonStringVal string
func (str jsonStringVal) Value() (driver.Value, error) {
return strconv.Quote(string(str)), nil
}
type jsonKeyPath string
func (str jsonKeyPath) Value() (driver.Value, error) {
return "{" + string(str) + "}", nil
}
type TraceOnAdapter struct{}
func (t *TraceOnAdapter) Printf(format string, v ...any) {
originalString := fmt.Sprintf(format, v...)
newString := strings.ReplaceAll(originalString, "\n", " ")
newString = strings.ReplaceAll(newString, "\t", " ")
newString = strings.ReplaceAll(newString, "\"", "")
mlog.Debug(newString)
}
type JSONSerializable interface {
ToJSON() string
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlAuditStore struct {
*SqlStore
}
func newSqlAuditStore(sqlStore *SqlStore) store.AuditStore {
return &SqlAuditStore{sqlStore}
}
func (s SqlAuditStore) Save(audit *model.Audit) error {
audit.Id = model.NewId()
audit.CreateAt = model.GetMillis()
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Audits
(Id, CreateAt, UserId, Action, ExtraInfo, IpAddress, SessionId)
VALUES
(:Id, :CreateAt, :UserId, :Action, :ExtraInfo, :IpAddress, :SessionId)`, audit); err != nil {
return errors.Wrapf(err, "failed to save Audit with userId=%s and action=%s", audit.UserId, audit.Action)
}
return nil
}
func (s SqlAuditStore) Get(userId string, offset int, limit int) (model.Audits, error) {
if limit > 1000 {
return nil, store.NewErrOutOfBounds(limit)
}
query := s.getQueryBuilder().
Select("*").
From("Audits").
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "audits_tosql")
}
var audits model.Audits
if err := s.GetReplicaX().Select(&audits, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Audit list for userId=%s", userId)
}
return audits, nil
}
func (s SqlAuditStore) PermanentDeleteByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Audits WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete Audit with userId=%s", userId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// bot is a subset of the model.Bot type, omitting the model.User fields.
type bot struct {
UserId string `json:"user_id"`
Description string `json:"description"`
OwnerId string `json:"owner_id"`
LastIconUpdate int64 `json:"last_icon_update"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
func botFromModel(b *model.Bot) *bot {
return &bot{
UserId: b.UserId,
Description: b.Description,
OwnerId: b.OwnerId,
LastIconUpdate: b.LastIconUpdate,
CreateAt: b.CreateAt,
UpdateAt: b.UpdateAt,
DeleteAt: b.DeleteAt,
}
}
// SqlBotStore is a store for managing bots in the database.
// Bots are otherwise normal users with extra metadata record in the Bots table. The primary key
// for a bot matches the primary key value for corresponding User record.
type SqlBotStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
// newSqlBotStore creates an instance of SqlBotStore, registering the table schema in question.
func newSqlBotStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.BotStore {
return &SqlBotStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
// Get fetches the given bot in the database.
func (us SqlBotStore) Get(botUserId string, includeDeleted bool) (*model.Bot, error) {
var excludeDeletedSql = "AND b.DeleteAt = 0"
if includeDeleted {
excludeDeletedSql = ""
}
query := `
SELECT
b.UserId,
u.Username,
u.FirstName AS DisplayName,
b.Description,
b.OwnerId,
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
b.CreateAt,
b.UpdateAt,
b.DeleteAt
FROM
Bots b
JOIN
Users u ON (u.Id = b.UserId)
WHERE
b.UserId = ?
` + excludeDeletedSql + `
`
var bot model.Bot
if err := us.GetReplicaX().Get(&bot, query, botUserId); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Bot", botUserId)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: user_id=%s", botUserId)
}
return &bot, nil
}
// GetAll fetches from all bots in the database.
func (us SqlBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
var conditions []string
var conditionsSql string
var additionalJoin string
var args []any
if !options.IncludeDeleted {
conditions = append(conditions, "b.DeleteAt = 0")
}
if options.OwnerId != "" {
conditions = append(conditions, "b.OwnerId = ?")
args = append(args, options.OwnerId)
}
if options.OnlyOrphaned {
additionalJoin = "JOIN Users o ON (o.Id = b.OwnerId)"
conditions = append(conditions, "o.DeleteAt != 0")
}
if len(conditions) > 0 {
conditionsSql = "WHERE " + strings.Join(conditions, " AND ")
}
sql := `
SELECT
b.UserId,
u.Username,
u.FirstName AS DisplayName,
b.Description,
b.OwnerId,
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
b.CreateAt,
b.UpdateAt,
b.DeleteAt
FROM
Bots b
JOIN
Users u ON (u.Id = b.UserId)
` + additionalJoin + `
` + conditionsSql + `
ORDER BY
b.CreateAt ASC,
u.Username ASC
LIMIT
?
OFFSET
?
`
// append limit, offset
args = append(args, options.PerPage, options.Page*options.PerPage)
bots := []*model.Bot{}
if err := us.GetReplicaX().Select(&bots, sql, args...); err != nil {
return nil, errors.Wrap(err, "error selecting all bots")
}
return bots, nil
}
// Save persists a new bot to the database.
// It assumes the corresponding user was saved via the user store.
func (us SqlBotStore) Save(bot *model.Bot) (*model.Bot, error) {
bot = bot.Clone()
bot.PreSave()
if err := bot.IsValid(); err != nil { // TODO: change to return error in v6.
return nil, err
}
if _, err := us.GetMasterX().NamedExec(`INSERT INTO Bots
(UserId, Description, OwnerId, LastIconUpdate, CreateAt, UpdateAt, DeleteAt)
VALUES
(:UserId, :Description, :OwnerId, :LastIconUpdate, :CreateAt, :UpdateAt, :DeleteAt)`, botFromModel(bot)); err != nil {
return nil, errors.Wrapf(err, "insert: user_id=%s", bot.UserId)
}
return bot, nil
}
// Update persists an updated bot to the database.
// It assumes the corresponding user was updated via the user store.
func (us SqlBotStore) Update(bot *model.Bot) (*model.Bot, error) {
bot = bot.Clone()
bot.PreUpdate()
if err := bot.IsValid(); err != nil { // TODO: needs to return error in v6
return nil, err
}
oldBot, err := us.Get(bot.UserId, true)
if err != nil {
return nil, err
}
oldBot.Description = bot.Description
oldBot.OwnerId = bot.OwnerId
oldBot.LastIconUpdate = bot.LastIconUpdate
oldBot.UpdateAt = bot.UpdateAt
oldBot.DeleteAt = bot.DeleteAt
bot = oldBot
res, err := us.GetMasterX().NamedExec(`UPDATE Bots
SET Description=:Description, OwnerId=:OwnerId, LastIconUpdate=:LastIconUpdate,
UpdateAt=:UpdateAt, DeleteAt=:DeleteAt
WHERE UserId=:UserId`, botFromModel(bot))
if err != nil {
return nil, errors.Wrapf(err, "update: user_id=%s", bot.UserId)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating bot: count=%d, userId=%s", count, bot.UserId)
}
return bot, nil
}
// PermanentDelete removes the bot from the database altogether.
// If the corresponding user is to be deleted, it must be done via the user store.
func (us SqlBotStore) PermanentDelete(botUserId string) error {
query := "DELETE FROM Bots WHERE UserId = ?"
if _, err := us.GetMasterX().Exec(query, botUserId); err != nil {
return store.NewErrInvalidInput("Bot", "UserId", botUserId).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlChannelMemberHistoryStore struct {
*SqlStore
}
func newSqlChannelMemberHistoryStore(sqlStore *SqlStore) store.ChannelMemberHistoryStore {
return &SqlChannelMemberHistoryStore{
SqlStore: sqlStore,
}
}
func (s SqlChannelMemberHistoryStore) LogJoinEvent(userId string, channelId string, joinTime int64) error {
channelMemberHistory := &model.ChannelMemberHistory{
UserId: userId,
ChannelId: channelId,
JoinTime: joinTime,
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO ChannelMemberHistory
(UserId, ChannelId, JoinTime)
VALUES
(:UserId, :ChannelId, :JoinTime)`, channelMemberHistory); err != nil {
return errors.Wrapf(err, "LogJoinEvent userId=%s channelId=%s joinTime=%d", userId, channelId, joinTime)
}
return nil
}
func (s SqlChannelMemberHistoryStore) LogLeaveEvent(userId string, channelId string, leaveTime int64) error {
query, params, err := s.getQueryBuilder().
Update("ChannelMemberHistory").
Set("LeaveTime", leaveTime).
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"ChannelId": channelId},
sq.Eq{"LeaveTime": nil},
}).ToSql()
if err != nil {
return errors.Wrap(err, "channel_member_history_to_sql")
}
sqlResult, err := s.GetMasterX().Exec(query, params...)
if err != nil {
return errors.Wrapf(err, "LogLeaveEvent userId=%s channelId=%s leaveTime=%d", userId, channelId, leaveTime)
}
if rows, err := sqlResult.RowsAffected(); err == nil && rows != 1 {
// there was no join event to update - this is best effort, so no need to raise an error
mlog.Warn("Channel join event for user and channel not found", mlog.String("user", userId), mlog.String("channel", channelId))
}
return nil
}
func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
useChannelMemberHistory, err := s.hasDataAtOrBefore(startTime)
if err != nil {
return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
if useChannelMemberHistory {
// the export period starts after the ChannelMemberHistory table was first introduced, so we can use the
// data from it for our export
channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelId)
if err2 != nil {
return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
return channelMemberHistories, nil
}
// the export period starts before the ChannelMemberHistory table was introduced, so we need to fake the
// data by assuming that anybody who has ever joined the channel in question was present during the export period.
// this may not always be true, but it's better than saying that somebody wasn't there when they were
channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelId)
if err != nil {
return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
return channelMemberHistories, nil
}
func (s SqlChannelMemberHistoryStore) hasDataAtOrBefore(time int64) (bool, error) {
type NullableCountResult struct {
Min sql.NullInt64
}
query, _, err := s.getQueryBuilder().Select("MIN(JoinTime) as Min").From("ChannelMemberHistory").ToSql()
if err != nil {
return false, errors.Wrap(err, "channel_member_history_to_sql")
}
var result NullableCountResult
if err := s.GetReplicaX().Get(&result, query); err != nil {
return false, err
} else if result.Min.Valid {
return result.Min.Int64 <= time, nil
} else {
// if the result was null, there are no rows in the table, so there is no data from before
return false, nil
}
}
func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`cmh.*, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
From("ChannelMemberHistory cmh").
Join("Users u ON cmh.UserId = u.Id").
LeftJoin("Bots ON Bots.UserId = u.Id").
Where(sq.And{
sq.Eq{"cmh.ChannelId": channelId},
sq.LtOrEq{"cmh.JoinTime": endTime},
sq.Or{
sq.Eq{"cmh.LeaveTime": nil},
sq.GtOrEq{"cmh.LeaveTime": startTime},
},
}).
OrderBy("cmh.JoinTime ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
histories := []*model.ChannelMemberHistoryResult{}
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
return nil, err
}
return histories, nil
}
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`ch.ChannelId, ch.UserId, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
Distinct().
From("ChannelMembers ch").
Join("Users u ON ch.UserId = u.id").
LeftJoin("Bots ON Bots.UserId = u.id").
Where(sq.Eq{"ch.ChannelId": channelId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
histories := []*model.ChannelMemberHistoryResult{}
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
return nil, err
}
// we have to fill in the join/leave times, because that data doesn't exist in the channel members table
for _, channelMemberHistory := range histories {
channelMemberHistory.JoinTime = startTime
channelMemberHistory.LeaveTime = model.NewInt64(endTime)
}
return histories, nil
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("ChannelMemberHistory.ChannelId, ChannelMemberHistory.UserId, ChannelMemberHistory.JoinTime").
From("ChannelMemberHistory")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "ChannelMemberHistory",
TimeColumn: "LeaveTime",
PrimaryKeys: []string{"ChannelId", "UserId", "JoinTime"},
ChannelIDTable: "ChannelMemberHistory",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes entries from ChannelMemberHistory when a corresponding channel no longer exists.
func (s SqlChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM ChannelMemberHistory WHERE (ChannelId, UserId, JoinTime) IN (
SELECT * FROM (
SELECT ChannelId, UserId, JoinTime FROM ChannelMemberHistory
LEFT JOIN Channels ON ChannelMemberHistory.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var (
query string
args []any
err error
)
if s.DriverName() == model.DatabaseDriverPostgres {
var innerSelect string
innerSelect, args, err = s.getQueryBuilder().
Select("ctid").
From("ChannelMemberHistory").
Where(sq.And{
sq.NotEq{"LeaveTime": nil},
sq.LtOrEq{"LeaveTime": endTime},
}).Limit(uint64(limit)).
ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_member_history_to_sql")
}
query, _, err = s.getQueryBuilder().
Delete("ChannelMemberHistory").
Where(fmt.Sprintf(
"ctid IN (%s)", innerSelect,
)).ToSql()
} else {
query, args, err = s.getQueryBuilder().
Delete("ChannelMemberHistory").
Where(sq.And{
sq.NotEq{"LeaveTime": nil},
sq.LtOrEq{"LeaveTime": endTime},
}).
Limit(uint64(limit)).ToSql()
}
if err != nil {
return 0, errors.Wrap(err, "channel_member_history_to_sql")
}
sqlResult, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
}
return rowsAffected, nil
}
// GetChannelsLeftSince returns list of channels that the user has left after a given time,
// but has not rejoined again.
func (s SqlChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
query, params, err := s.getQueryBuilder().
Select("ChannelId").
From("ChannelMemberHistory").
GroupBy("ChannelId").
Where(sq.Eq{"UserId": userID}).
Having("MAX(LeaveTime) > MAX(JoinTime) AND MAX(LeaveTime) IS NOT NULL AND MAX(LeaveTime) >= ?", since).ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
channelIds := []string{}
err = s.GetReplicaX().Select(&channelIds, query, params...)
if err != nil {
return nil, errors.Wrapf(err, "GetChannelsLeftSince userId=%s since=%d", userID, since)
}
return channelIds, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"sort"
"strconv"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
AllChannelMembersForUserCacheSize = model.SessionCacheSize
AllChannelMembersForUserCacheDuration = 15 * time.Minute // 15 mins
AllChannelMembersNotifyPropsForChannelCacheSize = model.SessionCacheSize
AllChannelMembersNotifyPropsForChannelCacheDuration = 30 * time.Minute // 30 mins
ChannelCacheDuration = 15 * time.Minute // 15 mins
)
type SqlChannelStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
// prepared query builders for use in multiple methods
channelMembersForTeamWithSchemeSelectQuery sq.SelectBuilder
}
type channelMember struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
UrgentMentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
MentionCountRoot int64
MsgCountRoot int64
}
func NewMapFromChannelMemberModel(cm *model.ChannelMember) map[string]any {
return map[string]any{
"ChannelId": cm.ChannelId,
"UserId": cm.UserId,
"Roles": cm.ExplicitRoles,
"LastViewedAt": cm.LastViewedAt,
"MsgCount": cm.MsgCount,
"MentionCount": cm.MentionCount,
"MentionCountRoot": cm.MentionCountRoot,
"UrgentMentionCount": cm.UrgentMentionCount,
"MsgCountRoot": cm.MsgCountRoot,
"NotifyProps": cm.NotifyProps,
"LastUpdateAt": cm.LastUpdateAt,
"SchemeGuest": sql.NullBool{Valid: true, Bool: cm.SchemeGuest},
"SchemeUser": sql.NullBool{Valid: true, Bool: cm.SchemeUser},
"SchemeAdmin": sql.NullBool{Valid: true, Bool: cm.SchemeAdmin},
}
}
type channelMemberWithSchemeRoles struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
MentionCountRoot int64
UrgentMentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
MsgCountRoot int64
}
type channelMemberWithTeamWithSchemeRoles struct {
channelMemberWithSchemeRoles
TeamDisplayName string
TeamName string
TeamUpdateAt int64
}
type channelMemberWithTeamWithSchemeRolesList []channelMemberWithTeamWithSchemeRoles
func channelMemberSliceColumns() []string {
return []string{"ChannelId", "UserId", "Roles", "LastViewedAt", "MsgCount", "MsgCountRoot", "MentionCount", "MentionCountRoot", "UrgentMentionCount", "NotifyProps", "LastUpdateAt", "SchemeUser", "SchemeAdmin", "SchemeGuest"}
}
func channelMemberToSlice(member *model.ChannelMember) []any {
resultSlice := []any{}
resultSlice = append(resultSlice, member.ChannelId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.LastViewedAt)
resultSlice = append(resultSlice, member.MsgCount)
resultSlice = append(resultSlice, member.MsgCountRoot)
resultSlice = append(resultSlice, member.MentionCount)
resultSlice = append(resultSlice, member.MentionCountRoot)
resultSlice = append(resultSlice, member.UrgentMentionCount)
resultSlice = append(resultSlice, model.MapToJSON(member.NotifyProps))
resultSlice = append(resultSlice, member.LastUpdateAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
return resultSlice
}
type channelMemberWithSchemeRolesList []channelMemberWithSchemeRoles
func getChannelRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole string,
roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.ChannelGuestRoleId:
result.schemeGuest = true
case model.ChannelUserRoleId:
result.schemeUser = true
case model.ChannelAdminRoleId:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultChannelGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelGuestRole)
} else if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelGuestRoleId)
}
}
if result.schemeUser {
if defaultChannelUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelUserRole)
} else if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelUserRoleId)
}
}
if result.schemeAdmin {
if defaultChannelAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelAdminRole)
} else if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
break
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db channelMemberWithSchemeRoles) ToModel() *model.ChannelMember {
// Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
// and exclude them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
defaultChannelGuestRole := ""
if db.ChannelSchemeDefaultGuestRole.Valid {
defaultChannelGuestRole = db.ChannelSchemeDefaultGuestRole.String
}
defaultChannelUserRole := ""
if db.ChannelSchemeDefaultUserRole.Valid {
defaultChannelUserRole = db.ChannelSchemeDefaultUserRole.String
}
defaultChannelAdminRole := ""
if db.ChannelSchemeDefaultAdminRole.Valid {
defaultChannelAdminRole = db.ChannelSchemeDefaultAdminRole.String
}
rolesResult := getChannelRoles(
schemeGuest, schemeUser, schemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(db.Roles),
)
return &model.ChannelMember{
ChannelId: db.ChannelId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
LastViewedAt: db.LastViewedAt,
MsgCount: db.MsgCount,
MsgCountRoot: db.MsgCountRoot,
MentionCount: db.MentionCount,
MentionCountRoot: db.MentionCountRoot,
UrgentMentionCount: db.UrgentMentionCount,
NotifyProps: db.NotifyProps,
LastUpdateAt: db.LastUpdateAt,
SchemeAdmin: rolesResult.schemeAdmin,
SchemeUser: rolesResult.schemeUser,
SchemeGuest: rolesResult.schemeGuest,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
}
}
// This is almost an entire copy of the above method with team information added.
func (db channelMemberWithTeamWithSchemeRoles) ToModel() *model.ChannelMemberWithTeamData {
// Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
// and exclude them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
defaultChannelGuestRole := ""
if db.ChannelSchemeDefaultGuestRole.Valid {
defaultChannelGuestRole = db.ChannelSchemeDefaultGuestRole.String
}
defaultChannelUserRole := ""
if db.ChannelSchemeDefaultUserRole.Valid {
defaultChannelUserRole = db.ChannelSchemeDefaultUserRole.String
}
defaultChannelAdminRole := ""
if db.ChannelSchemeDefaultAdminRole.Valid {
defaultChannelAdminRole = db.ChannelSchemeDefaultAdminRole.String
}
rolesResult := getChannelRoles(
schemeGuest, schemeUser, schemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(db.Roles),
)
return &model.ChannelMemberWithTeamData{
ChannelMember: model.ChannelMember{
ChannelId: db.ChannelId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
LastViewedAt: db.LastViewedAt,
MsgCount: db.MsgCount,
MsgCountRoot: db.MsgCountRoot,
MentionCount: db.MentionCount,
MentionCountRoot: db.MentionCountRoot,
UrgentMentionCount: db.UrgentMentionCount,
NotifyProps: db.NotifyProps,
LastUpdateAt: db.LastUpdateAt,
SchemeAdmin: rolesResult.schemeAdmin,
SchemeUser: rolesResult.schemeUser,
SchemeGuest: rolesResult.schemeGuest,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
},
TeamName: db.TeamName,
TeamDisplayName: db.TeamDisplayName,
TeamUpdateAt: db.TeamUpdateAt,
}
}
func (db channelMemberWithSchemeRolesList) ToModel() model.ChannelMembers {
cms := model.ChannelMembers{}
for _, cm := range db {
cms = append(cms, *cm.ToModel())
}
return cms
}
func (db channelMemberWithTeamWithSchemeRolesList) ToModel() model.ChannelMembersWithTeamData {
cms := model.ChannelMembersWithTeamData{}
for _, cm := range db {
cms = append(cms, *cm.ToModel())
}
return cms
}
type allChannelMember struct {
ChannelId string
Roles string
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
}
type allChannelMembers []allChannelMember
func (db allChannelMember) Process() (string, string) {
roles := strings.Fields(db.Roles)
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if db.SchemeGuest.Valid && db.SchemeGuest.Bool {
if db.ChannelSchemeDefaultGuestRole.Valid && db.ChannelSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultGuestRole.String)
} else if db.TeamSchemeDefaultGuestRole.Valid && db.TeamSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultGuestRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelGuestRoleId)
}
}
if db.SchemeUser.Valid && db.SchemeUser.Bool {
if db.ChannelSchemeDefaultUserRole.Valid && db.ChannelSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultUserRole.String)
} else if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelUserRoleId)
}
}
if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
if db.ChannelSchemeDefaultAdminRole.Valid && db.ChannelSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultAdminRole.String)
} else if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range roles {
if role == impliedRole {
alreadyThere = true
break
}
}
if !alreadyThere {
roles = append(roles, impliedRole)
}
}
return db.ChannelId, strings.Join(roles, " ")
}
func (db allChannelMembers) ToMapStringString() map[string]string {
result := make(map[string]string)
for _, item := range db {
key, value := item.Process()
result[key] = value
}
return result
}
// publicChannel is a subset of the metadata corresponding to public channels only.
type publicChannel struct {
Id string `json:"id"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
}
var allChannelMembersForUserCache = cache.NewLRU(cache.LRUOptions{
Size: AllChannelMembersForUserCacheSize,
})
var allChannelMembersNotifyPropsForChannelCache = cache.NewLRU(cache.LRUOptions{
Size: AllChannelMembersNotifyPropsForChannelCacheSize,
})
var channelByNameCache = cache.NewLRU(cache.LRUOptions{
Size: model.ChannelCacheSize,
})
func (s SqlChannelStore) ClearMembersForUserCache() {
allChannelMembersForUserCache.Purge()
}
func (s SqlChannelStore) ClearCaches() {
allChannelMembersForUserCache.Purge()
allChannelMembersNotifyPropsForChannelCache.Purge()
channelByNameCache.Purge()
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("Channel By Name - Purge")
}
}
func newSqlChannelStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.ChannelStore {
s := &SqlChannelStore{
SqlStore: sqlStore,
metrics: metrics,
}
s.initializeQueries()
return s
}
func (s *SqlChannelStore) initializeQueries() {
s.channelMembersForTeamWithSchemeSelectQuery = s.getQueryBuilder().
Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.Roles",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"ChannelMembers.SchemeGuest",
"TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole",
"ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole",
"ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole",
"ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole",
).
From("ChannelMembers").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id")
}
func (s SqlChannelStore) upsertPublicChannelT(transaction *sqlxTxWrapper, channel *model.Channel) error {
publicChannel := &publicChannel{
Id: channel.Id,
DeleteAt: channel.DeleteAt,
TeamId: channel.TeamId,
DisplayName: channel.DisplayName,
Name: channel.Name,
Header: channel.Header,
Purpose: channel.Purpose,
}
if channel.Type != model.ChannelTypeOpen {
if _, err := transaction.Exec(`DELETE FROM PublicChannels WHERE Id=?`, publicChannel.Id); err != nil {
return errors.Wrap(err, "failed to delete public channel")
}
return nil
}
vals := map[string]any{
"id": publicChannel.Id,
"deleteat": publicChannel.DeleteAt,
"teamid": publicChannel.TeamId,
"displayname": publicChannel.DisplayName,
"name": publicChannel.Name,
"header": publicChannel.Header,
"purpose": publicChannel.Purpose,
}
var err error
if s.DriverName() == model.DatabaseDriverMysql {
_, err = transaction.NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose)
`, vals)
if err != nil && IsUniqueConstraintError(err, []string{"PRIMARY"}) {
_, err = transaction.NamedExec(`UPDATE PublicChannels
SET deleteAt = :deleteat,
TeamId = :teamid,
DisplayName = :displayname,
Name = :name,
Header = :header,
Purpose = :purpose
WHERE Id=:id`, vals)
}
} else {
_, err = transaction.NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose)
ON CONFLICT (id) DO UPDATE
SET DeleteAt = :deleteat,
TeamId = :teamid,
DisplayName = :displayname,
Name = :name,
Header = :header,
Purpose = :purpose;
`, vals)
}
if err != nil {
return errors.Wrap(err, "failed to insert public channel")
}
return nil
}
// Save writes the (non-direct) channel to the database.
func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (_ *model.Channel, err error) {
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if channel.Type == model.ChannelTypeDirect {
return nil, store.NewErrInvalidInput("Channel", "Type", channel.Type)
}
var newChannel *model.Channel
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
newChannel, err = s.saveChannelT(transaction, channel, maxChannelsPerTeam)
if err != nil {
return newChannel, err
}
// Additionally propagate the write to the PublicChannels table.
if err = s.upsertPublicChannelT(transaction, newChannel); err != nil {
return nil, errors.Wrap(err, "upsert_public_channel")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
// There are cases when in case of conflict, the original channel value is returned.
// So we return both and let the caller do the checks.
return newChannel, err
}
func (s SqlChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
channel := new(model.Channel)
for _, option := range channelOptions {
option(channel)
}
channel.DisplayName = ""
channel.Name = model.GetDMNameFromIds(otherUser.Id, user.Id)
channel.Header = ""
channel.Type = model.ChannelTypeDirect
channel.Shared = model.NewBool(user.IsRemote() || otherUser.IsRemote())
channel.CreatorId = user.Id
cm1 := &model.ChannelMember{
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
cm2 := &model.ChannelMember{
UserId: otherUser.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: otherUser.IsGuest(),
SchemeUser: !otherUser.IsGuest(),
}
return s.SaveDirectChannel(channel, cm1, cm2)
}
func (s SqlChannelStore) SaveDirectChannel(directChannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (_ *model.Channel, err error) {
if directChannel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", directChannel.DeleteAt)
}
if directChannel.Type != model.ChannelTypeDirect {
return nil, store.NewErrInvalidInput("Channel", "Type", directChannel.Type)
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
directChannel.TeamId = ""
newChannel, err := s.saveChannelT(transaction, directChannel, 0)
if err != nil {
return newChannel, err
}
// Members need new channel ID
member1.ChannelId = newChannel.Id
member2.ChannelId = newChannel.Id
if member1.UserId != member2.UserId {
_, err = s.saveMultipleMembers([]*model.ChannelMember{member1, member2})
} else {
_, err = s.saveMemberT(member2)
}
if err != nil {
return nil, err
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return newChannel, nil
}
func (s SqlChannelStore) saveChannelT(transaction *sqlxTxWrapper, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
if channel.Id != "" && !channel.IsShared() {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
channel.PreSave()
if err := channel.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err // we just pass through the error as-is for now.
}
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup && maxChannelsPerTeam >= 0 {
var count int64
if err := transaction.Get(&count, "SELECT COUNT(0) FROM Channels WHERE TeamId = ? AND DeleteAt = 0 AND (Type = ? OR Type = ?)", channel.TeamId, model.ChannelTypeOpen, model.ChannelTypePrivate); err != nil {
return nil, errors.Wrapf(err, "save_channel_count: teamId=%s", channel.TeamId)
} else if count >= maxChannelsPerTeam {
return nil, store.NewErrLimitExceeded("channels_per_team", int(count), "teamId="+channel.TeamId)
}
}
if _, err := transaction.NamedExec(`INSERT INTO Channels
(Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, DisplayName, Name, Header, Purpose, LastPostAt, TotalMsgCount, ExtraUpdateAt, CreatorId, SchemeId, GroupConstrained, Shared, TotalMsgCountRoot, LastRootPostAt)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :TeamId, :Type, :DisplayName, :Name, :Header, :Purpose, :LastPostAt, :TotalMsgCount, :ExtraUpdateAt, :CreatorId, :SchemeId, :GroupConstrained, :Shared, :TotalMsgCountRoot, :LastRootPostAt)`, channel); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
if serr := s.GetMasterX().Get(&dupChannel, "SELECT * FROM Channels WHERE TeamId = ? AND Name = ?", channel.TeamId, channel.Name); serr != nil {
return nil, errors.Wrapf(serr, "error while retrieving existing channel %s", channel.Name) // do not return this as a *store.ErrConflict as it would be treated as a recoverable error
}
return &dupChannel, store.NewErrConflict("Channel", err, "id="+channel.Id)
}
return nil, errors.Wrapf(err, "save_channel: id=%s", channel.Id)
}
return channel, nil
}
// Update writes the updated channel to the database.
func (s SqlChannelStore) Update(channel *model.Channel) (_ *model.Channel, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedChannel, err := s.updateChannelT(transaction, channel)
if err != nil {
return nil, err
}
// Additionally propagate the write to the PublicChannels table.
if err := s.upsertPublicChannelT(transaction, updatedChannel); err != nil {
return nil, errors.Wrap(err, "upsertPublicChannelT: failed to upsert channel")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedChannel, nil
}
func (s SqlChannelStore) updateChannelT(transaction *sqlxTxWrapper, channel *model.Channel) (*model.Channel, error) {
channel.PreUpdate()
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if err := channel.IsValid(); err != nil {
return nil, err
}
res, err := transaction.NamedExec(`UPDATE Channels
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
DeleteAt=:DeleteAt,
TeamId=:TeamId,
Type=:Type,
DisplayName=:DisplayName,
Name=:Name,
Header=:Header,
Purpose=:Purpose,
LastPostAt=:LastPostAt,
TotalMsgCount=:TotalMsgCount,
ExtraUpdateAt=:ExtraUpdateAt,
CreatorId=:CreatorId,
SchemeId=:SchemeId,
GroupConstrained=:GroupConstrained,
Shared=:Shared,
TotalMsgCountRoot=:TotalMsgCountRoot,
LastRootPostAt=:LastRootPostAt
WHERE Id=:Id`, channel)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
s.GetReplicaX().Get(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name= :Name AND DeleteAt > 0", map[string]any{"TeamId": channel.TeamId, "Name": channel.Name})
if dupChannel.DeleteAt > 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, errors.Wrapf(err, "failed to update channel with id=%s", channel.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rowsAffected in updateChannelT")
}
if count > 1 {
return nil, fmt.Errorf("the expected number of channels to be updated is <=1 but was %d", count)
}
return channel, nil
}
func (s SqlChannelStore) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, error) {
var unreadChannel model.ChannelUnread
err := s.GetReplicaX().Get(&unreadChannel,
`SELECT
Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, (Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot, ChannelMembers.MentionCount MentionCount, ChannelMembers.MentionCountRoot MentionCountRoot, COALESCE(ChannelMembers.UrgentMentionCount, 0) UrgentMentionCount, ChannelMembers.NotifyProps NotifyProps
FROM
Channels, ChannelMembers
WHERE
Id = ChannelId
AND Id = ?
AND UserId = ?
AND DeleteAt = 0`,
channelId, userId)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("channelId=%s,userId=%s", channelId, userId))
}
return nil, errors.Wrapf(err, "failed to get Channel with channelId=%s and userId=%s", channelId, userId)
}
return &unreadChannel, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateChannel(id string) {
}
func (s SqlChannelStore) InvalidateChannelByName(teamId, name string) {
channelByNameCache.Remove(teamId + name)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("Channel by Name - Remove by TeamId and Name")
}
}
func (s SqlChannelStore) GetPinnedPosts(channelId string) (*model.PostList, error) {
pl := model.NewPostList()
posts := []*model.Post{}
if err := s.GetReplicaX().Select(&posts, "SELECT *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE IsPinned = true AND ChannelId = ? AND DeleteAt = 0 ORDER BY CreateAt ASC", channelId); err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
for _, post := range posts {
pl.AddPost(post)
pl.AddOrder(post.Id)
}
return pl, nil
}
//nolint:unparam
func (s SqlChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
ch := model.Channel{}
err := s.GetReplicaX().Get(&ch, `SELECT * FROM Channels WHERE Id=?`, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", id)
}
return nil, errors.Wrapf(err, "failed to find channel with id = %s", id)
}
return &ch, nil
}
//nolint:unparam
func (s SqlChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Id": ids})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getmany_tosql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with ids %v", ids)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("ids=%v", ids))
}
return channels, nil
}
// Delete records the given deleted timestamp to the channel in question.
func (s SqlChannelStore) Delete(channelId string, time int64) error {
return s.SetDeleteAt(channelId, time, time)
}
// Restore reverts a previous deleted timestamp from the channel in question.
func (s SqlChannelStore) Restore(channelId string, time int64) error {
return s.SetDeleteAt(channelId, 0, time)
}
// SetDeleteAt records the given deleted and updated timestamp to the channel in question.
func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) (err error) {
defer s.InvalidateChannel(channelId)
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "SetDeleteAt: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = s.setDeleteAtT(transaction, channelId, deleteAt, updateAt)
if err != nil {
return errors.Wrap(err, "setDeleteAtT")
}
// Additionally propagate the write to the PublicChannels table.
if _, err := transaction.Exec(`
UPDATE
PublicChannels
SET
DeleteAt = ?
WHERE
Id = ?
`, deleteAt, channelId); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrapf(err, "SetDeleteAt: commit_transaction")
}
return nil
}
func (s SqlChannelStore) setDeleteAtT(transaction *sqlxTxWrapper, channelId string, deleteAt, updateAt int64) error {
_, err := transaction.Exec(`UPDATE Channels
SET DeleteAt = ?,
UpdateAt = ?
WHERE Id = ?`, deleteAt, updateAt, channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
// PermanentDeleteByTeam removes all channels for the given team from the database.
func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.permanentDeleteByTeamtT(transaction, teamId); err != nil {
return errors.Wrap(err, "permanentDeleteByTeamtT")
}
// Additionally propagate the deletions to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
TeamId = ?
`, teamId); err != nil {
return errors.Wrapf(err, "failed to delete public channels by team with teamId=%s", teamId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteByTeamtT(transaction *sqlxTxWrapper, teamId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE TeamId = ?", teamId); err != nil {
return errors.Wrapf(err, "failed to delete channel by team with teamId=%s", teamId)
}
return nil
}
// PermanentDelete removes the given channel from the database.
func (s SqlChannelStore) PermanentDelete(channelId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "PermanentDelete: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.permanentDeleteT(transaction, channelId); err != nil {
return errors.Wrap(err, "permanentDeleteT")
}
// Additionally propagate the deletion to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
Id = ?
`, channelId); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDelete: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteT(transaction *sqlxTxWrapper, channelId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE Id = ?", channelId); err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM ChannelMembers WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete Channel with channelId=%s", channelId)
}
return nil
}
func (s SqlChannelStore) GetChannels(teamId string, userId string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("ch.*").
From("Channels ch, ChannelMembers cm").
Where(
sq.And{
sq.Expr("ch.Id = cm.ChannelId"),
sq.Eq{"cm.UserId": userId},
},
).
OrderBy("ch.DisplayName")
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"ch.TeamId": teamId},
sq.Eq{"ch.TeamId": ""},
})
}
if opts.IncludeDeleted {
if opts.LastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"ch.DeleteAt": 0},
sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt},
})
}
// If opts.LastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"ch.DeleteAt": 0})
}
if opts.LastUpdateAt > 0 {
query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt})
}
channels := model.ChannelList{}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", "userId="+userId)
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("ch.*").
From("Channels ch, ChannelMembers cm").
Where(
sq.And{
sq.Expr("ch.Id = cm.ChannelId"),
sq.Eq{"cm.UserId": userId},
},
).
OrderBy("ch.Id")
if opts.PerPage != nil {
// The limit is verified at the GraphQL layer.
query = query.Limit(uint64(*opts.PerPage))
}
if afterChannelID != "" {
query = query.Where(sq.Gt{"ch.Id": afterChannelID})
}
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"ch.TeamId": teamId},
sq.Eq{"ch.TeamId": ""},
})
}
if opts.IncludeDeleted {
if opts.LastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"ch.DeleteAt": 0},
sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt},
})
}
// If opts.LastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"ch.DeleteAt": 0})
}
if opts.LastUpdateAt > 0 {
query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt})
}
channels := model.ChannelList{}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsByUser(userId string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels, ChannelMembers").
Where(
sq.And{
sq.Expr("Id = ChannelId"),
sq.Eq{"UserId": userId},
},
).
OrderBy("Id ASC")
if fromChannelID != "" {
query = query.Where(sq.Gt{"Id": fromChannelID})
}
if pageSize != -1 {
query = query.Limit(uint64(pageSize))
}
if includeDeleted {
if lastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"DeleteAt": 0},
sq.GtOrEq{"DeleteAt": lastDeleteAt},
})
}
// If lastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with UserId=%s", userId)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", "userId="+userId)
}
return channels, nil
}
func (s SqlChannelStore) GetAllChannelMembersById(channelID string) ([]string, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.Where(sq.Eq{
"ChannelId": channelID,
}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetAllChannelMembersById_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMembers with channelID=%s", channelID)
}
res := make([]string, len(dbMembers))
for i, member := range dbMembers.ToModel() {
res[i] = member.UserId
}
return res, nil
}
func (s SqlChannelStore) GetAllChannels(offset, limit int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
query := s.getAllChannelsQuery(opts, false)
query = query.
OrderBy("c.DisplayName, Teams.DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to create query")
}
data := model.ChannelListWithTeamData{}
err = s.GetReplicaX().Select(&data, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get all channels")
}
return data, nil
}
func (s SqlChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
query := s.getAllChannelsQuery(opts, true)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "failed to create query")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count all channels")
}
return count, nil
}
func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCount bool) sq.SelectBuilder {
var selectStr string
if forCount {
selectStr = "count(c.Id)"
} else {
selectStr = "c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt"
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId AS PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Where(sq.Eq{"c.Type": []model.ChannelType{model.ChannelTypePrivate, model.ChannelTypeOpen}})
if !forCount {
query = query.Join("Teams ON Teams.Id = c.TeamId")
}
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.NotAssociatedToGroup != "" {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if opts.ExcludePolicyConstrained || opts.IncludePolicyID {
query = query.LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
if opts.ExcludePolicyConstrained {
query = query.Where("RetentionPoliciesChannels.ChannelId IS NULL")
}
return query
}
func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = ?
AND c.DeleteAt = 0
AND c.Id NOT IN (
SELECT
c.Id
FROM
PublicChannels c
JOIN
ChannelMembers cm ON (cm.ChannelId = c.Id)
WHERE
c.TeamId = ?
AND cm.UserId = ?
AND c.DeleteAt = 0
)
ORDER BY
c.DisplayName
LIMIT ?
OFFSET ?
`, teamId, teamId, userId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed getting channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlChannelStore) GetPrivateChannelsForTeam(teamId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
builder := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Type": model.ChannelTypePrivate, "TeamId": teamId, "DeleteAt": 0}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channels_tosql")
}
err = s.GetReplicaX().Select(&channels, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find channel with teamId=%s", teamId)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels pc ON (pc.Id = Channels.Id)
WHERE
pc.TeamId = ?
AND pc.DeleteAt = 0
ORDER BY pc.DisplayName
LIMIT ?
OFFSET ?
`, teamId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find channel with teamId=%s", teamId)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) (model.ChannelList, error) {
props := make(map[string]any)
props["teamId"] = teamId
idQuery := ""
for index, channelId := range channelIds {
if idQuery != "" {
idQuery += ", "
}
props["channelId"+strconv.Itoa(index)] = channelId
idQuery += ":channelId" + strconv.Itoa(index)
}
var data model.ChannelList
builder := s.getQueryBuilder().
Select("Channels.*").
From("Channels").
Join("PublicChannels pc ON (pc.Id = Channels.Id)").
Where(sq.And{
sq.Eq{"pc.TeamId": teamId},
sq.Eq{"pc.DeleteAt": 0},
sq.Eq{"pc.Id": channelIds},
}).
OrderBy("pc.DisplayName")
queryString, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetPublicChannelsByIdsForTeam to_sql")
}
err = s.GetReplicaX().Select(&data, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
if len(data) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("teamId=%s, channelIds=%v", teamId, channelIds))
}
return data, nil
}
func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, error) {
data := []struct {
Id string
TotalMsgCount int64
TotalMsgCountRoot int64
UpdateAt int64
}{}
err := s.GetReplicaX().Select(&data, `SELECT Id, TotalMsgCount, TotalMsgCountRoot, UpdateAt
FROM Channels
WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = ?)
AND (TeamId = ? OR TeamId = '')
AND DeleteAt = 0
ORDER BY DisplayName`, userId, teamId)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels count with teamId=%s and userId=%s", teamId, userId)
}
counts := &model.ChannelCounts{
Counts: make(map[string]int64),
CountsRoot: make(map[string]int64),
UpdateTimes: make(map[string]int64),
}
for i := range data {
v := data[i]
counts.Counts[v.Id] = v.TotalMsgCount
counts.CountsRoot[v.Id] = v.TotalMsgCountRoot
counts.UpdateTimes[v.Id] = v.UpdateAt
}
return counts, nil
}
func (s SqlChannelStore) GetTeamChannels(teamId string) (model.ChannelList, error) {
data := model.ChannelList{}
err := s.GetReplicaX().Select(&data, "SELECT * FROM Channels WHERE TeamId = ? And Type != ? ORDER BY DisplayName", teamId, model.ChannelTypeDirect)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s", teamId)
}
if len(data) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("teamId=%s", teamId))
}
return data, nil
}
func (s SqlChannelStore) GetByName(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, false, allowFromCache)
}
func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) {
var channels []*model.Channel
if allowFromCache {
var misses []string
visited := make(map[string]struct{})
for _, name := range names {
if _, ok := visited[name]; ok {
continue
}
visited[name] = struct{}{}
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
channels = append(channels, cacheItem)
} else {
misses = append(misses, name)
}
}
names = misses
}
if len(names) > 0 {
builder := s.getQueryBuilder().
Select("*").
From("Channels").
Where(
sq.And{
sq.Eq{"Name": names},
sq.Eq{"DeleteAt": 0},
},
)
if teamId != "" {
builder = builder.Where(sq.Eq{"TeamId": teamId})
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetByNames_tosql")
}
dbChannels := []*model.Channel{}
if err := s.GetReplicaX().Select(&dbChannels, query, args...); err != nil && err != sql.ErrNoRows {
msg := fmt.Sprintf("failed to get channels with names=%v", names)
if teamId != "" {
msg += fmt.Sprintf(" teamId=%s", teamId)
}
return nil, errors.Wrap(err, msg)
}
for _, channel := range dbChannels {
channelByNameCache.SetWithExpiry(teamId+channel.Name, channel, ChannelCacheDuration)
channels = append(channels, channel)
}
// Not all channels are in cache. Increment aggregate miss counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name - Aggregate")
}
} else {
// All of the channel names are in cache. Increment aggregate hit counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name - Aggregate")
}
}
return channels, nil
}
func (s SqlChannelStore) GetByNameIncludeDeleted(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, true, allowFromCache)
}
func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Name": name})
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"TeamId": teamId},
sq.Eq{"TeamId": ""},
})
}
channel := model.Channel{}
if allowFromCache {
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name")
}
return cacheItem, nil
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name")
}
}
queryStr, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getByName_tosql")
}
if err = s.GetReplicaX().Get(&channel, queryStr, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s&Name=%s", teamId, name))
}
return nil, errors.Wrapf(err, "failed to find channel with TeamId=%s and Name=%s", teamId, name)
}
err = channelByNameCache.SetWithExpiry(teamId+name, &channel, ChannelCacheDuration)
return &channel, err
}
func (s SqlChannelStore) GetDeletedByName(teamId string, name string) (*model.Channel, error) {
channel := model.Channel{}
if err := s.GetReplicaX().Get(&channel, `SELECT *
FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND Name = ?
AND DeleteAt != 0`, teamId, name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get channel by teamId=%s and name=%s", teamId, name)
}
return &channel, nil
}
func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId string) (model.ChannelList, error) {
channels := model.ChannelList{}
query := `
SELECT * FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND DeleteAt != 0
AND Type != ?
UNION
SELECT * FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND DeleteAt != 0
AND Type = ?
AND Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = ?)
ORDER BY DisplayName LIMIT ? OFFSET ?
`
if err := s.GetReplicaX().Select(&channels, query, teamId, model.ChannelTypePrivate, teamId, model.ChannelTypePrivate, userId, limit, offset); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s,UserId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to get deleted channels with TeamId=%s and UserId=%s", teamId, userId)
}
return channels, nil
}
var channelMembersWithSchemeSelectQuery = `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
ChannelMembers.SchemeGuest,
COALESCE(Teams.DisplayName, '') TeamDisplayName,
COALESCE(Teams.Name, '') TeamName,
COALESCE(Teams.UpdateAt, 0) TeamUpdateAt,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
`
func (s SqlChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
for _, member := range members {
defer s.InvalidateAllChannelMembersForUser(member.UserId)
}
newMembers, err := s.saveMultipleMembers(members)
if err != nil {
return nil, err
}
return newMembers, nil
}
func (s SqlChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
newMembers, err := s.SaveMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return newMembers[0], nil
}
func (s SqlChannelStore) saveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
newChannelMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
if val, ok := newChannelMembers[member.ChannelId]; val < 1 || !ok {
newChannelMembers[member.ChannelId] = 1
} else {
newChannelMembers[member.ChannelId]++
}
users[member.UserId] = true
member.PreSave()
if err := member.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err
}
}
channels := []string{}
for channel := range newChannelMembers {
channels = append(channels, channel)
}
defaultChannelRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
channelRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"ChannelScheme.DefaultChannelGuestRole as Guest",
"ChannelScheme.DefaultChannelUserRole as User",
"ChannelScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
channelRolesSql, channelRolesArgs, err := channelRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_roles_tosql")
}
defaultChannelsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultChannelsRoles, channelRolesSql, channelRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_channel_roles_select")
}
for _, defaultRoles := range defaultChannelsRoles {
defaultChannelRolesByChannel[defaultRoles.Id] = defaultRoles
}
defaultTeamRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
teamRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"TeamScheme.DefaultChannelGuestRole as Guest",
"TeamScheme.DefaultChannelUserRole as User",
"TeamScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Teams ON Teams.Id = Channels.TeamId").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
teamRolesSql, teamRolesArgs, err := teamRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, teamRolesSql, teamRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByChannel[defaultRoles.Id] = defaultRoles
}
query := s.getQueryBuilder().Insert("ChannelMembers").Columns(channelMemberSliceColumns()...)
for _, member := range members {
query = query.Values(channelMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"ChannelId", "channelmembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("ChannelMembers", err, "")
}
return nil, errors.Wrap(err, "channel_members_save")
}
newMembers := []*model.ChannelMember{}
for _, member := range members {
defaultTeamGuestRole := defaultTeamRolesByChannel[member.ChannelId].Guest.String
defaultTeamUserRole := defaultTeamRolesByChannel[member.ChannelId].User.String
defaultTeamAdminRole := defaultTeamRolesByChannel[member.ChannelId].Admin.String
defaultChannelGuestRole := defaultChannelRolesByChannel[member.ChannelId].Guest.String
defaultChannelUserRole := defaultChannelRolesByChannel[member.ChannelId].User.String
defaultChannelAdminRole := defaultChannelRolesByChannel[member.ChannelId].Admin.String
rolesResult := getChannelRoles(
member.SchemeGuest, member.SchemeUser, member.SchemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(member.ExplicitRoles),
)
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlChannelStore) saveMemberT(member *model.ChannelMember) (*model.ChannelMember, error) {
members, err := s.saveMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) (_ []*model.ChannelMember, err error) {
for _, member := range members {
member.PreUpdate()
if err := member.IsValid(); err != nil {
return nil, err
}
}
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedMembers := []*model.ChannelMember{}
for _, member := range members {
update := s.getQueryBuilder().
Update("ChannelMembers").
SetMap(NewMapFromChannelMemberModel(member)).
Where(sq.Eq{
"ChannelId": member.ChannelId,
"UserId": member.UserId,
})
sqlUpdate, args, err := update.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMultipleMembers_Update_ToSql ChannelID=%s UserID=%s", member.ChannelId, member.UserId)
}
if _, err = transaction.Exec(sqlUpdate, args...); err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMember")
}
sqlSelect, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": member.ChannelId,
"ChannelMembers.UserId": member.UserId,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMultipleMembers_Select_ToSql ChannelID=%s UserID=%s", member.ChannelId, member.UserId)
}
// TODO: Get this out of the transaction when is possible
var dbMember channelMemberWithSchemeRoles
if err := transaction.Get(&dbMember, sqlSelect, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", member.ChannelId, member.UserId))
}
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s and userId=%s", member.ChannelId, member.UserId)
}
updatedMembers = append(updatedMembers, dbMember.ToModel())
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedMembers, nil
}
func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
updatedMembers, err := s.UpdateMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return updatedMembers[0], nil
}
func (s SqlChannelStore) UpdateMemberNotifyProps(channelID, userID string, props map[string]string) (_ *model.ChannelMember, err error) {
tx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
if s.DriverName() == model.DatabaseDriverPostgres {
sql, args, err2 := s.getQueryBuilder().
Update("channelmembers").
Set("notifyprops", sq.Expr("notifyprops || ?::jsonb", model.MapToJSON(props))).
Where(sq.Eq{
"userid": userID,
"channelid": channelID,
}).ToSql()
if err2 != nil {
return nil, errors.Wrapf(err2, "UpdateMemberNotifyProps_Update_Postgres_ToSql channelID=%s and userID=%s", channelID, userID)
}
_, err = tx.Exec(sql, args...)
} else if len(props) > 0 {
// It's difficult to construct a SQL query for MySQL
// to handle a case of empty map. So we just ignore it.
// unpack the keys and values to pass to MySQL.
jsonArgs, jsonSQL := constructMySQLJSONArgs(props)
jsonExpr := sq.Expr(fmt.Sprintf("JSON_SET(NotifyProps, %s)", jsonSQL), jsonArgs...)
// Example: UPDATE ChannelMembers
// SET NotifyProps = JSON_SET(NotifyProps, '$.mark_unread', '"yes"' [, ...])
// WHERE ...
sql, args, err2 := s.getQueryBuilder().
Update("ChannelMembers").
Set("NotifyProps", jsonExpr).
Where(sq.Eq{
"UserId": userID,
"ChannelId": channelID,
}).ToSql()
if err2 != nil {
return nil, errors.Wrapf(err2, "UpdateMemberNotifyProps_Update_MySQL_ToSql channelID=%s and userID=%s", channelID, userID)
}
_, err = tx.Exec(sql, args...)
}
if err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMember with channelID=%s and userID=%s", channelID, userID)
}
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userID,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMemberNotifyProps_Select_ToSql channelID=%s and userID=%s", channelID, userID)
}
var dbMember channelMemberWithSchemeRoles
if err2 := tx.Get(&dbMember, selectSQL, args...); err2 != nil {
if err2 == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", channelID, userID))
}
return nil, errors.Wrapf(err2, "failed to get ChannelMember with channelId=%s and userId=%s", channelID, userID)
}
if err2 := tx.Commit(); err2 != nil {
return nil, errors.Wrap(err2, "commit_transaction")
}
return dbMember.ToModel(), err
}
func (s SqlChannelStore) GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelId": channelID,
}).
Limit(uint64(limit)).
Offset(uint64(offset)).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMember_ToSql ChannelID=%s", channelID)
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMembers with channelId=%s", channelID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.StringMap, error) {
dbMembersTimezone := []model.StringMap{}
err := s.GetReplicaX().Select(&dbMembersTimezone, `
SELECT
Users.Timezone
FROM
ChannelMembers
LEFT JOIN
Users ON ChannelMembers.UserId = Id
WHERE ChannelId = ?
`, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find user timezones for users in channels with channelId=%s", channelId)
}
return dbMembersTimezone, nil
}
func (s SqlChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userID,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMember_ToSql ChannelID=%s UserID=%s", channelID, userID)
}
var dbMember channelMemberWithSchemeRoles
if err := s.DBXFromContext(ctx).Get(&dbMember, selectSQL, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", channelID, userID))
}
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s and userId=%s", channelID, userID)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
allChannelMembersForUserCache.Remove(userId)
allChannelMembersForUserCache.Remove(userId + "_deleted")
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Remove by UserId")
}
}
func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
var ids map[string]string
if err := allChannelMembersForUserCache.Get(userId, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
ids, err := s.GetAllChannelMembersForUser(userId, true, false)
if err != nil {
mlog.Error("Error getting all channel members for user", mlog.Err(err))
return false
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model.ChannelMember, error) {
var dbMember channelMemberWithSchemeRoles
query := `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
ChannelMembers.SchemeGuest,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Posts ON ChannelMembers.ChannelId = Posts.ChannelId
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
WHERE
ChannelMembers.UserId = ?
AND
Posts.Id = ?`
if err := s.GetReplicaX().Get(&dbMember, query, userId, postId); err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMember with postId=%s and userId=%s", postId, userId)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool, includeDeleted bool) (_ map[string]string, err error) {
cache_key := userId
if includeDeleted {
cache_key += "_deleted"
}
if allowFromCache {
ids := make(map[string]string)
if err = allChannelMembersForUserCache.Get(cache_key, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
return ids, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
query := s.getQueryBuilder().
Select(`
ChannelMembers.ChannelId, ChannelMembers.Roles, ChannelMembers.SchemeGuest,
ChannelMembers.SchemeUser, ChannelMembers.SchemeAdmin,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
`).
From("ChannelMembers").
Join("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"ChannelMembers.UserId": userId})
if !includeDeleted {
query = query.Where(sq.Eq{"Channels.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers, TeamScheme and ChannelScheme data")
}
defer deferClose(rows, &err)
var data allChannelMembers
for rows.Next() {
var cm allChannelMember
err = rows.Scan(
&cm.ChannelId, &cm.Roles, &cm.SchemeGuest, &cm.SchemeUser,
&cm.SchemeAdmin, &cm.TeamSchemeDefaultGuestRole, &cm.TeamSchemeDefaultUserRole,
&cm.TeamSchemeDefaultAdminRole, &cm.ChannelSchemeDefaultGuestRole,
&cm.ChannelSchemeDefaultUserRole, &cm.ChannelSchemeDefaultAdminRole,
)
if err != nil {
return nil, errors.Wrap(err, "unable to scan columns")
}
data = append(data, cm)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "error while iterating over rows")
}
ids := data.ToMapStringString()
if allowFromCache {
allChannelMembersForUserCache.SetWithExpiry(cache_key, ids, AllChannelMembersForUserCacheDuration)
}
return ids, nil
}
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Remove by ChannelId")
}
}
type allChannelMemberNotifyProps struct {
UserId string
NotifyProps model.StringMap
}
func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId string, allowFromCache bool) (map[string]model.StringMap, error) {
if allowFromCache {
var cacheItem map[string]model.StringMap
if err := allChannelMembersNotifyPropsForChannelCache.Get(channelId, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members Notify Props for Channel")
}
return cacheItem, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members Notify Props for Channel")
}
data := []allChannelMemberNotifyProps{}
err := s.GetReplicaX().Select(&data, `
SELECT UserId, NotifyProps
FROM ChannelMembers
WHERE ChannelId = ?`, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find data from ChannelMembers with channelId=%s", channelId)
}
props := make(map[string]model.StringMap)
for i := range data {
props[data[i].UserId] = data[i].NotifyProps
}
allChannelMembersNotifyPropsForChannelCache.SetWithExpiry(channelId, props, AllChannelMembersNotifyPropsForChannelCacheDuration)
return props, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateMemberCount(channelId string) {
}
func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 {
count, _ := s.GetMemberCount(channelId, true)
return count
}
func (s SqlChannelStore) GetFileCount(channelId string) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
COUNT(*)
FROM
FileInfo
WHERE
FileInfo.DeleteAt = 0
AND FileInfo.ChannelId = ?`,
channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count files with channelId=%s", channelId)
}
return count, nil
}
//nolint:unparam
func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
count(*)
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = ?
AND Users.DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count ChannelMembers with channelId=%s", channelId)
}
return count, nil
}
// GetMemberCountsByGroup returns a slice of ChannelMemberCountByGroup for a given channel
// which contains the number of channel members for each group and optionally the number of unique timezones present for each group in the channel
func (s SqlChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
if includeTimezones {
if s.DriverName() == model.DatabaseDriverMysql {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'true' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.automaticTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.automaticTimezone')
WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'false' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.manualTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.manualTimezone')
END
)) AS ChannelMemberTimezonesCount`
} else if s.DriverName() == model.DatabaseDriverPostgres {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
THEN Timezone->>'automaticTimezone'
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
THEN Timezone->>'manualTimezone'
END
)) AS ChannelMemberTimezonesCount`
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("ChannelMembers").
Join("GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0")
if includeTimezones {
query = query.Join("Users ON Users.Id = GroupMembers.UserId")
}
query = query.Where(sq.Eq{"ChannelMembers.ChannelId": channelID}).GroupBy("GroupMembers.GroupId")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
data := []*model.ChannelMemberCountByGroup{}
if err := s.DBXFromContext(ctx).Select(&data, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to count ChannelMembers with channelId=%s", channelID)
}
return data, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidatePinnedPostCount(channelId string) {
}
//nolint:unparam
func (s SqlChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT count(*)
FROM Posts
WHERE
IsPinned = true
AND ChannelId = ?
AND DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count pinned Posts with channelId=%s", channelId)
}
return count, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateGuestCount(channelId string) {
}
//nolint:unparam
func (s SqlChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
var indexHint string
if s.DriverName() == model.DatabaseDriverMysql {
indexHint = `USE INDEX(idx_channelmembers_channel_id_scheme_guest_user_id)`
}
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
count(*)
FROM
ChannelMembers `+indexHint+`,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = ?
AND ChannelMembers.SchemeGuest = TRUE
AND Users.DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Guests with channelId=%s", channelId)
}
return count, nil
}
func (s SqlChannelStore) RemoveMembers(channelId string, userIds []string) error {
builder := s.getQueryBuilder().
Delete("ChannelMembers").
Where(sq.Eq{"ChannelId": channelId}).
Where(sq.Eq{"UserId": userIds})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "channel_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to delete ChannelMembers")
}
// cleanup sidebarchannels table if the user is no longer a member of that channel
query, args, err = s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Eq{"UserId": userIds},
}).ToSql()
if err != nil {
return errors.Wrap(err, "channel_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to delete SidebarChannels")
}
return nil
}
func (s SqlChannelStore) RemoveMember(channelId string, userId string) error {
return s.RemoveMembers(channelId, []string{userId})
}
func (s SqlChannelStore) RemoveAllDeactivatedMembers(channelId string) error {
query := `
DELETE
FROM
ChannelMembers
WHERE
UserId IN (
SELECT
Id
FROM
Users
WHERE
Users.DeleteAt != 0
)
AND
ChannelMembers.ChannelId = ?
`
_, err := s.GetMasterX().Exec(query, channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete ChannelMembers with channelId=%s", channelId)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM ChannelMembers WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to permanent delete ChannelMembers with userId=%s", userId)
}
return nil
}
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
lastPostAtTimes := []struct {
Id string
LastPostAt int64
TotalMsgCount int64
TotalMsgCountRoot int64
}{}
// We use the question placeholder format for both databases, because
// we replace that with the dollar format later on.
// It's needed to support the prefix CTE query. See: https://github.com/Masterminds/squirrel/issues/285.
query := sq.StatementBuilder.PlaceholderFormat(sq.Question).
Select("Id, LastPostAt, TotalMsgCount, TotalMsgCountRoot").
From("Channels").
Where(sq.Eq{"Id": channelIds})
// TODO: use a CTE for mysql too when version 8 becomes the minimum supported version.
if s.DriverName() == model.DatabaseDriverPostgres {
with := query.Prefix("WITH c AS (").Suffix(") ,")
update := sq.StatementBuilder.PlaceholderFormat(sq.Question).
Update("ChannelMembers cm").
Set("MentionCount", 0).
Set("MentionCountRoot", 0).
Set("UrgentMentionCount", 0).
Set("MsgCount", sq.Expr("greatest(cm.MsgCount, c.TotalMsgCount)")).
Set("MsgCountRoot", sq.Expr("greatest(cm.MsgCountRoot, c.TotalMsgCountRoot)")).
Set("LastViewedAt", sq.Expr("greatest(cm.LastViewedAt, c.LastPostAt)")).
Set("LastUpdateAt", sq.Expr("greatest(cm.LastViewedAt, c.LastPostAt)")).
SuffixExpr(sq.Expr("FROM c WHERE cm.UserId = ? AND c.Id = cm.ChannelId", userId))
updateWrap := update.Prefix("updated AS (").Suffix(")")
query = with.SuffixExpr(updateWrap).Suffix("SELECT Id, LastPostAt FROM c")
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_CTE_Tosql")
}
if s.DriverName() == model.DatabaseDriverPostgres {
sql, err = sq.Dollar.ReplacePlaceholders(sql)
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_ReplacePlaceholders")
}
}
err = s.GetMasterX().Select(&lastPostAtTimes, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with userId=%s and channelId in %v", userId, channelIds)
}
if len(lastPostAtTimes) == 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", fmt.Sprintf("%v", channelIds))
}
times := map[string]int64{}
if s.DriverName() == model.DatabaseDriverPostgres {
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
}
return times, nil
}
var msgCountQuery, msgCountQueryRoot, lastViewedQuery = sq.Case("ChannelId"), sq.Case("ChannelId"), sq.Case("ChannelId")
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
msgCountQuery = msgCountQuery.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(MsgCount, ?)", t.TotalMsgCount))
msgCountQueryRoot = msgCountQueryRoot.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(MsgCountRoot, ?)", t.TotalMsgCountRoot))
lastViewedQuery = lastViewedQuery.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(LastViewedAt, ?)", t.LastPostAt))
}
updateQuery := s.getQueryBuilder().Update("ChannelMembers").
Set("MentionCount", 0).
Set("MentionCountRoot", 0).
Set("UrgentMentionCount", 0).
Set("MsgCount", msgCountQuery).
Set("MsgCountRoot", msgCountQueryRoot).
Set("LastViewedAt", lastViewedQuery).
Set("LastUpdateAt", sq.Expr("LastViewedAt")).
Where(sq.Eq{
"UserId": userId,
"ChannelId": channelIds,
})
sql, args, err = updateQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_Update_Tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s and channelId in %v", userId, channelIds)
}
return times, nil
}
func (s SqlChannelStore) CountUrgentPostsAfter(channelId string, timestamp int64, userId string) (int, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("PostsPriority").
Join("Posts ON Posts.Id = PostsPriority.PostId").
Where(sq.And{
sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent},
sq.Eq{"Posts.ChannelId": channelId},
sq.Gt{"Posts.CreateAt": timestamp},
sq.Eq{"Posts.DeleteAt": 0},
})
if userId != "" {
query = query.Where(sq.Eq{"Posts.UserId": userId})
}
var urgent int64
err := s.GetReplicaX().GetBuilder(&urgent, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count urgent Posts")
}
return int(urgent), nil
}
// CountPostsAfter returns the number of posts in the given channel created after but not including the given timestamp. If given a non-empty user ID, only counts posts made by that user.
func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, userId string) (int, int, error) {
joinLeavePostTypes := []string{
// These types correspond to the ones checked by Post.IsJoinLeaveMessage
model.PostTypeJoinLeave,
model.PostTypeAddRemove,
model.PostTypeJoinChannel,
model.PostTypeLeaveChannel,
model.PostTypeJoinTeam,
model.PostTypeLeaveTeam,
model.PostTypeAddToChannel,
model.PostTypeRemoveFromChannel,
model.PostTypeAddToTeam,
model.PostTypeRemoveFromTeam,
}
query := s.getQueryBuilder().
Select("count(*)").
From("Posts").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Gt{"CreateAt": timestamp},
sq.NotEq{"Type": joinLeavePostTypes},
sq.Eq{"DeleteAt": 0},
})
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, 0, errors.Wrap(err, "CountPostsAfter_ToSql1")
}
var unread int64
err = s.GetReplicaX().Get(&unread, sql, args...)
if err != nil {
return 0, 0, errors.Wrap(err, "failed to count Posts")
}
sql2, args2, err := query.Where(sq.Eq{"RootId": ""}).ToSql()
if err != nil {
return 0, 0, errors.Wrap(err, "CountPostsAfter_ToSql2")
}
var unreadRoot int64
err = s.GetReplicaX().Get(&unreadRoot, sql2, args2...)
if err != nil {
return 0, 0, errors.Wrap(err, "failed to count root Posts")
}
return int(unread), int(unreadRoot), nil
}
// UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post.
// If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns
// an updated model.ChannelUnreadAt that can be returned to the client.
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
unreadDate := unreadPost.CreateAt - 1
unread, unreadRoot, err := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "")
if err != nil {
return nil, err
}
if !setUnreadCountRoot {
unreadRoot = 0
}
params := map[string]any{
"mentions": mentionCount,
"mentionsroot": mentionCountRoot,
"urgentmentions": urgentMentionCount,
"unreadcount": unread,
"unreadcountroot": unreadRoot,
"lastviewedat": unreadDate,
"userid": userID,
"channelid": unreadPost.ChannelId,
"updatedat": model.GetMillis(),
}
// msg count uses the value from channels to prevent counting on older channels where no. of messages can be high.
// we only count the unread which will be a lot less in 99% cases
setUnreadQuery := `
UPDATE
ChannelMembers
SET
MentionCount = :mentions,
MentionCountRoot = :mentionsroot,
UrgentMentionCount = :urgentmentions,
MsgCount = (SELECT TotalMsgCount FROM Channels WHERE ID = :channelid) - :unreadcount,
MsgCountRoot = (SELECT TotalMsgCountRoot FROM Channels WHERE ID = :channelid) - :unreadcountroot,
LastViewedAt = :lastviewedat,
LastUpdateAt = :updatedat
WHERE
UserId = :userid
AND ChannelId = :channelid
`
_, err = s.GetMasterX().NamedExec(setUnreadQuery, params)
if err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMembers")
}
chanUnreadQuery := `
SELECT
c.TeamId TeamId,
cm.UserId UserId,
cm.ChannelId ChannelId,
cm.MsgCount MsgCount,
cm.MsgCountRoot MsgCountRoot,
cm.MentionCount MentionCount,
cm.MentionCountRoot MentionCountRoot,
COALESCE(cm.UrgentMentionCount, 0) UrgentMentionCount,
cm.LastViewedAt LastViewedAt,
cm.NotifyProps NotifyProps
FROM
ChannelMembers cm
LEFT JOIN Channels c ON c.Id=cm.ChannelId
WHERE
cm.UserId = ?
AND cm.channelId = ?
AND c.DeleteAt = 0
`
result := &model.ChannelUnreadAt{}
if err = s.GetMasterX().Get(result, chanUnreadQuery, userID, unreadPost.ChannelId); err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s", unreadPost.ChannelId)
}
return result, nil
}
func (s SqlChannelStore) IncrementMentionCount(channelId string, userIDs []string, isRoot bool, isUrgent bool) error {
now := model.GetMillis()
rootInc := 0
if isRoot {
rootInc = 1
}
urgentInc := 0
if isUrgent {
urgentInc = 1
}
sql, args, err := s.getQueryBuilder().
Update("ChannelMembers").
Set("MentionCount", sq.Expr("MentionCount + 1")).
Set("MentionCountRoot", sq.Expr("MentionCountRoot + ?", rootInc)).
Set("UrgentMentionCount", sq.Expr("UrgentMentionCount + ?", urgentInc)).
Set("LastUpdateAt", now).
Where(sq.Eq{
"UserId": userIDs,
"ChannelId": channelId,
}).
ToSql()
if err != nil {
return errors.Wrap(err, "IncrementMentionCount_Tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "failed to Update ChannelMembers with channelId=%s and userId=%v", channelId, userIDs)
}
return nil
}
func (s SqlChannelStore) GetAll(teamId string) ([]*model.Channel, error) {
data := []*model.Channel{}
err := s.GetReplicaX().Select(&data, "SELECT * FROM Channels WHERE TeamId = ? AND Type != ? ORDER BY Name", teamId, model.ChannelTypeDirect)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s", teamId)
}
return data, nil
}
func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Id": channelIds}).
OrderBy("Name")
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetChannelsByIds_tosql")
}
channels := []*model.Channel{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsWithTeamDataByIds(channelIDs []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
query := s.getQueryBuilder().
Select("c.*",
"COALESCE(t.DisplayName, '') As TeamDisplayName",
"COALESCE(t.Name, '') AS TeamName",
"COALESCE(t.UpdateAt, 0) AS TeamUpdateAt").
From("Channels c").
LeftJoin("Teams t ON c.TeamId = t.Id").
Where(sq.Eq{"c.Id": channelIDs}).
OrderBy("c.Name")
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getChannelsWithTeamData_tosql")
}
channels := []*model.ChannelWithTeamData{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) GetForPost(postId string) (*model.Channel, error) {
channel := model.Channel{}
if err := s.GetReplicaX().Get(
&channel,
`SELECT
Channels.*
FROM
Channels,
Posts
WHERE
Channels.Id = Posts.ChannelId
AND Posts.Id = ?`, postId); err != nil {
return nil, errors.Wrapf(err, "failed to get Channel with postId=%s", postId)
}
return &channel, nil
}
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType model.ChannelType) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*) AS Value").
From("Channels")
if channelType != "" {
query = query.Where(sq.Eq{"Type": channelType})
}
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "AnalyticsTypeCount_ToSql")
}
var value int64
err = s.GetReplicaX().Get(&value, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Channels")
}
return value, nil
}
func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType model.ChannelType) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(Id) AS Value").
From("Channels").
Where(sq.And{
sq.Eq{"Type": channelType},
sq.Gt{"DeleteAt": 0},
})
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "AnalyticsDeletedTypeCount_ToSql")
}
var v int64
err = s.GetReplicaX().Get(&v, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Channels with teamId=%s and channelType=%s", teamId, channelType)
}
return v, nil
}
func (s SqlChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.And{
sq.Eq{"ChannelMembers.UserId": userID},
sq.Or{
sq.Eq{"Teams.Id": teamID},
sq.Eq{"Teams.Id": ""},
sq.Eq{"Teams.Id": nil},
},
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMembersForUser_ToSql teamID=%s userID=%s", teamID, userID)
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with teamId=%s and userId=%s", teamID, userID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersForUserWithCursor(userID, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
query := s.getQueryBuilder().
Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.Roles",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"ChannelMembers.SchemeGuest",
"TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole",
"ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole",
"ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole",
"ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole").
From("ChannelMembers").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{
"ChannelMembers.UserId": userID,
"Channels.DeleteAt": 0,
}).
OrderBy("ChannelId, UserId ASC").
// The limit is verified at the GraphQL layer.
Limit(uint64(opts.Limit))
if teamID != "" {
if opts.ExcludeTeam {
// Exclude this team and DM/GMs
query = query.Where(sq.And{
sq.NotEq{"Channels.TeamId": teamID},
sq.NotEq{"Channels.TeamId": ""},
})
} else {
// Include this team and DM/GMs
query = query.Where(sq.Or{
sq.Eq{"Channels.TeamId": teamID},
sq.Eq{"Channels.TeamId": ""},
})
}
}
if opts.AfterChannel != "" && opts.AfterUser != "" {
query = query.Where(sq.Or{
sq.Gt{"ChannelMembers.ChannelId": opts.AfterChannel},
sq.And{
sq.Eq{"ChannelMembers.ChannelId": opts.AfterChannel},
sq.Gt{"ChannelMembers.UserId": opts.AfterUser},
},
})
}
if opts.LastUpdateAt != 0 {
query = query.Where(sq.GtOrEq{"ChannelMembers.LastUpdateAt": opts.LastUpdateAt})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getMembersForUserWithCursor_tosql")
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with userId=%s", userID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersForUserWithPagination(userId string, page, perPage int) (model.ChannelMembersWithTeamData, error) {
dbMembers := channelMemberWithTeamWithSchemeRolesList{}
offset := page * perPage
err := s.GetReplicaX().Select(&dbMembers, channelMembersWithSchemeSelectQuery+"WHERE ChannelMembers.UserId = ? ORDER BY ChannelId ASC Limit ? Offset ?", userId, perPage, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with and userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
teamMemberIDs := []string{}
if err := s.GetReplicaX().Select(&teamMemberIDs, `SELECT tm.UserId
FROM Channels c, Teams t, TeamMembers tm
WHERE
c.TeamId=t.Id
AND
t.Id=tm.TeamId
AND
c.Id = ?`,
channelID); err != nil {
return nil, errors.Wrapf(err, "error while getting team members for a channel")
}
return teamMemberIDs, nil
}
func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
query := s.getQueryBuilder().Select("c.*",
"t.DisplayName AS TeamDisplayName",
"t.Name AS TeamName",
"t.UpdateAt AS TeamUpdateAt").
From("Channels c, Teams t, TeamMembers tm").
Where(sq.And{
sq.Expr("c.TeamId = t.id"),
sq.Expr("t.id = tm.TeamId"),
sq.Eq{"tm.UserId": userID},
}).
OrderBy("c.DisplayName")
if !includeDeleted {
query = query.Where(sq.And{
sq.Eq{"c.DeleteAt": 0},
sq.Eq{"tm.DeleteAt": 0},
})
}
if isGuest {
query = query.Where(sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})))
} else {
query = query.Where(sq.Or{
sq.NotEq{"c.Type": model.ChannelTypePrivate},
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})),
},
})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Autocomplete_Tosql")
}
channels := model.ChannelListWithTeamData{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "could not find channel with term=%s", term)
}
return channels, nil
}
func (s SqlChannelStore) AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("*").
From("Channels c").
Where(sq.Eq{"c.TeamId": teamID}).
OrderBy("c.DisplayName").
Limit(model.ChannelSearchDefaultLimit)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
if isGuest {
query = query.Where(sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})))
} else {
query = query.Where(sq.Or{
sq.NotEq{"c.Type": model.ChannelTypePrivate},
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})),
},
})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
// shared query
query := s.getSubQueryBuilder().Select("C.*").
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
Limit(50).
Where(sq.And{
sq.Or{
sq.Eq{"C.TeamId": teamID},
sq.Eq{
"C.TeamId": "",
"C.Type": model.ChannelTypeGroup,
},
},
sq.Eq{"CM.UserId": userID},
})
if !includeDeleted {
// include the DeleteAt = 0 condition
query.Where(sq.Eq{"DeleteAt": 0})
}
var (
channels = model.ChannelList{}
sql string
args []any
)
// build the like clause
like := s.buildLIKEClauseX(term, "Name", "DisplayName", "Purpose")
if like == nil {
var err error
// generate the SQL query
sql, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Tosql")
}
} else {
// build the full text search clause
full := s.buildFulltextClauseX(term, "Name", "DisplayName", "Purpose")
// build the LIKE query
likeSQL, likeArgs, err := query.Where(like).ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Like_Tosql")
}
// build the full text query
fullSQL, fullArgs, err := query.Where(full).ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Full_Tosql")
}
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
// query you would get using an OR of the LIKE and full-text clauses.
sql = fmt.Sprintf("(%s) UNION (%s) LIMIT 50", likeSQL, fullSQL)
args = append(likeArgs, fullArgs...)
}
var err error
// since the UNION is not part of squirrel, we need to assemble it and then update
// the placeholders manually
if s.DriverName() == model.DatabaseDriverPostgres {
sql, err = sq.Dollar.ReplacePlaceholders(sql)
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Placeholder")
}
}
// query the database
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
}
directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userID, term)
if err != nil {
return nil, err
}
channels = append(channels, directChannels...)
sort.Slice(channels, func(a, b int) bool {
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
})
return channels, nil
}
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string, term string) ([]*model.Channel, error) {
// create the main query
query := s.getQueryBuilder().Select("C.*", "OtherUsers.Username as DisplayName").
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
Where(sq.Eq{
"C.Type": model.ChannelTypeDirect,
"CM.UserId": userID,
}).
Limit(50)
// create the subquery
subQuery := s.getSubQueryBuilder().Select("ICM.ChannelId AS ChannelId", "IU.Username AS Username").
From("Users AS IU").
Join("ChannelMembers AS ICM ON ICM.UserId = IU.Id").
Where(sq.NotEq{"IU.Id": userID})
// try to create a LIKE clause from the search term
if like := s.buildLIKEClauseX(term, "IU.Username", "IU.Nickname"); like != nil {
subQuery = subQuery.Where(like)
}
// put the subquery into an INNER JOIN
innerJoin := subQuery.
Prefix("INNER JOIN (").
Suffix(") AS OtherUsers ON OtherUsers.ChannelId = C.Id")
// add the subquery to the main query
query = query.JoinClause(innerJoin)
// create the SQL query and argument list
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "autocompleteInTeamForSearchDirectMessages_InnerJoin_Tosql")
}
// query the channel list from the database using SQLX
channels := model.ChannelList{}
if err := s.GetReplicaX().Select(&channels, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s' (%s %% %v)", term, sql, args)
}
return channels, nil
}
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Where(sq.Eq{"c.TeamId": teamId}).
OrderBy("c.DisplayName").
Limit(100)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
if term != "" {
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (model.ChannelList, error) {
queryBase := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("Channels c ON (c.Id = Channels.Id)").
Where(sq.And{
sq.Eq{"c.TeamId": teamId},
sq.NotEq{"c.DeleteAt": 0},
}).
OrderBy("c.DisplayName").
Limit(100)
searchClause := s.searchClause(term)
if searchClause != nil {
queryBase = queryBase.Where(searchClause)
}
publicQuery := queryBase.
Where(sq.NotEq{"c.Type": model.ChannelTypePrivate})
privateQuery := queryBase.
Where(
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userId})),
})
publicChannels, err := s.performSearch(publicQuery, term)
if err != nil {
return nil, err
}
privateChannels, err := s.performSearch(privateQuery, term)
if err != nil {
return nil, err
}
output := publicChannels
output = append(output, privateChannels...)
return output, nil
}
func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Join("ChannelMembers cm ON (c.Id = cm.ChannelId)").
Where(sq.Eq{
"c.TeamId": teamId,
"cm.UserId": userId,
}).
OrderBy("c.DisplayName").
Limit(100)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.SelectBuilder {
var limit int
if opts.PerPage != nil {
limit = *opts.PerPage
} else {
limit = 100
}
var selectStr string
if opts.CountOnly {
selectStr = "count(*)"
} else {
selectStr = "c.*"
if opts.IncludeTeamInfo {
selectStr += ", t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"
}
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId AS PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Join("Teams AS t ON t.Id = c.TeamId")
// don't bother ordering or limiting if we're just getting the count
if !opts.CountOnly {
query = query.
OrderBy("c.DisplayName, t.DisplayName").
Limit(uint64(limit))
}
if opts.Deleted {
query = query.Where(sq.NotEq{"c.DeleteAt": int(0)})
} else if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.IsPaginated() && !opts.CountOnly {
query = query.Offset(uint64(*opts.Page * *opts.PerPage))
}
if opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": opts.PolicyID})
} else if opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where("RetentionPoliciesChannels.ChannelId IS NULL")
} else if opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
likeFields := "c.Name, c.DisplayName, c.Purpose"
if opts.IncludeSearchById {
likeFields = likeFields + ", c.Id"
}
likeClause, likeTerm := s.buildLIKEClause(opts.Term, likeFields)
if likeTerm != "" {
// Keep the number of likeTerms same as the number of columns
// (c.Name, c.DisplayName, c.Purpose, c.Id?)
likeTerms := make([]any, len(strings.Split(likeFields, ",")))
for i := 0; i < len(likeTerms); i++ {
likeTerms[i] = likeTerm
}
likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "?")
fulltextClause, fulltextTerm := s.buildFulltextClause(opts.Term, "c.Name, c.DisplayName, c.Purpose")
fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "?")
query = query.Where(sq.Or{
sq.Expr(likeClause, likeTerms...),
sq.Expr(fulltextClause, fulltextTerm),
})
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if opts.NotAssociatedToGroup != "" {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.TeamIds) > 0 {
query = query.Where(sq.Eq{"c.TeamId": opts.TeamIds})
}
if opts.GroupConstrained {
query = query.Where(sq.Eq{"c.GroupConstrained": true})
} else if opts.ExcludeGroupConstrained {
query = query.Where(sq.Or{
sq.NotEq{"c.GroupConstrained": true},
sq.Eq{"c.GroupConstrained": nil},
})
}
if opts.Public && !opts.Private {
query = query.InnerJoin("PublicChannels ON c.Id = PublicChannels.Id")
} else if opts.Private && !opts.Public {
query = query.Where(sq.Eq{"c.Type": model.ChannelTypePrivate})
} else {
query = query.Where(sq.Or{
sq.Eq{"c.Type": model.ChannelTypeOpen},
sq.Eq{"c.Type": model.ChannelTypePrivate},
})
}
return query
}
func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
opts.Term = term
opts.IncludeTeamInfo = true
queryString, args, err := s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}
channels := model.ChannelListWithTeamData{}
if err2 := s.GetReplicaX().Select(&channels, queryString, args...); err2 != nil {
return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term)
}
var totalCount int64
// only query a 2nd time for the count if the results are being requested paginated.
if opts.IsPaginated() {
opts.CountOnly = true
queryString, args, err = s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}
if err2 := s.GetReplicaX().Get(&totalCount, queryString, args...); err2 != nil {
return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term)
}
} else {
totalCount = int64(len(channels))
}
return channels, totalCount, nil
}
func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) (model.ChannelList, error) {
teamQuery := s.getSubQueryBuilder().Select("c.Id").
From("PublicChannels c").
Join("ChannelMembers cm ON (cm.ChannelId = c.Id)").
Where(sq.Eq{
"c.TeamId": teamId,
"cm.UserId": userId,
"c.DeleteAt": 0,
})
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id=Channels.Id)").
Where(sq.And{
sq.Eq{"c.TeamId": teamId},
sq.Eq{"c.DeleteAt": 0},
sq.Expr("c.Id NOT IN (?)", teamQuery),
}).
OrderBy("c.DisplayName").
Limit(100)
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) buildLIKEClause(term string, searchColumns string) (likeClause, likeTerm string) {
likeTerm = sanitizeSearchTerm(term, "*")
if likeTerm == "" {
return
}
// Prepare the LIKE portion of the query.
var searchFields []string
for _, field := range strings.Split(searchColumns, ", ") {
if s.DriverName() == model.DatabaseDriverPostgres {
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(%s) escape '*'", field, ":LikeTerm"))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE %s escape '*'", field, ":LikeTerm"))
}
}
likeClause = fmt.Sprintf("(%s)", strings.Join(searchFields, " OR "))
likeTerm = wildcardSearchTerm(likeTerm)
return
}
func (s SqlChannelStore) buildLIKEClauseX(term string, searchColumns ...string) sq.Sqlizer {
// escape the special characters with *
likeTerm := sanitizeSearchTerm(term, "*")
if likeTerm == "" {
return nil
}
// add a placeholder at the beginning and end
likeTerm = wildcardSearchTerm(likeTerm)
// Prepare the LIKE portion of the query.
var searchFields sq.Or
for _, field := range searchColumns {
if s.DriverName() == model.DatabaseDriverPostgres {
expr := fmt.Sprintf("LOWER(%s) LIKE LOWER(?) ESCAPE '*'", field)
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
} else {
expr := fmt.Sprintf("%s LIKE ? ESCAPE '*'", field)
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
}
}
return searchFields
}
const spaceFulltextSearchChars = "<>+-()~:*\"!@"
func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) {
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm = term
// These chars must be treated as spaces in the fulltext query.
fulltextTerm = strings.Map(func(r rune) rune {
if strings.ContainsRune(spaceFulltextSearchChars, r) {
return ' '
}
return r
}, fulltextTerm)
// Prepare the FULLTEXT portion of the query.
if s.DriverName() == model.DatabaseDriverPostgres {
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
splitTerm[i] = t + ":*"
}
fulltextTerm = strings.Join(splitTerm, " & ")
fulltextClause = fmt.Sprintf("((to_tsvector('%[1]s', %[2]s)) @@ to_tsquery('%[1]s', :FulltextTerm))", s.pgDefaultTextSearchConfig, convertMySQLFullTextColumnsToPostgres(searchColumns))
} else if s.DriverName() == model.DatabaseDriverMysql {
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
splitTerm[i] = "+" + t + "*"
}
fulltextTerm = strings.Join(splitTerm, " ")
fulltextClause = fmt.Sprintf("MATCH(%s) AGAINST (:FulltextTerm IN BOOLEAN MODE)", searchColumns)
}
return
}
func (s SqlChannelStore) buildFulltextClauseX(term string, searchColumns ...string) sq.Sqlizer {
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm := term
// These chars must be treated as spaces in the fulltext query.
fulltextTerm = strings.Map(func(r rune) rune {
if strings.ContainsRune(spaceFulltextSearchChars, r) {
return ' '
}
return r
}, fulltextTerm)
// Prepare the FULLTEXT portion of the query.
if s.DriverName() == model.DatabaseDriverPostgres {
// remove all pipes |
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
// split the search term and append :* to each part
splitTerm := strings.Fields(fulltextTerm)
for i, t := range splitTerm {
splitTerm[i] = t + ":*"
}
// join the search term with &
fulltextTerm = strings.Join(splitTerm, " & ")
expr := fmt.Sprintf("((to_tsvector('%[1]s', %[2]s)) @@ to_tsquery('%[1]s', ?))", s.pgDefaultTextSearchConfig, strings.Join(searchColumns, " || ' ' || "))
return sq.Expr(expr, fulltextTerm)
}
splitTerm := strings.Fields(fulltextTerm)
for i, t := range splitTerm {
splitTerm[i] = "+" + t + "*"
}
fulltextTerm = strings.Join(splitTerm, " ")
expr := fmt.Sprintf("MATCH(%s) AGAINST (? IN BOOLEAN MODE)", strings.Join(searchColumns, ", "))
return sq.Expr(expr, fulltextTerm)
}
func (s SqlChannelStore) performSearch(searchQuery sq.SelectBuilder, term string) (model.ChannelList, error) {
sql, args, err := searchQuery.ToSql()
if err != nil {
return model.ChannelList{}, errors.Wrapf(err, "performSearch_ToSql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
}
return channels, nil
}
func (s SqlChannelStore) searchClause(term string) sq.Sqlizer {
likeClause := s.buildLIKEClauseX(term, "c.Name", "c.DisplayName", "c.Purpose")
if likeClause == nil {
return nil
}
fulltextClause := s.buildFulltextClauseX(term, "c.Name", "c.DisplayName", "c.Purpose")
return sq.Or{
likeClause,
fulltextClause,
}
}
func (s SqlChannelStore) searchGroupChannelsQuery(userId, term string, isPostgreSQL bool) sq.SelectBuilder {
var baseLikeTerm string
terms := strings.Fields((strings.ToLower(term)))
having := sq.And{}
if isPostgreSQL {
baseLikeTerm = "ARRAY_TO_STRING(ARRAY_AGG(u.Username), ', ') LIKE ?"
cc := s.getSubQueryBuilder().Select("c.Id").
From("Channels c").
Join("ChannelMembers cm ON c.Id=cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
Where(sq.Eq{
"c.Type": model.ChannelTypeGroup,
"u.id": userId,
}).
GroupBy("c.Id")
for _, term := range terms {
term = sanitizeSearchTerm(term, "\\")
having = append(having, sq.Expr(baseLikeTerm, "%"+term+"%"))
}
subq := s.getSubQueryBuilder().Select("cc.id").
FromSelect(cc, "cc").
Join("ChannelMembers cm On cc.Id = cm.ChannelId").
Join("Users u On u.Id = cm.UserId").
GroupBy("cc.Id").
Having(having).
Limit(model.ChannelSearchDefaultLimit)
return s.getQueryBuilder().Select("*").
From("Channels").
Where(sq.Expr("Id IN (?)", subq))
}
baseLikeTerm = "GROUP_CONCAT(u.Username SEPARATOR ', ') LIKE ?"
for _, term := range terms {
term = sanitizeSearchTerm(term, "\\")
having = append(having, sq.Expr(baseLikeTerm, "%"+term+"%"))
}
cc := s.getSubQueryBuilder().Select("c.*").
From("Channels c").
Join("ChannelMembers cm ON c.Id=cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
Where(sq.Eq{
"c.Type": model.ChannelTypeGroup,
"u.Id": userId,
}).
GroupBy("c.Id")
return s.getQueryBuilder().Select("cc.*").
FromSelect(cc, "cc").
Join("ChannelMembers cm on cc.Id = cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
GroupBy("cc.Id").
Having(having).
Limit(model.ChannelSearchDefaultLimit)
}
func (s SqlChannelStore) SearchGroupChannels(userId, term string) (model.ChannelList, error) {
isPostgreSQL := s.DriverName() == model.DatabaseDriverPostgres
query := s.searchGroupChannelsQuery(userId, term, isPostgreSQL)
sql, params, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "SearchGroupChannels_Tosql")
}
groupChannels := model.ChannelList{}
if err := s.GetReplicaX().Select(&groupChannels, sql, params...); err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", term, userId)
}
return groupChannels, nil
}
func (s SqlChannelStore) GetMembersByIds(channelID string, userIDs []string) (model.ChannelMembers, error) {
query := s.channelMembersForTeamWithSchemeSelectQuery.Where(
sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userIDs,
},
)
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetMembersByIds_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
if err := s.GetReplicaX().Select(&dbMembers, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers with channelId=%s and userId in %v", channelID, userIDs)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersByChannelIds(channelIDs []string, userID string) (model.ChannelMembers, error) {
query := s.channelMembersForTeamWithSchemeSelectQuery.Where(
sq.Eq{
"ChannelMembers.ChannelId": channelIDs,
"ChannelMembers.UserId": userID,
},
)
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetMembersByChannelIds_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
if err := s.GetReplicaX().Select(&dbMembers, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers with userId=%s and channelId in %v", userID, channelIDs)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
query := s.getQueryBuilder().
Select("Channels.Id as ChannelId, Users.Id, Users.FirstName, Users.LastName, Users.Nickname, Users.Username").
From("ChannelMembers as cm").
Join("Channels ON cm.ChannelId = Channels.Id").
Join("Users ON cm.UserId = Users.Id").
Where(sq.Eq{
"Channels.Id": channelIDs,
"Channels.DeleteAt": 0,
})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "dm_gm_names_tosql")
}
res := []*struct {
model.User
ChannelId string
}{}
if err := s.GetReplicaX().Select(&res, sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to find channels display name")
}
if len(res) == 0 {
return nil, store.NewErrNotFound("User", fmt.Sprintf("%v", channelIDs))
}
userInfo := make(map[string][]*model.User)
for _, item := range res {
userInfo[item.ChannelId] = append(userInfo[item.ChannelId], &item.User)
}
return userInfo, nil
}
func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = ? ORDER BY DisplayName LIMIT ? OFFSET ?", schemeId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with schemeId=%s", schemeId)
}
return channels, nil
}
// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) (_ map[string]string, err error) {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
channelMembers := []channelMember{}
query := `
SELECT
ChannelId,
UserId,
Roles,
LastViewedAt,
MsgCount,
MentionCount,
MentionCountRoot,
COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount,
MsgCountRoot,
NotifyProps,
LastUpdateAt,
SchemeUser,
SchemeAdmin,
SchemeGuest
FROM
ChannelMembers
WHERE
(ChannelId, UserId) > (?, ?)
ORDER BY ChannelId, UserId
LIMIT 100
`
if err := transaction.Select(&channelMembers, query, fromChannelId, fromUserId); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
if len(channelMembers) == 0 {
// No more channel members in query result means that the migration has finished.
return nil, nil
}
for i := range channelMembers {
member := channelMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.ChannelAdminRoleId {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.ChannelUserRoleId {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.ChannelGuestRoleId {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.NamedExec(`UPDATE ChannelMembers
SET Roles=:Roles,
LastViewedAt=:LastViewedAt,
MsgCount=:MsgCount,
MentionCount=:MentionCount,
UrgentMentionCount=:UrgentMentionCount,
NotifyProps=:NotifyProps,
LastUpdateAt=:LastUpdateAt,
SchemeUser=:SchemeUser,
SchemeAdmin=:SchemeAdmin,
SchemeGuest=:SchemeGuest,
MentionCountRoot=:MentionCountRoot,
MsgCountRoot=:MsgCountRoot
WHERE ChannelId=:ChannelId AND UserId=:UserId`, &member); err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMember")
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
data := make(map[string]string)
data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId
data["UserId"] = channelMembers[len(channelMembers)-1].UserId
return data, nil
}
func (s SqlChannelStore) ResetAllChannelSchemes() (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = s.resetAllChannelSchemesT(transaction)
if err != nil {
return err
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlChannelStore) resetAllChannelSchemesT(transaction *sqlxTxWrapper) error {
if _, err := transaction.Exec("UPDATE Channels SET SchemeId=''"); err != nil {
return errors.Wrap(err, "failed to update Channels")
}
return nil
}
func (s SqlChannelStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastChannelId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
channelMembers := []*channelMember{}
query := `
SELECT
ChannelId,
UserId,
Roles,
LastViewedAt,
MsgCount,
MentionCount,
MentionCountRoot,
COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount,
MsgCountRoot,
NotifyProps,
LastUpdateAt,
SchemeUser,
SchemeAdmin,
SchemeGuest
FROM
ChannelMembers
WHERE
(ChannelId, UserId) > (?, ?)
ORDER BY ChannelId, UserId
LIMIT 1000
`
if err = transaction.Select(&channelMembers, query, lastChannelId, lastUserId); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "failed to find ChannelMembers")
}
if len(channelMembers) == 0 {
finalizeTransactionX(transaction, &err)
break
}
for _, member := range channelMembers {
lastUserId = member.UserId
lastChannelId = member.ChannelId
var newRoles []string
for _, role := range strings.Fields(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err = transaction.Exec("UPDATE ChannelMembers SET Roles = ? WHERE UserId = ? AND ChannelId = ?", newRolesString, member.UserId, member.ChannelId); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "failed to update ChannelMembers")
}
}
}
if err = transaction.Commit(); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) ([]*model.ChannelForExport, error) {
channels := []*model.ChannelForExport{}
if err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*,
Teams.Name as TeamName,
Schemes.Name as SchemeName
FROM Channels
INNER JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes ON Channels.SchemeId = Schemes.Id
WHERE
Channels.Id > ?
AND Channels.Type IN (?, ?)
ORDER BY
Id
LIMIT ?`,
afterId, model.ChannelTypeOpen, model.ChannelTypePrivate, limit); err != nil {
return nil, errors.Wrap(err, "failed to find Channels for export")
}
return channels, nil
}
func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string) ([]*model.ChannelMemberForExport, error) {
members := []*model.ChannelMemberForExport{}
err := s.GetReplicaX().Select(&members, `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) as SchemeGuest,
Channels.Name as ChannelName
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
WHERE
ChannelMembers.UserId = ?
AND Channels.TeamId = ?
AND Channels.DeleteAt = 0`,
userId, teamId)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels for export")
}
return members, nil
}
func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string) ([]*model.DirectChannelForExport, error) {
directChannelsForExport := []*model.DirectChannelForExport{}
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels").
Where(sq.And{
sq.Gt{"Channels.Id": afterId},
sq.Eq{"Channels.DeleteAt": int(0)},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}).
OrderBy("Channels.Id").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
if err2 := s.GetReplicaX().Select(&directChannelsForExport, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find direct Channels for export")
}
var channelIds []string
for _, channel := range directChannelsForExport {
channelIds = append(channelIds, channel.Id)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, COALESCE(UrgentMentionCount, 0) UrgentMentionCount, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
From("ChannelMembers cm").
Join("Users u ON ( u.Id = cm.UserId )").
Where(sq.And{
sq.Eq{"cm.ChannelId": channelIds},
sq.Eq{"u.DeleteAt": int(0)},
})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
channelMembers := []*model.ChannelMemberForExport{}
if err2 := s.GetReplicaX().Select(&channelMembers, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find ChannelMembers")
}
// Populate each channel with its members
dmChannelsMap := make(map[string]*model.DirectChannelForExport)
for _, channel := range directChannelsForExport {
channel.Members = &[]string{}
dmChannelsMap[channel.Id] = channel
}
for _, member := range channelMembers {
members := dmChannelsMap[member.ChannelId].Members
*members = append(*members, member.Username)
}
return directChannelsForExport, nil
}
func (s SqlChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
query :=
`SELECT
*
FROM
Channels
WHERE
CreateAt > ?
OR
(CreateAt = ? AND Id > ?)
ORDER BY
CreateAt ASC, Id ASC
LIMIT
?`
channels := []*model.Channel{}
err := s.GetSearchReplicaX().Select(&channels, query, startTime, startTime, startChannelID, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) UserBelongsToChannels(userId string, channelIds []string) (bool, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("ChannelMembers").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"ChannelId": channelIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "channel_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to count ChannelMembers")
}
return c > 0, nil
}
// TODO: parameterize userIDs
func (s SqlChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
sql := fmt.Sprintf(`
UPDATE
ChannelMembers
SET
SchemeAdmin = CASE WHEN UserId IN ('%s') THEN
TRUE
ELSE
FALSE
END
WHERE
ChannelId = ?
AND (SchemeGuest = false OR SchemeGuest IS NULL)
`, strings.Join(userIDs, "', '"))
if _, err := s.GetMasterX().Exec(sql, channelID); err != nil {
return errors.Wrap(err, "failed to update ChannelMembers")
}
return nil
}
func (s SqlChannelStore) GroupSyncedChannelCount() (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("Channels").
Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Channels")
}
return count, nil
}
// SetShared sets the Shared flag true/false
func (s SqlChannelStore) SetShared(channelId string, shared bool) error {
squery, args, err := s.getQueryBuilder().
Update("Channels").
Set("Shared", shared).
Where(sq.Eq{"Id": channelId}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_set_shared_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update `Shared` for Channels")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", channelId)
}
return nil
}
// GetTeamForChannel returns the team for a given channelID.
func (s SqlChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
nestedQ, nestedArgs, err := s.getQueryBuilder().Select("TeamId").From("Channels").Where(sq.Eq{"Id": channelID}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_for_channel_nested_tosql")
}
query, args, err := s.getQueryBuilder().
Select("*").
From("Teams").Where(sq.Expr("Id = ("+nestedQ+")", nestedArgs...)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_for_channel_tosql")
}
team := model.Team{}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("channel_id=%s", channelID))
}
return nil, errors.Wrapf(err, "failed to find team with channel_id=%s", channelID)
}
return &team, nil
}
// GetTopChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
postgresPropQuery := `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
mySqlPropsQuery := `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
query := `
SELECT
ID,
Type,
DisplayName,
Name,
TeamID,
MessageCount
FROM
((SELECT
Posts.ChannelId AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
PublicChannels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
PublicChannels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN PublicChannels on Posts.ChannelId = PublicChannels.Id
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = []any{since}
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND PublicChannels.TeamId = ?
GROUP BY
Posts.ChannelId,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId,
PublicChannels.DeleteAt)
UNION ALL
(SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
Channels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = append(args, teamID, since)
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND Channels.TeamId = ?
AND Channels.Type = 'P'
AND ChannelMembers.UserId = ?
GROUP BY
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId,
Channels.DeleteAt)) AS A
WHERE
DeleteAt = 0
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, teamID, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
var query string
var propsQuery string
if s.DriverName() == model.DatabaseDriverMysql {
propsQuery = `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
propsQuery = `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query = `
SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''
AND Posts.UserID = ?
AND Channels.DeleteAt = 0
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
query += propsQuery
args = []any{since, userID, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
query := `
SELECT
ID,
Type,
DisplayName,
Name,
MessageCount,
LastActivityAt
FROM
((SELECT
PublicChannels.Id AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
PublicChannels
LEFT JOIN Posts on Posts.ChannelId = PublicChannels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN Channels on Channels.Id = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
AND PublicChannels.DeleteAt = 0
AND Channels.CreateAt < ?
GROUP BY
PublicChannels.Id,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId)
UNION ALL
(SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.TeamId = ?
AND Channels.CreateAt < ?
AND Channels.Type = 'P'
AND Channels.DeleteAt = 0
AND ChannelMembers.UserId = ?
GROUP BY
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name)) AS A
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, since, teamID, since, since, teamID, since, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
var query string
query = `
SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.DeleteAt = 0
AND Channels.CreateAt < ?
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
args = []any{since, since, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
func postProcessTopInactiveChannels(s SqlChannelStore, channels []*model.TopInactiveChannel) ([]*model.TopInactiveChannel, error) {
// query channel members for Ids
var conditionalAggrSelector string
if s.DriverName() == model.DatabaseDriverMysql {
conditionalAggrSelector = "GROUP_CONCAT(UserId SEPARATOR ',') as UserIds"
} else if s.DriverName() == model.DatabaseDriverPostgres {
conditionalAggrSelector = "string_agg(UserId, ',') as UserIds"
}
var channelIds []string
for _, channel := range channels {
channelIds = append(channelIds, channel.ID)
}
q := s.getQueryBuilder().Select("ChannelId", conditionalAggrSelector).From("ChannelMembers").
Where(sq.Eq{
"ChannelId": channelIds,
}).GroupBy("ChannelId")
channelsUserIdsMap := make(map[string]string, len(channels))
type ChannelUserIdsResult struct {
ChannelId string
UserIds string
}
channelsUserIdsResultList := make([]ChannelUserIdsResult, len(channels))
sql, args, err := q.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to stringify squirrel query")
}
if err := s.GetReplicaX().Select(&channelsUserIdsResultList, sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels users")
}
for _, channelUserIds := range channelsUserIdsResultList {
channelsUserIdsMap[channelUserIds.ChannelId] = channelUserIds.UserIds
}
for index, channel := range channels {
userIds := channelsUserIdsMap[channel.ID]
userIdsSlice := strings.Split(userIds, ",")
channels[index].Participants = userIdsSlice
// handle channels with 0 participants
if len(userIdsSlice) == 1 && userIdsSlice[0] == "" {
channels[index].Participants = make([]string, 0)
}
}
return channels, nil
}
func (s SqlChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, atLocation *time.Location) ([]*model.DurationPostCount, error) {
var unixSelect string
var propsQuery string
loc := atLocation.String()
if loc == "Local" {
loc = "UTC"
}
var format string
if s.DriverName() == model.DatabaseDriverMysql {
if duration == model.PostsByDay {
format = `%Y-%m-%d`
} else {
format = `%Y-%m-%dT%H`
}
unixSelect = fmt.Sprintf(`DATE_FORMAT(
COALESCE(
CONVERT_TZ(FROM_UNIXTIME(Posts.CreateAt / 1000), 'GMT', '%s'),
FROM_UNIXTIME(Posts.CreateAt / 1000)
),
'%s') AS duration`, loc, format)
propsQuery = `(JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
if duration == model.PostsByDay {
format = "YYYY-MM-DD"
} else {
format = `YYYY-MM-DD"T"HH24`
}
unixSelect = fmt.Sprintf(`TO_CHAR(TO_TIMESTAMP(Posts.CreateAt / 1000) AT TIME ZONE '%s', '%s') AS duration`, loc, format)
propsQuery = `(Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query := sq.
Select("Posts.ChannelId AS channelid", unixSelect, "count(Posts.Id) AS postcount").
From("Posts").
LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
Where(sq.And{
sq.Eq{"Posts.DeleteAt": 0},
sq.Gt{"Posts.CreateAt": sinceUnixMillis},
sq.Eq{"Posts.Type": ""},
sq.Eq{"Channels.Id": channelIDs},
}).
Where(propsQuery).
GroupBy("channelid", "duration").
OrderBy("channelid", "duration")
if userID != nil && model.IsValidId(*userID) {
query = query.Where(sq.And{sq.Eq{"Posts.UserId": *userID}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to parse query")
}
dailyPostCounts := make([]*model.DurationPostCount, 0)
if err := s.GetReplicaX().Select(&dailyPostCounts, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to get post counts by duration")
}
return dailyPostCounts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// dbSelecter is an interface used to enable some internal store methods
// using both transaction and normal queries.
type dbSelecter interface {
Select(i any, query string, args ...any) error
}
func (s SqlChannelStore) CreateInitialSidebarCategories(userId string, opts *store.SidebarCategorySearchOpts) (_ *model.OrderedSidebarCategories, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamsWithExclude, err := s.SqlStore.stores.team.GetTeamsForUser(context.Background(), userId, opts.TeamID, false)
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: GetTeamsForUser")
}
excludedTeamIDs := make([]string, 0, len(teamsWithExclude))
for _, tm := range teamsWithExclude {
excludedTeamIDs = append(excludedTeamIDs, tm.TeamId)
}
if err = s.createInitialSidebarCategoriesT(transaction, userId, excludedTeamIDs, opts); err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: createInitialSidebarCategoriesT")
}
oc, err := s.getSidebarCategoriesT(transaction, userId, opts)
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: getSidebarCategoriesT")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: commit_transaction")
}
return oc, nil
}
func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *sqlxTxWrapper, userId string, excludedTeamIDs []string, opts *store.SidebarCategorySearchOpts) error {
query := s.getQueryBuilder().
Select("Type, TeamId").
From("SidebarCategories").
Where(sq.Eq{
"UserId": userId,
"Type": []model.SidebarCategoryType{
model.SidebarCategoryFavorites,
model.SidebarCategoryChannels,
model.SidebarCategoryDirectMessages,
},
})
if !opts.ExcludeTeam {
query = query.Where(sq.Eq{"TeamId": opts.TeamID})
} else {
query = query.Where(sq.NotEq{"TeamId": opts.TeamID})
}
selectQuery, selectParams, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT_Tosql")
}
existingTypes := []struct {
Type model.SidebarCategoryType
TeamId string
}{}
err = transaction.Select(&existingTypes, selectQuery, selectParams...)
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to select existing categories")
}
hasCategoryOfType := make(map[model.SidebarCategoryType]map[string]bool, len(existingTypes))
for _, existingType := range existingTypes {
if hasCategoryOfType[existingType.Type] == nil {
hasCategoryOfType[existingType.Type] = make(map[string]bool)
hasCategoryOfType[existingType.Type][existingType.TeamId] = true
}
}
insertBuilder := s.getQueryBuilder().Insert("SidebarCategories").
Columns("Id, UserId, TeamId, SortOrder, Sorting, Type, DisplayName, Muted, Collapsed")
hasInsert := false
getRequiredTeamIDs := func(category model.SidebarCategoryType, opts *store.SidebarCategorySearchOpts) []string {
// if category == nil - nothing
// if not exclude - just that team
// otherwise get all teams excluding that team
// if != nil - then partial
// if not exclude, and team exists in map then skip.
// otherwise, get all teams excluding that team, subtract all items from map.
if hasCategoryOfType[category] == nil {
// If not exclude, do for only single team
// if exclude, get all teams, excluding that team
if !opts.ExcludeTeam {
return []string{opts.TeamID}
}
return excludedTeamIDs
}
mapEntry := hasCategoryOfType[category]
if !opts.ExcludeTeam && mapEntry[opts.TeamID] {
// continue, nothing to do since entry already exists.
} else {
for i, tID := range excludedTeamIDs {
if mapEntry[tID] {
// remove from slice
copy(excludedTeamIDs[i:], excludedTeamIDs[i+1:])
excludedTeamIDs[len(excludedTeamIDs)-1] = ""
excludedTeamIDs = excludedTeamIDs[:len(excludedTeamIDs)-1]
}
}
return excludedTeamIDs
}
return []string{}
}
teamIDs := getRequiredTeamIDs(model.SidebarCategoryFavorites, opts)
for _, teamID := range teamIDs {
// Use deterministic IDs for default categories to prevent potentially creating multiple copies of a default category
favoritesCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryFavorites, userId, teamID)
// Create the SidebarChannels first since there's more opportunity for something to fail here
if err := s.migrateFavoritesToSidebarT(transaction, userId, teamID, favoritesCategoryId); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to migrate favorites to sidebar")
}
insertBuilder = insertBuilder.Values(favoritesCategoryId, userId, teamID, model.DefaultSidebarSortOrderFavorites, model.SidebarCategorySortDefault, model.SidebarCategoryFavorites, "Favorites" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
teamIDs = getRequiredTeamIDs(model.SidebarCategoryChannels, opts)
for _, teamID := range teamIDs {
channelsCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryChannels, userId, teamID)
insertBuilder = insertBuilder.Values(channelsCategoryId, userId, teamID, model.DefaultSidebarSortOrderChannels, model.SidebarCategorySortDefault, model.SidebarCategoryChannels, "Channels" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
teamIDs = getRequiredTeamIDs(model.SidebarCategoryDirectMessages, opts)
for _, teamID := range teamIDs {
directMessagesCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryDirectMessages, userId, teamID)
insertBuilder = insertBuilder.Values(directMessagesCategoryId, userId, teamID, model.DefaultSidebarSortOrderDMs, model.SidebarCategorySortRecent, model.SidebarCategoryDirectMessages, "Direct Messages" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
if hasInsert {
sql, args, err := insertBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "insertSidebarCategories_Tosql")
}
_, err = transaction.Exec(sql, args...)
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert categories")
}
}
return nil
}
type userMembership struct {
UserId string
ChannelId string
CategoryId string
}
func (s SqlChannelStore) migrateMembershipToSidebar(transaction *sqlxTxWrapper, runningOrder *int64, sql string, args ...any) ([]userMembership, error) {
memberships := []userMembership{}
if err := transaction.Select(&memberships, sql, args...); err != nil {
return nil, err
}
for _, favorite := range memberships {
sql, args, err := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder").
Values(favorite.ChannelId, favorite.UserId, favorite.CategoryId, *runningOrder).ToSql()
if err != nil {
return nil, err
}
if _, err := transaction.Exec(sql, args...); err != nil && !IsUniqueConstraintError(err, []string{"UserId", "PRIMARY"}) {
return nil, err
}
*runningOrder = *runningOrder + model.MinimalSidebarSortDistance
}
if err := transaction.Commit(); err != nil {
return nil, err
}
return memberships, nil
}
func (s SqlChannelStore) migrateFavoritesToSidebarT(transaction *sqlxTxWrapper, userId, teamId, favoritesCategoryId string) error {
favoritesQuery, favoritesParams, err := s.getQueryBuilder().
Select("Preferences.Name").
From("Preferences").
Join("Channels on Preferences.Name = Channels.Id").
Join("ChannelMembers on Preferences.Name = ChannelMembers.ChannelId and Preferences.UserId = ChannelMembers.UserId").
Where(sq.Eq{
"Preferences.UserId": userId,
"Preferences.Category": model.PreferenceCategoryFavoriteChannel,
"Preferences.Value": "true",
}).
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId},
sq.Eq{"Channels.TeamId": ""},
}).
OrderBy(
"Channels.DisplayName",
"Channels.Name ASC",
).ToSql()
if err != nil {
return err
}
favoriteChannelIds := []string{}
if err := transaction.Select(&favoriteChannelIds, favoritesQuery, favoritesParams...); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to get favorite channel IDs")
}
for i, channelId := range favoriteChannelIds {
if _, err := transaction.NamedExec(`INSERT INTO
SidebarChannels(ChannelId, UserId, CategoryId, SortOrder)
VALUES(:ChannelId, :UserId, :CategoryId, :SortOrder)`, &model.SidebarChannel{
ChannelId: channelId,
CategoryId: favoritesCategoryId,
UserId: userId,
SortOrder: int64(i * model.MinimalSidebarSortDistance),
}); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to insert SidebarChannel")
}
}
return nil
}
// MigrateFavoritesToSidebarChannels populates the SidebarChannels table by analyzing existing user preferences for favorites
// **IMPORTANT** This function should only be called from the migration task and shouldn't be used by itself
func (s SqlChannelStore) MigrateFavoritesToSidebarChannels(lastUserId string, runningOrder int64) (_ map[string]any, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
sb := s.
getQueryBuilder().
Select("Preferences.UserId", "Preferences.Name AS ChannelId", "SidebarCategories.Id AS CategoryId").
From("Preferences").
Where(sq.And{
sq.Eq{"Preferences.Category": model.PreferenceCategoryFavoriteChannel},
sq.NotEq{"Preferences.Value": "false"},
sq.NotEq{"SidebarCategories.Id": nil},
sq.Gt{"Preferences.UserId": lastUserId},
}).
LeftJoin("Channels ON (Channels.Id=Preferences.Name)").
LeftJoin("SidebarCategories ON (SidebarCategories.UserId=Preferences.UserId AND SidebarCategories.Type='"+string(model.SidebarCategoryFavorites)+"' AND (SidebarCategories.TeamId=Channels.TeamId OR Channels.TeamId=''))").
OrderBy("Preferences.UserId", "Channels.Name DESC").
Limit(100)
sql, args, err := sb.ToSql()
if err != nil {
return nil, err
}
userFavorites, err := s.migrateMembershipToSidebar(transaction, &runningOrder, sql, args...)
if err != nil {
return nil, err
}
if len(userFavorites) == 0 {
return nil, nil
}
data := make(map[string]any)
data["UserId"] = userFavorites[len(userFavorites)-1].UserId
data["SortOrder"] = runningOrder
return data, nil
}
type sidebarCategoryForJoin struct {
model.SidebarCategory
ChannelId *string
}
func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
categoriesWithOrder, err := s.getSidebarCategoriesT(transaction, userId, opts)
if err != nil {
return nil, err
} else if len(categoriesWithOrder.Categories) == 0 {
return nil, store.NewErrNotFound("categories not found", fmt.Sprintf("userId=%s,teamId=%s", userId, teamId))
}
newOrder := categoriesWithOrder.Order
newCategoryId := model.NewId()
newCategorySortOrder := 0
/*
When a new category is created, it should be placed as follows:
1. If the Favorites category is first, the new category should be placed after it
2. Otherwise, the new category should be placed first.
*/
if categoriesWithOrder.Categories[0].Type == model.SidebarCategoryFavorites {
newOrder = append([]string{newOrder[0], newCategoryId}, newOrder[1:]...)
newCategorySortOrder = model.MinimalSidebarSortDistance
} else {
newOrder = append([]string{newCategoryId}, newOrder...)
}
category := &model.SidebarCategory{
DisplayName: newCategory.DisplayName,
Id: newCategoryId,
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortDefault,
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
Type: model.SidebarCategoryCustom,
Muted: newCategory.Muted,
}
if _, err2 := transaction.NamedExec(`INSERT INTO
SidebarCategories(Id, UserId, TeamId, SortOrder, Sorting, Type, DisplayName, Muted, Collapsed)
VALUES(:Id, :UserId, :TeamId, :SortOrder, :Sorting, :Type, :DisplayName, :Muted, :Collapsed)`, category); err2 != nil {
return nil, errors.Wrap(err2, "failed to save SidebarCategory")
}
if len(newCategory.Channels) > 0 {
placeHolder, channelIdArgs := constructArrayArgs(newCategory.Channels)
// Remove any channels from their previous categories and add them to the new one
var deleteQuery string
if s.DriverName() == model.DatabaseDriverMysql {
deleteQuery = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId IN ` + placeHolder + `
AND SidebarCategories.TeamId = ?`
} else {
deleteQuery = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId IN ` + placeHolder + `
AND SidebarCategories.TeamId = ?`
}
args := []any{userId}
args = append(args, channelIdArgs...)
args = append(args, teamId)
_, err = transaction.Exec(deleteQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to delete SidebarChannels")
}
insertQuery := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
for i, channelID := range newCategory.Channels {
insertQuery = insertQuery.Values(channelID, userId, newCategoryId, int64(i*model.MinimalSidebarSortDistance))
}
sql, args, err := insertQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "InsertSidebarChannels_Tosql")
}
if _, err := transaction.Exec(sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to save SidebarChannels")
}
}
// now we re-order the categories according to the new order
if err := s.updateSidebarCategoryOrderT(transaction, newOrder); err != nil {
return nil, err
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
// patch category to return proper sort order
category.SortOrder = int64(newCategorySortOrder)
result := &model.SidebarCategoryWithChannels{
SidebarCategory: *category,
Channels: newCategory.Channels,
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
result, err := s.completePopulatingCategoryChannelsT(transaction, category)
if err != nil {
return nil, err
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites {
return category, nil
}
var channelTypeFilter sq.Sqlizer
if category.Type == model.SidebarCategoryDirectMessages {
// any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category
channelTypeFilter = sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}}
} else if category.Type == model.SidebarCategoryChannels {
// any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category
channelTypeFilter = sq.And{
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeOpen, model.ChannelTypePrivate}},
sq.Eq{"Channels.TeamId": category.TeamId},
}
} else {
return nil, fmt.Errorf("invalid category type: %q", category.Type)
}
// A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team
doesNotHaveSidebarChannel := sq.Select("1").
Prefix("NOT EXISTS (").
From("SidebarChannels").
Join("SidebarCategories on SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.And{
sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"),
sq.Eq{"SidebarCategories.UserId": category.UserId},
sq.Eq{"SidebarCategories.TeamId": category.TeamId},
}).
Suffix(")")
channels := []string{}
sql, args, err := s.getQueryBuilder().
Select("Id").
From("ChannelMembers").
LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId").
Where(sq.And{
sq.Eq{"ChannelMembers.UserId": category.UserId},
channelTypeFilter,
sq.Eq{"Channels.DeleteAt": 0},
doesNotHaveSidebarChannel,
}).
OrderBy("DisplayName ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
if err := db.Select(&channels, sql, args...); err != nil {
return nil, store.NewErrNotFound("ChannelMembers", "<too many fields>").Wrap(err)
}
category.Channels = append(channels, category.Channels...)
return category, nil
}
func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) {
sql, args, err := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.Eq{"SidebarCategories.Id": categoryId}).
OrderBy("SidebarChannels.SortOrder ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_category_tosql")
}
categories := []*sidebarCategoryForJoin{}
if err = s.GetReplicaX().Select(&categories, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", categoryId).Wrap(err)
}
if len(categories) == 0 {
return nil, store.NewErrNotFound("SidebarCategories", categoryId)
}
result := &model.SidebarCategoryWithChannels{
SidebarCategory: categories[0].SidebarCategory,
Channels: make([]string, 0),
}
for _, category := range categories {
if category.ChannelId != nil {
result.Channels = append(result.Channels, *category.ChannelId)
}
}
return s.completePopulatingCategoryChannels(result)
}
func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
oc := model.OrderedSidebarCategories{
Categories: make(model.SidebarCategoriesWithChannels, 0),
Order: make([]string, 0),
}
categories := []*sidebarCategoryForJoin{}
query := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id").
InnerJoin("Teams ON Teams.Id=SidebarCategories.TeamId").
InnerJoin("TeamMembers ON TeamMembers.TeamId=SidebarCategories.TeamId").
Where(sq.And{
sq.Eq{"TeamMembers.UserId": userId},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
Where(sq.And{
sq.Eq{"SidebarCategories.UserId": userId},
}).
OrderBy("SidebarCategories.SortOrder ASC, SidebarChannels.SortOrder ASC")
if opts.ExcludeTeam {
query = query.Where(sq.NotEq{"SidebarCategories.TeamId": opts.TeamID})
} else {
query = query.Where(sq.Eq{"SidebarCategories.TeamId": opts.TeamID})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_categories_tosql")
}
if err := db.Select(&categories, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", fmt.Sprintf("userId=%s,teamId=%s", userId, opts.TeamID)).Wrap(err)
}
for _, category := range categories {
var prevCategory *model.SidebarCategoryWithChannels
for _, existing := range oc.Categories {
if existing.Id == category.Id {
prevCategory = existing
break
}
}
if prevCategory == nil {
prevCategory = &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
Channels: make([]string, 0),
}
oc.Categories = append(oc.Categories, prevCategory)
oc.Order = append(oc.Order, category.Id)
}
if category.ChannelId != nil {
prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId)
}
}
for _, category := range oc.Categories {
if _, err := s.completePopulatingCategoryChannelsT(db, category); err != nil {
return nil, err
}
}
return &oc, nil
}
func (s SqlChannelStore) GetSidebarCategoriesForTeamForUser(userId, teamId string) (*model.OrderedSidebarCategories, error) {
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
return s.getSidebarCategoriesT(s.GetReplicaX(), userId, opts)
}
func (s SqlChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
return s.getSidebarCategoriesT(s.GetReplicaX(), userID, opts)
}
func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, error) {
ids := []string{}
sql, args, err := s.getQueryBuilder().
Select("Id").
From("SidebarCategories").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"TeamId": teamId},
}).
OrderBy("SidebarCategories.SortOrder ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_category_tosql")
}
if err := s.GetReplicaX().Select(&ids, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", fmt.Sprintf("userId=%s,teamId=%s", userId, teamId)).Wrap(err)
}
return ids, nil
}
func (s SqlChannelStore) updateSidebarCategoryOrderT(transaction *sqlxTxWrapper, categoryOrder []string) error {
runningOrder := 0
for _, categoryId := range categoryOrder {
sql, args, err := s.getQueryBuilder().
Update("SidebarCategories").
Set("SortOrder", runningOrder).
Where(sq.Eq{"Id": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "updateSidebarCategoryOrderT_Tosql")
}
if _, err := transaction.Exec(sql, args...); err != nil {
return errors.Wrap(err, "Error updating sidebar category order")
}
runningOrder += model.MinimalSidebarSortDistance
}
return nil
}
func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
// Ensure no invalid categories are included and that no categories are left out
existingOrder, err := s.GetSidebarCategoryOrder(userId, teamId)
if err != nil {
return err
}
if len(existingOrder) != len(categoryOrder) {
return errors.New("cannot update category order, passed list of categories different size than in DB")
}
for _, originalCategoryId := range existingOrder {
found := false
for _, newCategoryId := range categoryOrder {
if newCategoryId == originalCategoryId {
found = true
break
}
}
if !found {
return store.NewErrInvalidInput("SidebarCategories", "id", fmt.Sprintf("%v", categoryOrder))
}
}
if err := s.updateSidebarCategoryOrderT(transaction, categoryOrder); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
//nolint:unparam
func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) (updated []*model.SidebarCategoryWithChannels, original []*model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedCategories := []*model.SidebarCategoryWithChannels{}
originalCategories := []*model.SidebarCategoryWithChannels{}
for _, category := range categories {
srcCategory, err2 := s.GetSidebarCategory(category.Id)
if err2 != nil {
return nil, nil, errors.Wrap(err2, "failed to find SidebarCategories")
}
// Copy category to avoid modifying an argument
destCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
}
// Prevent any changes to read-only fields of SidebarCategories
destCategory.UserId = srcCategory.UserId
destCategory.TeamId = srcCategory.TeamId
destCategory.SortOrder = srcCategory.SortOrder
destCategory.Type = srcCategory.Type
destCategory.Muted = srcCategory.Muted
if destCategory.Type != model.SidebarCategoryCustom {
destCategory.DisplayName = srcCategory.DisplayName
}
if destCategory.Type != model.SidebarCategoryDirectMessages {
destCategory.Channels = make([]string, len(category.Channels))
copy(destCategory.Channels, category.Channels)
destCategory.Muted = category.Muted
}
// The order in which the queries are executed in the transaction is important.
// SidebarCategories need to be update first, and then SidebarChannels should be deleted.
// The net effect remains the same, but it prevents deadlocks from other transactions
// operating on the tables in reverse order.
updateQuery, updateParams, err2 := s.getQueryBuilder().
Update("SidebarCategories").
Set("DisplayName", destCategory.DisplayName).
Set("Sorting", destCategory.Sorting).
Set("Muted", destCategory.Muted).
Set("Collapsed", destCategory.Collapsed).
Where(sq.Eq{"Id": destCategory.Id}).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql1")
}
if _, err = transaction.Exec(updateQuery, updateParams...); err != nil {
return nil, nil, errors.Wrap(err, "failed to update SidebarCategories")
}
// if we are updating DM category, it's order can't channel order cannot be changed.
if category.Type != model.SidebarCategoryDirectMessages {
// Remove any SidebarChannels entries that were either:
// - previously in this category (and any ones that are still in the category will be recreated below)
// - in another category and are being added to this category
query, args, err2 := s.getQueryBuilder().
Delete("SidebarChannels").
Where(
sq.And{
sq.Eq{"ChannelId": srcCategory.Channels},
sq.Eq{"CategoryId": category.Id},
},
).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql2")
}
if _, err = transaction.Exec(query, args...); err != nil {
return nil, nil, errors.Wrap(err, "failed to delete SidebarChannels")
}
runningOrder := 0
insertQuery := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
for _, channelID := range category.Channels {
insertQuery = insertQuery.Values(channelID, userId, category.Id, int64(runningOrder))
runningOrder += model.MinimalSidebarSortDistance
}
if len(category.Channels) > 0 {
sql, args, err2 := insertQuery.ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "InsertSidebarChannels_Tosql")
}
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
return nil, nil, errors.Wrap(err2, "failed to save SidebarChannels")
}
}
}
// Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility
if category.Type == model.SidebarCategoryFavorites {
// Remove any old favorites
sql, args, err2 := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": srcCategory.Channels,
"Category": model.PreferenceCategoryFavoriteChannel,
},
).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "UpdateSidebarChannels_Tosql_DeletePreferences")
}
if _, err = transaction.Exec(sql, args...); err != nil {
return nil, nil, errors.Wrap(err, "failed to delete Preferences")
}
// And then add the new ones
for _, channelID := range category.Channels {
// This breaks the PreferenceStore abstraction, but it should be safe to assume that everything is a SQL
// store in this package.
if err = s.Preference().(*SqlPreferenceStore).save(transaction, &model.Preference{
Name: channelID,
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
}); err != nil {
return nil, nil, errors.Wrap(err, "failed to save Preference")
}
}
} else {
// Remove any old favorites that might have been in this category
query, args, nErr := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": category.Channels,
"Category": model.PreferenceCategoryFavoriteChannel,
},
).ToSql()
if nErr != nil {
return nil, nil, errors.Wrap(nErr, "update_sidebar_categories_tosql")
}
if _, nErr = transaction.Exec(query, args...); nErr != nil {
return nil, nil, errors.Wrap(nErr, "failed to delete Preferences")
}
}
updatedCategories = append(updatedCategories, destCategory)
originalCategories = append(originalCategories, srcCategory)
}
// Ensure Channels are populated for Channels/Direct Messages category if they change
for i, updatedCategory := range updatedCategories {
populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory)
if nErr != nil {
return nil, nil, nErr
}
updatedCategories[i] = populated
}
if err = transaction.Commit(); err != nil {
return nil, nil, errors.Wrap(err, "commit_transaction")
}
return updatedCategories, originalCategories, nil
}
// UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if preference.Category != model.PreferenceCategoryFavoriteChannel {
continue
}
// if new preference is false - remove the channel from the appropriate sidebar category
if preference.Value == "false" {
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
} else {
if err := s.addChannelToFavoritesCategoryT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: addChannelToFavoritesCategoryT")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
func (s SqlChannelStore) removeSidebarEntriesForPreferenceT(transaction *sqlxTxWrapper, preference *model.Preference) error {
if preference.Category != model.PreferenceCategoryFavoriteChannel {
return nil
}
// Delete any corresponding SidebarChannels entries in a Favorites category corresponding to this preference.
var query string
if s.DriverName() == model.DatabaseDriverMysql {
query = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId = ?
AND SidebarCategories.Type = ?`
} else {
query = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId = ?
AND SidebarCategories.Type = ?`
}
if _, err := transaction.Exec(query, preference.UserId, preference.Name, model.SidebarCategoryFavorites); err != nil {
return errors.Wrap(err, "Failed to remove sidebar entries for preference")
}
return nil
}
func (s SqlChannelStore) addChannelToFavoritesCategoryT(transaction *sqlxTxWrapper, preference *model.Preference) error {
if preference.Category != model.PreferenceCategoryFavoriteChannel {
return nil
}
var channel model.Channel
if err := transaction.Get(&channel, `SELECT * FROM Channels WHERE Id=?`, preference.Name); err != nil {
return errors.Wrapf(err, "Failed to get favorited channel with id=%s", preference.Name)
} else if channel.Id == "" {
return store.NewErrNotFound("Channel", preference.Name)
}
// Get the IDs of the Favorites category/categories that the channel needs to be added to
builder := s.getQueryBuilder().
Select("SidebarCategories.Id").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId and SidebarChannels.ChannelId = ?", preference.Name).
Where(sq.Eq{
"SidebarCategories.UserId": preference.UserId,
"Type": model.SidebarCategoryFavorites,
}).
Where("SidebarChannels.ChannelId is null")
if channel.TeamId != "" {
builder = builder.Where(sq.Eq{"TeamId": channel.TeamId})
}
idsQuery, idsParams, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "addChannelToFavoritesCategoryT_ToSql_Select")
}
categoryIds := []string{}
if err = transaction.Select(&categoryIds, idsQuery, idsParams...); err != nil {
return errors.Wrap(err, "Failed to get Favorites sidebar categories")
}
if len(categoryIds) == 0 {
// The channel is already in the Favorites category/categories
return nil
}
// For each category ID, insert a row into SidebarChannels with the given channel ID and a SortOrder that's less than
// all existing SortOrders in the category so that the newly favorited channel comes first
insertQuery, insertParams, err := s.getQueryBuilder().
Insert("SidebarChannels").
Columns(
"ChannelId",
"CategoryId",
"UserId",
"SortOrder",
).
Select(
sq.Select().
Column("? as ChannelId", preference.Name).
Column("SidebarCategories.Id as CategoryId").
Column("? as UserId", preference.UserId).
Column("COALESCE(MIN(SidebarChannels.SortOrder) - 10, 0) as SortOrder").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId").
Where(sq.Eq{
"SidebarCategories.Id": categoryIds,
}).
GroupBy("SidebarCategories.Id")).ToSql()
if err != nil {
return errors.Wrap(err, "addChannelToFavoritesCategoryT_ToSql_Insert")
}
if _, err := transaction.Exec(insertQuery, insertParams...); err != nil {
return errors.Wrap(err, "Failed to add sidebar entries for favorited channel")
}
return nil
}
// DeleteSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if preference.Category != model.PreferenceCategoryFavoriteChannel {
continue
}
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
//nolint:unparam
func (s SqlChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamId string) error {
// if channel is being moved, remove it from the categories, since it's possible that there's no matching category in the new team
if _, err := s.GetMasterX().Exec("DELETE FROM SidebarChannels WHERE ChannelId=?", channel.Id); err != nil {
return errors.Wrapf(err, "failed to delete SidebarChannels with channelId=%s", channel.Id)
}
return nil
}
func (s SqlChannelStore) ClearSidebarOnTeamLeave(userId, teamId string) error {
// if user leaves the team, clean their team related entries in sidebar channels and categories
var deleteQuery string
if s.DriverName() == model.DatabaseDriverMysql {
deleteQuery = "DELETE SidebarChannels FROM SidebarChannels LEFT JOIN SidebarCategories ON SidebarCategories.Id = SidebarChannels.CategoryId WHERE SidebarCategories.TeamId=? AND SidebarCategories.UserId=?"
} else {
deleteQuery = `
DELETE FROM
SidebarChannels
WHERE
CategoryId IN (
SELECT
CategoryId
FROM
SidebarChannels,
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarCategories.TeamId = ?
AND SidebarChannels.UserId = ?)`
}
if _, err := s.GetMasterX().Exec(deleteQuery, teamId, userId); err != nil {
return errors.Wrap(err, "failed to delete from SidebarChannels")
}
if _, err := s.GetMasterX().Exec("DELETE FROM SidebarCategories WHERE SidebarCategories.TeamId = ? AND SidebarCategories.UserId = ?", teamId, userId); err != nil {
return errors.Wrap(err, "failed to delete from SidebarCategories")
}
return nil
}
// DeleteSidebarCategory removes a custom category and moves any channels into it into the Channels and Direct Messages
// categories respectively. Assumes that the provided user ID and team ID match the given category ID.
func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
// Ensure that we're deleting a custom category
var category model.SidebarCategory
if err = transaction.Get(&category, "SELECT * FROM SidebarCategories WHERE Id = ?", categoryId); err != nil {
return errors.Wrapf(err, "failed to find SidebarCategories with id=%s", categoryId)
}
if category.Type != model.SidebarCategoryCustom {
return store.NewErrInvalidInput("SidebarCategory", "id", categoryId)
}
// The order in which the queries are executed in the transaction is important.
// SidebarCategories need to be deleted first, and then SidebarChannels.
// The net effect remains the same, but it prevents deadlocks from other transactions
// operating on the tables in reverse order.
// Delete the category itself
query, args, err := s.getQueryBuilder().
Delete("SidebarCategories").
Where(sq.Eq{"Id": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "delete_sidebar_category_tosql")
}
if _, err = transaction.Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to delete SidebarCategory")
}
// Delete the channels in the category
query, args, err = s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.Eq{"CategoryId": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "delete_sidebar_category_tosql")
}
if _, err = transaction.Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to delete SidebarChannel")
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type sqlClusterDiscoveryStore struct {
*SqlStore
}
func newSqlClusterDiscoveryStore(sqlStore *SqlStore) store.ClusterDiscoveryStore {
return &sqlClusterDiscoveryStore{sqlStore}
}
func (s sqlClusterDiscoveryStore) Save(ClusterDiscovery *model.ClusterDiscovery) error {
ClusterDiscovery.PreSave()
if err := ClusterDiscovery.IsValid(); err != nil {
return err
}
if _, err := s.GetMasterX().NamedExec(`
INSERT INTO
ClusterDiscovery
(Id, Type, ClusterName, Hostname, GossipPort, Port, CreateAt, LastPingAt)
VALUES
(:Id, :Type, :ClusterName, :Hostname, :GossipPort, :Port, :CreateAt, :LastPingAt)
`, ClusterDiscovery); err != nil {
return errors.Wrap(err, "failed to save ClusterDiscovery")
}
return nil
}
func (s sqlClusterDiscoveryStore) Delete(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
query := s.getQueryBuilder().
Delete("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "cluster_discovery_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete ClusterDiscovery")
}
count, err := res.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to count rows affected")
}
return count != 0, nil
}
func (s sqlClusterDiscoveryStore) Exists(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "cluster_discovery_tosql")
}
var count int
if err := s.GetMasterX().Get(&count, queryString, args...); err != nil {
return false, errors.Wrap(err, "failed to count ClusterDiscovery")
}
return count != 0, nil
}
func (s sqlClusterDiscoveryStore) GetAll(ClusterDiscoveryType, clusterName string) ([]*model.ClusterDiscovery, error) {
query := s.getQueryBuilder().
Select("*").
From("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscoveryType}).
Where(sq.Eq{"ClusterName": clusterName}).
Where(sq.Gt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "cluster_discovery_tosql")
}
list := []*model.ClusterDiscovery{}
if err := s.GetMasterX().Select(&list, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ClusterDiscovery")
}
return list, nil
}
func (s sqlClusterDiscoveryStore) SetLastPingAt(ClusterDiscovery *model.ClusterDiscovery) error {
query := s.getQueryBuilder().
Update("ClusterDiscovery").
Set("LastPingAt", model.GetMillis()).
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "cluster_discovery_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to update ClusterDiscovery")
}
return nil
}
func (s sqlClusterDiscoveryStore) Cleanup() error {
query := s.getQueryBuilder().
Delete("ClusterDiscovery").
Where(sq.Lt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "cluster_discovery_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to delete ClusterDiscoveries")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlCommandStore struct {
*SqlStore
commandsQuery sq.SelectBuilder
}
func newSqlCommandStore(sqlStore *SqlStore) store.CommandStore {
s := &SqlCommandStore{SqlStore: sqlStore}
s.commandsQuery = s.getQueryBuilder().
Select("*").
From("Commands")
return s
}
func (s SqlCommandStore) Save(command *model.Command) (*model.Command, error) {
if command.Id != "" {
return nil, store.NewErrInvalidInput("Command", "CommandId", command.Id)
}
command.PreSave()
if err := command.IsValid(); err != nil {
return nil, err
}
// Trigger is a keyword
trigger := s.toReserveCase("trigger")
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Commands (Id, Token, CreateAt,
UpdateAt, DeleteAt, CreatorId, TeamId, `+trigger+`, Method, Username,
IconURL, AutoComplete, AutoCompleteDesc, AutoCompleteHint, DisplayName, Description,
URL, PluginId)
VALUES (:Id, :Token, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :TeamId, :Trigger, :Method,
:Username, :IconURL, :AutoComplete, :AutoCompleteDesc, :AutoCompleteHint, :DisplayName,
:Description, :URL, :PluginId)`, command); err != nil {
return nil, errors.Wrapf(err, "insert: command_id=%s", command.Id)
}
return command, nil
}
func (s SqlCommandStore) Get(id string) (*model.Command, error) {
var command model.Command
query, args, err := s.commandsQuery.
Where(sq.Eq{"Id": id, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err = s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Command", id)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: command_id=%s", id)
}
return &command, nil
}
func (s SqlCommandStore) GetByTeam(teamId string) ([]*model.Command, error) {
commands := []*model.Command{}
sql, args, err := s.commandsQuery.
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err := s.GetReplicaX().Select(&commands, sql, args...); err != nil {
return nil, errors.Wrapf(err, "select: team_id=%s", teamId)
}
return commands, nil
}
func (s SqlCommandStore) GetByTrigger(teamId string, trigger string) (*model.Command, error) {
var command model.Command
var triggerStr string
if s.DriverName() == "mysql" {
triggerStr = "`Trigger`"
} else {
triggerStr = "\"trigger\""
}
query, args, err := s.commandsQuery.
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0, triggerStr: trigger}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err := s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
errorId := "teamId=" + teamId + ", trigger=" + trigger
return nil, store.NewErrNotFound("Command", errorId)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: team_id=%s, trigger=%s", teamId, trigger)
}
return &command, nil
}
func (s SqlCommandStore) Delete(commandId string, time int64) error {
sql, args, err := s.getQueryBuilder().
Update("Commands").
SetMap(sq.Eq{"DeleteAt": time, "UpdateAt": time}).
Where(sq.Eq{"Id": commandId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
errors.Wrapf(err, "delete: command_id=%s", commandId)
}
return nil
}
func (s SqlCommandStore) PermanentDeleteByTeam(teamId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Commands").
Where(sq.Eq{"TeamId": teamId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "delete: team_id=%s", teamId)
}
return nil
}
func (s SqlCommandStore) PermanentDeleteByUser(userId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Commands").
Where(sq.Eq{"CreatorId": userId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "delete: user_id=%s", userId)
}
return nil
}
func (s SqlCommandStore) Update(cmd *model.Command) (*model.Command, error) {
cmd.UpdateAt = model.GetMillis()
if err := cmd.IsValid(); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Commands").
Set("Token", cmd.Token).
Set("CreateAt", cmd.CreateAt).
Set("UpdateAt", cmd.UpdateAt).
Set("CreatorId", cmd.CreatorId).
Set("TeamId", cmd.TeamId).
Set("Method", cmd.Method).
Set("Username", cmd.Username).
Set("IconURL", cmd.IconURL).
Set("AutoComplete", cmd.AutoComplete).
Set("AutoCompleteDesc", cmd.AutoCompleteDesc).
Set("AutoCompleteHint", cmd.AutoCompleteHint).
Set("DisplayName", cmd.DisplayName).
Set("Description", cmd.Description).
Set("URL", cmd.URL).
Set("PluginId", cmd.PluginId).
Where(sq.Eq{"Id": cmd.Id})
// Trigger is a keyword
query = query.Set(s.toReserveCase("trigger"), cmd.Trigger)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "commands_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update commands")
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating commands: count=%d, Id=%s", count, cmd.Id)
}
return cmd, nil
}
func (s SqlCommandStore) AnalyticsCommandCount(teamId string) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("Commands").
Where(sq.Eq{"DeleteAt": 0})
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrapf(err, "commands_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "unable to count the commands: team_id=%s", teamId)
}
return c, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlCommandWebhookStore struct {
*SqlStore
}
func newSqlCommandWebhookStore(sqlStore *SqlStore) store.CommandWebhookStore {
return &SqlCommandWebhookStore{sqlStore}
}
func (s SqlCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("CommandWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO CommandWebhooks
(Id,CreateAt,CommandId,UserId,ChannelId,RootId,UseCount)
Values
(:Id, :CreateAt, :CommandId, :UserId, :ChannelId, :RootId, :UseCount)`, webhook); err != nil {
return nil, errors.Wrapf(err, "save: id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
var webhook model.CommandWebhook
exptime := model.GetMillis() - model.CommandWebhookLifetime
query := s.getQueryBuilder().
Select("*").
From("CommandWebhooks").
Where(sq.Eq{"Id": id}).
Where(sq.Gt{"CreateAt": exptime})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_tosql")
}
if err := s.GetReplicaX().Get(&webhook, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("CommandWebhook", id)
}
return nil, errors.Wrapf(err, "get: id=%s", id)
}
return &webhook, nil
}
func (s SqlCommandWebhookStore) TryUse(id string, limit int) error {
query := s.getQueryBuilder().
Update("CommandWebhooks").
Set("UseCount", sq.Expr("UseCount + 1")).
Where(sq.Eq{"Id": id}).
Where(sq.Lt{"UseCount": limit})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "tryuse_tosql")
}
if sqlResult, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "tryuse: id=%s limit=%d", id, limit)
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
return store.NewErrInvalidInput("CommandWebhook", "id", id).Wrap(err)
}
return nil
}
func (s SqlCommandWebhookStore) Cleanup() {
mlog.Debug("Cleaning up command webhook store.")
exptime := model.GetMillis() - model.CommandWebhookLifetime
query := s.getQueryBuilder().
Delete("CommandWebhooks").
Where(sq.Lt{"CreateAt": exptime})
queryString, args, err := query.ToSql()
if err != nil {
mlog.Error("Failed to build query when trying to perform a cleanup in command webhook store.")
return
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
mlog.Error("Unable to cleanup command webhook store.")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlComplianceStore struct {
*SqlStore
}
func newSqlComplianceStore(sqlStore *SqlStore) store.ComplianceStore {
return &SqlComplianceStore{sqlStore}
}
func (s SqlComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
compliance.PreSave()
if err := compliance.IsValid(); err != nil {
return nil, err
}
// DESC is a keyword
desc := s.toReserveCase("desc")
query := `INSERT INTO Compliances (Id, CreateAt, UserId, Status, Count, ` + desc + `, Type, StartAt, EndAt, Keywords, Emails)
VALUES
(:Id, :CreateAt, :UserId, :Status, :Count, :Desc, :Type, :StartAt, :EndAt, :Keywords, :Emails)`
if _, err := s.GetMasterX().NamedExec(query, compliance); err != nil {
return nil, errors.Wrap(err, "failed to save Compliance")
}
return compliance, nil
}
func (s SqlComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
if err := compliance.IsValid(); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Compliances").
Set("CreateAt", compliance.CreateAt).
Set("UserId", compliance.UserId).
Set("Status", compliance.Status).
Set("Count", compliance.Count).
Set("Type", compliance.Type).
Set("StartAt", compliance.StartAt).
Set("EndAt", compliance.EndAt).
Set("Keywords", compliance.Keywords).
Set("Emails", compliance.Emails).
Where(sq.Eq{"Id": compliance.Id})
// DESC is a keyword
query = query.Set(s.toReserveCase("desc"), compliance.Desc)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "compliances_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update Compliance")
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating compliances: count=%d, Id=%s", count, compliance.Id)
}
return compliance, nil
}
func (s SqlComplianceStore) GetAll(offset, limit int) (model.Compliances, error) {
query := "SELECT * FROM Compliances ORDER BY CreateAt DESC LIMIT ? OFFSET ?"
compliances := model.Compliances{}
if err := s.GetReplicaX().Select(&compliances, query, limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find all Compliances")
}
return compliances, nil
}
func (s SqlComplianceStore) Get(id string) (*model.Compliance, error) {
var compliance model.Compliance
if err := s.GetReplicaX().Get(&compliance, `SELECT * FROM Compliances WHERE Id = ?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Compliances", id)
}
return nil, errors.Wrapf(err, "failed to get Compliance with id=%s", id)
}
if compliance.Id == "" {
return nil, store.NewErrNotFound("Compliance", id)
}
return &compliance, nil
}
func (s SqlComplianceStore) ComplianceExport(job *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
keywordQuery := ""
var argsKeywords []any
keywords := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Keywords, ",", " ", -1))))
if len(keywords) > 0 {
clauses := make([]string, len(keywords))
for i, keyword := range keywords {
keyword = sanitizeSearchTerm(keyword, "\\")
clauses[i] = "LOWER(Posts.Message) LIKE ?"
argsKeywords = append(argsKeywords, "%"+keyword+"%")
}
keywordQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
}
emailQuery := ""
var argsEmails []any
emails := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Emails, ",", " ", -1))))
if len(emails) > 0 {
clauses := make([]string, len(emails))
for i, email := range emails {
clauses[i] = "Users.Email = ?"
argsEmails = append(argsEmails, email)
}
emailQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
}
// The idea is to first iterate over the channel posts, and then when we run out of those,
// start iterating over the direct message posts.
channelPosts := []*model.CompliancePost{}
channelsQuery := ""
var argsChannelsQuery []any
if !cursor.ChannelsQueryCompleted {
if cursor.LastChannelsQueryPostCreateAt == 0 {
cursor.LastChannelsQueryPostCreateAt = job.StartAt
}
// append the named parameters of SQL query in the correct order to argsChannelsQuery
argsChannelsQuery = append(argsChannelsQuery, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostID, job.EndAt)
argsChannelsQuery = append(argsChannelsQuery, argsEmails...)
argsChannelsQuery = append(argsChannelsQuery, argsKeywords...)
argsChannelsQuery = append(argsChannelsQuery, limit)
channelsQuery = `
SELECT
Teams.Name AS TeamName,
Teams.DisplayName AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.RootId AS PostRootId,
Posts.OriginalId AS PostOriginalId,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.Hashtags AS PostHashtags,
Posts.FileIds AS PostFileIds,
Bots.UserId IS NOT NULL AS IsBot
FROM
Teams,
Channels,
Users,
Posts
LEFT JOIN
Bots ON Bots.UserId = Posts.UserId
WHERE
Teams.Id = Channels.TeamId
AND Posts.ChannelId = Channels.Id
AND Posts.UserId = Users.Id
AND (
Posts.CreateAt > ?
OR (Posts.CreateAt = ? AND Posts.Id > ?)
)
AND Posts.CreateAt < ?
` + emailQuery + `
` + keywordQuery + `
ORDER BY Posts.CreateAt, Posts.Id
LIMIT ?`
if err := s.GetReplicaX().Select(&channelPosts, channelsQuery, argsChannelsQuery...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export compliance")
}
if len(channelPosts) < limit {
cursor.ChannelsQueryCompleted = true
} else {
cursor.LastChannelsQueryPostCreateAt = channelPosts[len(channelPosts)-1].PostCreateAt
cursor.LastChannelsQueryPostID = channelPosts[len(channelPosts)-1].PostId
}
}
directMessagePosts := []*model.CompliancePost{}
directMessagesQuery := ""
var argsDirectMessagesQuery []any
if !cursor.DirectMessagesQueryCompleted && len(channelPosts) < limit {
if cursor.LastDirectMessagesQueryPostCreateAt == 0 {
cursor.LastDirectMessagesQueryPostCreateAt = job.StartAt
}
// append the named parameters of SQL query in the correct order to argsDirectMessagesQuery
argsDirectMessagesQuery = append(argsDirectMessagesQuery, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostID, job.EndAt)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsEmails...)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsKeywords...)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, limit-len(channelPosts))
directMessagesQuery = `
SELECT
'direct-messages' AS TeamName,
'Direct Messages' AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.RootId AS PostRootId,
Posts.OriginalId AS PostOriginalId,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.Hashtags AS PostHashtags,
Posts.FileIds AS PostFileIds,
Bots.UserId IS NOT NULL AS IsBot
FROM
Channels,
Users,
Posts
LEFT JOIN
Bots ON Bots.UserId = Posts.UserId
WHERE
Channels.TeamId = ''
AND Posts.ChannelId = Channels.Id
AND Posts.UserId = Users.Id
AND (
Posts.CreateAt > ?
OR (Posts.CreateAt = ? AND Posts.Id > ?)
)
AND Posts.CreateAt < ?
` + emailQuery + `
` + keywordQuery + `
ORDER BY Posts.CreateAt, Posts.Id
LIMIT ?`
if err := s.GetReplicaX().Select(&directMessagePosts, directMessagesQuery, argsDirectMessagesQuery...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export compliance")
}
if len(directMessagePosts) < limit {
cursor.DirectMessagesQueryCompleted = true
} else {
cursor.LastDirectMessagesQueryPostCreateAt = directMessagePosts[len(directMessagePosts)-1].PostCreateAt
cursor.LastDirectMessagesQueryPostID = directMessagePosts[len(directMessagePosts)-1].PostId
}
}
return append(channelPosts, directMessagePosts...), cursor, nil
}
func (s SqlComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
var args []any
args = append(args, model.ChannelTypeDirect, model.ChannelTypeGroup, cursor.LastPostUpdateAt, cursor.LastPostUpdateAt, cursor.LastPostId, limit)
query :=
`SELECT
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.OriginalId AS PostOriginalId,
Posts.RootId AS PostRootId,
Posts.FileIds AS PostFileIds,
Teams.Id AS TeamId,
Teams.Name AS TeamName,
Teams.DisplayName AS TeamDisplayName,
Channels.Id AS ChannelId,
CASE
WHEN Channels.Type = ? THEN 'Direct Message'
WHEN Channels.Type = ? THEN 'Group Message'
ELSE Channels.DisplayName
END AS ChannelDisplayName,
Channels.Name AS ChannelName,
Channels.Type AS ChannelType,
Users.Id AS UserId,
Users.Email AS UserEmail,
Users.Username,
Bots.UserId IS NOT NULL AS IsBot
FROM
Posts
LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id
LEFT OUTER JOIN Teams ON Channels.TeamId = Teams.Id
LEFT OUTER JOIN Users ON Posts.UserId = Users.Id
LEFT JOIN Bots ON Bots.UserId = Posts.UserId
WHERE (
Posts.UpdateAt > ?
OR (
Posts.UpdateAt = ?
AND Posts.Id > ?
)
) AND Posts.Type NOT LIKE 'system_%'
ORDER BY PostUpdateAt, PostId
LIMIT ?`
cposts := []*model.MessageExport{}
if err := s.GetReplicaX().SelectCtx(ctx, &cposts, query, args...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export messages")
}
if len(cposts) > 0 {
cursor.LastPostUpdateAt = *cposts[len(cposts)-1].PostUpdateAt
cursor.LastPostId = *cposts[len(cposts)-1].PostId
}
return cposts, cursor, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
)
// storeContextKey is the base type for all context keys for the store.
type storeContextKey string
// contextValue is a type to hold some pre-determined context values.
type contextValue string
// Different possible values of contextValue.
const (
useMaster contextValue = "useMaster"
)
// WithMaster adds the context value that master DB should be selected for this request.
func WithMaster(ctx context.Context) context.Context {
return context.WithValue(ctx, storeContextKey(useMaster), true)
}
// hasMaster is a helper function to check whether master DB should be selected or not.
func hasMaster(ctx context.Context) bool {
if v := ctx.Value(storeContextKey(useMaster)); v != nil {
if res, ok := v.(bool); ok && res {
return true
}
}
return false
}
// DBXFromContext is a helper utility that returns the sqlx DB handle from a given context.
func (ss *SqlStore) DBXFromContext(ctx context.Context) *sqlxDBWrapper {
if hasMaster(ctx) {
return ss.GetMasterX()
}
return ss.GetReplicaX()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"sync"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlDraftStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
maxDraftSizeOnce sync.Once
maxDraftSizeCached int
}
func draftSliceColumns() []string {
return []string{
"CreateAt",
"UpdateAt",
"DeleteAt",
"Message",
"RootId",
"ChannelId",
"UserId",
"FileIds",
"Props",
"Priority",
}
}
func draftToSlice(draft *model.Draft) []interface{} {
return []interface{}{
draft.CreateAt,
draft.UpdateAt,
draft.DeleteAt,
draft.Message,
draft.RootId,
draft.ChannelId,
draft.UserId,
model.ArrayToJSON(draft.FileIds),
model.StringInterfaceToJSON(draft.Props),
model.StringInterfaceToJSON(draft.Priority),
}
}
func newSqlDraftStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DraftStore {
return &SqlDraftStore{
SqlStore: sqlStore,
metrics: metrics,
maxDraftSizeCached: model.PostMessageMaxRunesV1,
}
}
func (s *SqlDraftStore) Get(userId, channelId, rootId string, includeDeleted bool) (*model.Draft, error) {
query := s.getQueryBuilder().
Select(draftSliceColumns()...).
From("Drafts").
Where(sq.Eq{
"UserId": userId,
"ChannelId": channelId,
"RootId": rootId,
})
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
dt := model.Draft{}
err := s.GetReplicaX().GetBuilder(&dt, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Draft", channelId)
}
return nil, errors.Wrapf(err, "failed to find draft with channelid = %s", channelId)
}
return &dt, nil
}
func (s *SqlDraftStore) Save(draft *model.Draft) (*model.Draft, error) {
draft.PreSave()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
builder := s.getQueryBuilder().Insert("Drafts").Columns(draftSliceColumns()...).Values(draftToSlice(draft)...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "save_draft_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save Draft")
}
return draft, nil
}
func (s *SqlDraftStore) Update(draft *model.Draft) (*model.Draft, error) {
draft.PreUpdate()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", draft.UpdateAt).
Set("Message", draft.Message).
Set("Props", draft.Props).
Set("FileIds", draft.FileIds).
Set("Priority", draft.Priority).
Set("DeleteAt", 0).
Where(sq.Eq{
"UserId": draft.UserId,
"ChannelId": draft.ChannelId,
"RootId": draft.RootId,
})
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update Draft with channelid=%s", draft.ChannelId)
}
return draft, nil
}
func (s *SqlDraftStore) GetDraftsForUser(userID, teamID string) ([]*model.Draft, error) {
var drafts []*model.Draft
query := s.getQueryBuilder().
Select(
"Drafts.CreateAt",
"Drafts.UpdateAt",
"Drafts.Message",
"Drafts.RootId",
"Drafts.ChannelId",
"Drafts.UserId",
"Drafts.FileIds",
"Drafts.Props",
"Drafts.Priority",
).
From("Drafts").
InnerJoin("ChannelMembers ON ChannelMembers.ChannelId = Drafts.ChannelId").
Where(sq.And{
sq.Eq{"Drafts.DeleteAt": 0},
sq.Eq{"Drafts.UserId": userID},
sq.Eq{"ChannelMembers.UserId": userID},
}).
OrderBy("Drafts.UpdateAt DESC")
if teamID != "" {
query = query.
Join("Channels ON Drafts.ChannelId = Channels.Id").
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamID},
sq.Eq{"Channels.TeamId": ""},
})
}
err := s.GetReplicaX().SelectBuilder(&drafts, query)
if err != nil {
return nil, errors.Wrap(err, "failed to get user drafts")
}
return drafts, nil
}
func (s *SqlDraftStore) Delete(userID, channelID, rootID string) error {
time := model.GetMillis()
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", time).
Set("DeleteAt", time).
Where(sq.Eq{
"UserId": userID,
"ChannelId": channelID,
"RootId": rootID,
})
sql, args, err := query.ToSql()
if err != nil {
return errors.Wrapf(err, "failed to convert to sql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrap(err, "failed to delete Draft")
}
return nil
}
// GetMaxDraftSize returns the maximum number of runes that may be stored in a post.
func (s *SqlDraftStore) GetMaxDraftSize() int {
s.maxDraftSizeOnce.Do(func() {
s.maxDraftSizeCached = s.determineMaxDraftSize()
})
return s.maxDraftSizeCached
}
func (s *SqlDraftStore) determineMaxDraftSize() int {
var maxDraftSizeBytes int32
if s.DriverName() == model.DatabaseDriverPostgres {
// The Draft.Message column in Postgres has historically been VARCHAR(4000), but
// may be manually enlarged to support longer drafts.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
table_name = 'drafts'
AND column_name = 'message'
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else if s.DriverName() == model.DatabaseDriverMysql {
// The Draft.Message column in MySQL has historically been TEXT, with a maximum
// limit of 65535.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND table_name = 'Drafts'
AND column_name = 'Message'
LIMIT 0, 1
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else {
mlog.Warn("No implementation found to determine the maximum supported draft size")
}
// Assume a worst-case representation of four bytes per rune.
maxDraftSize := int(maxDraftSizeBytes) / 4
mlog.Info("Draft.Message has size restrictions", mlog.Int("max_characters", maxDraftSize), mlog.Int32("max_bytes", maxDraftSizeBytes))
return maxDraftSize
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlEmojiStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlEmojiStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.EmojiStore {
return &SqlEmojiStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
func (es SqlEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
emoji.PreSave()
if err := emoji.IsValid(); err != nil {
return nil, err
}
if _, err := es.GetMasterX().NamedExec(`INSERT INTO Emoji
(Id, CreateAt, UpdateAt, DeleteAt, CreatorId, Name)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :Name)`, emoji); err != nil {
return nil, errors.Wrap(err, "error saving emoji")
}
return emoji, nil
}
func (es SqlEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
return es.getBy(ctx, "Id", id)
}
func (es SqlEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
return es.getBy(ctx, "Name", name)
}
func (es SqlEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
// Creating (?, ?, ?) len(names) number of times.
keys := strings.Join(strings.Fields(strings.Repeat("? ", len(names))), ",")
args := makeStringArgs(names)
emojis := []*model.Emoji{}
if err := es.GetReplicaX().Select(&emojis,
`SELECT
*
FROM
Emoji
WHERE
Name IN (`+keys+`)
AND DeleteAt = 0`, args...); err != nil {
return nil, errors.Wrapf(err, "error getting emoji by names %v", names)
}
return emojis, nil
}
func (es SqlEmojiStore) GetList(offset, limit int, sort string) ([]*model.Emoji, error) {
emojis := []*model.Emoji{}
query := "SELECT * FROM Emoji WHERE DeleteAt = 0"
if sort == model.EmojiSortByName {
query += " ORDER BY Name"
}
query += " LIMIT ? OFFSET ?"
if err := es.GetReplicaX().Select(&emojis, query, limit, offset); err != nil {
return nil, errors.Wrap(err, "could not get list of emojis")
}
return emojis, nil
}
func (es SqlEmojiStore) Delete(emoji *model.Emoji, time int64) error {
if sqlResult, err := es.GetMasterX().Exec(
`UPDATE
Emoji
SET
DeleteAt = ?,
UpdateAt = ?
WHERE
Id = ?
AND DeleteAt = 0`, time, time, emoji.Id); err != nil {
return errors.Wrap(err, "could not delete emoji")
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
return store.NewErrNotFound("Emoji", emoji.Id).Wrap(err)
}
return nil
}
func (es SqlEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
emojis := []*model.Emoji{}
name = sanitizeSearchTerm(name, "\\")
term := ""
if !prefixOnly {
term = "%"
}
term += name + "%"
if err := es.GetReplicaX().Select(&emojis,
`SELECT
*
FROM
Emoji
WHERE
Name LIKE ?
AND DeleteAt = 0
ORDER BY Name
LIMIT ?`, term, limit); err != nil {
return nil, errors.Wrapf(err, "could not search emojis by name %s", name)
}
return emojis, nil
}
// getBy returns one active (not deleted) emoji, found by any one column (what/key).
func (es SqlEmojiStore) getBy(ctx context.Context, what, key string) (*model.Emoji, error) {
var emoji model.Emoji
err := es.DBXFromContext(ctx).Get(&emoji,
`SELECT
*
FROM
Emoji
WHERE
`+what+` = ?
AND DeleteAt = 0`, key)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Emoji", fmt.Sprintf("%s=%s", what, key))
}
return nil, errors.Wrapf(err, "could not get emoji by %s with value %s", what, key)
}
return &emoji, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type fileInfoWithChannelID struct {
Id string
CreatorId string
PostId string
ChannelId string
CreateAt int64
UpdateAt int64
DeleteAt int64
Path string
ThumbnailPath string
PreviewPath string
Name string
Extension string
Size int64
MimeType string
Width int
Height int
HasPreviewImage bool
MiniPreview *[]byte
Content string
RemoteId *string
Archived bool
}
func (fi fileInfoWithChannelID) ToModel() *model.FileInfo {
return &model.FileInfo{
Id: fi.Id,
CreatorId: fi.CreatorId,
PostId: fi.PostId,
ChannelId: fi.ChannelId,
CreateAt: fi.CreateAt,
UpdateAt: fi.UpdateAt,
DeleteAt: fi.DeleteAt,
Path: fi.Path,
ThumbnailPath: fi.ThumbnailPath,
PreviewPath: fi.PreviewPath,
Name: fi.Name,
Extension: fi.Extension,
Size: fi.Size,
MimeType: fi.MimeType,
Width: fi.Width,
Height: fi.Height,
HasPreviewImage: fi.HasPreviewImage,
MiniPreview: fi.MiniPreview,
Content: fi.Content,
RemoteId: fi.RemoteId,
}
}
type SqlFileInfoStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
queryFields []string
}
func (fs SqlFileInfoStore) ClearCaches() {
}
func newSqlFileInfoStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.FileInfoStore {
s := &SqlFileInfoStore{
SqlStore: sqlStore,
metrics: metrics,
}
s.queryFields = []string{
"FileInfo.Id",
"FileInfo.CreatorId",
"FileInfo.PostId",
"COALESCE(FileInfo.ChannelId, '') AS ChannelId",
"FileInfo.CreateAt",
"FileInfo.UpdateAt",
"FileInfo.DeleteAt",
"FileInfo.Path",
"FileInfo.ThumbnailPath",
"FileInfo.PreviewPath",
"FileInfo.Name",
"FileInfo.Extension",
"FileInfo.Size",
"FileInfo.MimeType",
"FileInfo.Width",
"FileInfo.Height",
"FileInfo.HasPreviewImage",
"FileInfo.MiniPreview",
"Coalesce(FileInfo.Content, '') AS Content",
"Coalesce(FileInfo.RemoteId, '') AS RemoteId",
"FileInfo.Archived",
}
return s
}
func (fs SqlFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
info.PreSave()
if err := info.IsValid(); err != nil {
return nil, err
}
query := `
INSERT INTO FileInfo
(Id, CreatorId, PostId, ChannelId, CreateAt, UpdateAt, DeleteAt, Path, ThumbnailPath, PreviewPath,
Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview, Content, RemoteId)
VALUES
(:Id, :CreatorId, :PostId, :ChannelId, :CreateAt, :UpdateAt, :DeleteAt, :Path, :ThumbnailPath, :PreviewPath,
:Name, :Extension, :Size, :MimeType, :Width, :Height, :HasPreviewImage, :MiniPreview, :Content, :RemoteId)
`
if _, err := fs.GetMasterX().NamedExec(query, info); err != nil {
return nil, errors.Wrap(err, "failed to save FileInfo")
}
return info, nil
}
func (fs SqlFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"FileInfo.Id": ids}).
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
items := []fileInfoWithChannelID{}
if err := fs.GetReplicaX().Select(&items, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find FileInfos")
}
if len(items) == 0 {
return nil, nil
}
infos := make([]*model.FileInfo, 0, len(items))
for _, item := range items {
infos = append(infos, item.ToModel())
}
return infos, nil
}
func (fs SqlFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
info.PreSave()
if err := info.IsValid(); err != nil {
return nil, err
}
// PostID and ChannelID are deliberately ignored
// from the list of fields to keep those two immutable.
queryString, args, err := fs.getQueryBuilder().
Update("FileInfo").
SetMap(map[string]any{
"UpdateAt": info.UpdateAt,
"DeleteAt": info.DeleteAt,
"Path": info.Path,
"ThumbnailPath": info.ThumbnailPath,
"PreviewPath": info.PreviewPath,
"Name": info.Name,
"Extension": info.Extension,
"Size": info.Size,
"MimeType": info.MimeType,
"Width": info.Width,
"Height": info.Height,
"HasPreviewImage": info.HasPreviewImage,
"MiniPreview": info.MiniPreview,
"Content": info.Content,
"RemoteId": info.RemoteId,
}).
Where(sq.Eq{"Id": info.Id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update FileInfo")
}
count, err := sqlResult.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "unable to retrieve rows affected")
}
if count == 0 {
return fs.Save(info)
}
return info, nil
}
func (fs SqlFileInfoStore) get(id string, fromMaster bool) (*model.FileInfo, error) {
info := &model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"Id": id}).
Where(sq.Eq{"DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
db := fs.GetReplicaX()
if fromMaster {
db = fs.GetMasterX()
}
if err := db.Get(info, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("FileInfo", id)
}
return nil, errors.Wrapf(err, "failed to get FileInfo with id=%s", id)
}
return info, nil
}
func (fs SqlFileInfoStore) Get(id string) (*model.FileInfo, error) {
return fs.get(id, false)
}
func (fs SqlFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
return fs.get(id, true)
}
func (fs SqlFileInfoStore) GetWithOptions(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
if perPage < 0 {
return nil, store.NewErrLimitExceeded("perPage", perPage, "value used in pagination while getting FileInfos")
} else if page < 0 {
return nil, store.NewErrLimitExceeded("page", page, "value used in pagination while getting FileInfos")
}
if perPage == 0 {
return nil, nil
}
if opt == nil {
opt = &model.GetFileInfosOptions{}
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo")
if len(opt.ChannelIds) > 0 {
query = query.Where(sq.Eq{"FileInfo.ChannelId": opt.ChannelIds})
}
if len(opt.UserIds) > 0 {
query = query.Where(sq.Eq{"FileInfo.CreatorId": opt.UserIds})
}
if opt.Since > 0 {
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": opt.Since})
}
if !opt.IncludeDeleted {
query = query.Where("FileInfo.DeleteAt = 0")
}
if opt.SortBy == "" {
opt.SortBy = model.FileinfoSortByCreated
}
sortDirection := "ASC"
if opt.SortDescending {
sortDirection = "DESC"
}
switch opt.SortBy {
case model.FileinfoSortByCreated:
query = query.OrderBy("FileInfo.CreateAt " + sortDirection)
case model.FileinfoSortBySize:
query = query.OrderBy("FileInfo.Size " + sortDirection)
default:
return nil, store.NewErrInvalidInput("FileInfo", "<sortOption>", opt.SortBy)
}
query = query.OrderBy("FileInfo.Id ASC") // secondary sort for sort stability
query = query.Limit(uint64(perPage)).Offset(uint64(perPage * page))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
infos := []*model.FileInfo{}
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find FileInfos")
}
return infos, nil
}
func (fs SqlFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
info := &model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"Path": path}).
Where(sq.Eq{"DeleteAt": 0}).
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := fs.GetReplicaX().Get(info, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("FileInfo", fmt.Sprintf("path=%s", path))
}
return nil, errors.Wrapf(err, "failed to get FileInfo with path=%s", path)
}
return info, nil
}
func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
}
func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
infos := []*model.FileInfo{}
dbmap := fs.GetReplicaX()
if readFromMaster {
dbmap = fs.GetMasterX()
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"PostId": postId}).
OrderBy("CreateAt")
if !includeDeleted {
query = query.Where("DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := dbmap.Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find FileInfos with postId=%s", postId)
}
return infos, nil
}
func (fs SqlFileInfoStore) GetForUser(userId string) ([]*model.FileInfo, error) {
infos := []*model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"CreatorId": userId}).
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("CreateAt")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find FileInfos with creatorId=%s", userId)
}
return infos, nil
}
func (fs SqlFileInfoStore) AttachToPost(fileId, postId, channelId, creatorId string) error {
query := fs.getQueryBuilder().
Update("FileInfo").
Set("PostId", postId).
Set("ChannelId", channelId).
Where(sq.And{
sq.Eq{"Id": fileId},
sq.Eq{"PostId": ""},
sq.Or{
sq.Eq{"CreatorId": creatorId},
sq.Eq{"CreatorId": "nouser"},
},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "file_info_tosql")
}
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to update FileInfo with id=%s and postId=%s", fileId, postId)
}
count, err := sqlResult.RowsAffected()
if err != nil {
// RowsAffected should never fail with the MySQL or Postgres drivers
return errors.Wrap(err, "unable to retrieve rows affected")
} else if count == 0 {
// Could not attach the file to the post
return store.NewErrInvalidInput("FileInfo", "<id, postId, creatorId>", fmt.Sprintf("<%s, %s, %s>", fileId, postId, creatorId))
}
return nil
}
func (fs SqlFileInfoStore) SetContent(fileId, content string) error {
query := fs.getQueryBuilder().
Update("FileInfo").
Set("Content", content).
Where(sq.Eq{"Id": fileId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "file_info_tosql")
}
_, err = fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to update FileInfo content with id=%s", fileId)
}
return nil
}
func (fs SqlFileInfoStore) DeleteForPost(postId string) (string, error) {
if _, err := fs.GetMasterX().Exec(
`UPDATE
FileInfo
SET
DeleteAt = ?
WHERE
PostId = ?`, model.GetMillis(), postId); err != nil {
return "", errors.Wrapf(err, "failed to update FileInfo with postId=%s", postId)
}
return postId, nil
}
func (fs SqlFileInfoStore) PermanentDelete(fileId string) error {
if _, err := fs.GetMasterX().Exec(`DELETE FROM FileInfo WHERE Id = ?`, fileId); err != nil {
return errors.Wrapf(err, "failed to delete FileInfo with id=%s", fileId)
}
return nil
}
func (fs SqlFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if fs.DriverName() == "postgres" {
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from FileInfo WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := fs.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete FileInfos in batch")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
}
return rowsAffected, nil
}
func (fs SqlFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
query := "DELETE from FileInfo WHERE CreatorId = ?"
sqlResult, err := fs.GetMasterX().Exec(query, userId)
if err != nil {
return 0, errors.Wrapf(err, "failed to delete FileInfo with creatorId=%s", userId)
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
}
return rowsAffected, nil
}
func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
// Since we don't support paging for DB search, we just return nothing for later pages
if page > 0 {
return model.NewFileInfoList(), nil
}
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId").
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC").
Limit(100)
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"C.TeamId": teamId},
sq.Eq{"C.TeamId": ""},
})
}
now := model.GetMillis()
for _, params := range paramsList {
if params.Modifier == model.ModifierFiles {
// Deliberately keeping non-alphanumeric characters to
// prevent surprises in UI.
buf, err := json.Marshal(params)
if err != nil {
return nil, err
}
err = fs.stores.post.LogRecentSearch(userId, buf, now)
if err != nil {
return nil, err
}
}
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
if !params.IncludeDeletedChannels {
query = query.Where(sq.Eq{"C.DeleteAt": 0})
}
if !params.SearchWithoutUserId {
query = query.Where(sq.Eq{"CM.UserId": userId})
}
if len(params.InChannels) != 0 {
query = query.Where(sq.Eq{"C.Id": params.InChannels})
}
if len(params.Extensions) != 0 {
query = query.Where(sq.Eq{"FileInfo.Extension": params.Extensions})
}
if len(params.ExcludedExtensions) != 0 {
query = query.Where(sq.NotEq{"FileInfo.Extension": params.ExcludedExtensions})
}
if len(params.ExcludedChannels) != 0 {
query = query.Where(sq.NotEq{"C.Id": params.ExcludedChannels})
}
if len(params.FromUsers) != 0 {
query = query.Where(sq.Eq{"FileInfo.CreatorId": params.FromUsers})
}
if len(params.ExcludedUsers) != 0 {
query = query.Where(sq.NotEq{"FileInfo.CreatorId": params.ExcludedUsers})
}
// handle after: before: on: filters
if params.OnDate != "" {
onDateStart, onDateEnd := params.GetOnDateMillis()
query = query.Where(sq.Expr("FileInfo.CreateAt BETWEEN ? AND ?", strconv.FormatInt(onDateStart, 10), strconv.FormatInt(onDateEnd, 10)))
} else {
if params.ExcludedDate != "" {
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
query = query.Where(sq.Expr("FileInfo.CreateAt NOT BETWEEN ? AND ?", strconv.FormatInt(excludedDateStart, 10), strconv.FormatInt(excludedDateEnd, 10)))
}
if params.AfterDate != "" {
afterDate := params.GetAfterDateMillis()
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
}
if params.BeforeDate != "" {
beforeDate := params.GetBeforeDateMillis()
query = query.Where(sq.LtOrEq{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
}
if params.ExcludedAfterDate != "" {
afterDate := params.GetExcludedAfterDateMillis()
query = query.Where(sq.Lt{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
}
if params.ExcludedBeforeDate != "" {
beforeDate := params.GetExcludedBeforeDateMillis()
query = query.Where(sq.Gt{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
}
}
terms := params.Terms
excludedTerms := params.ExcludedTerms
for _, c := range fs.specialSearchChars() {
terms = strings.Replace(terms, c, " ", -1)
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
}
if terms == "" && excludedTerms == "" {
// we've already confirmed that we have a channel or user to search for
} else if fs.DriverName() == model.DatabaseDriverPostgres {
// Parse text for wildcards
if wildcard, err := regexp.Compile(`\*($| )`); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
}
queryTerms := ""
if params.OrTerms {
queryTerms = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else {
queryTerms = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
}
query = query.Where(sq.Or{
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Name) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', Translate(FileInfo.Name, '.,-', ' ')) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Content) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
})
} else if fs.DriverName() == model.DatabaseDriverMysql {
var err error
terms, err = removeMysqlStopWordsFromTerms(terms)
if err != nil {
return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms")
}
if terms == "" {
return model.NewFileInfoList(), nil
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " -(" + excludedTerms + ")"
}
queryTerms := ""
if params.OrTerms {
queryTerms = terms + excludeClause
} else {
splitTerms := []string{}
for _, t := range strings.Fields(terms) {
splitTerms = append(splitTerms, "+"+t)
}
queryTerms = strings.Join(splitTerms, " ") + excludeClause
}
query = query.Where(sq.Or{
sq.Expr("MATCH (FileInfo.Name) AGAINST (? IN BOOLEAN MODE)", queryTerms),
sq.Expr("MATCH (FileInfo.Content) AGAINST (? IN BOOLEAN MODE)", queryTerms),
})
}
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
list := model.NewFileInfoList()
items := []fileInfoWithChannelID{}
err = fs.GetSearchReplicaX().Select(&items, queryString, args...)
if err != nil {
mlog.Warn("Query error searching files.", mlog.Err(err))
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
} else {
for _, item := range items {
info := item.ToModel()
list.AddFileInfo(info)
list.AddOrder(info.Id)
}
}
list.MakeNonNil()
return list, nil
}
func (fs SqlFileInfoStore) CountAll() (int64, error) {
query := fs.getQueryBuilder().
Select("COUNT(*)").
From("FileInfo").
Where("DeleteAt = 0")
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_tosql")
}
var count int64
err = fs.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Files")
}
return count, nil
}
func (fs SqlFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
files := []*model.FileForIndexing{}
sql, args, _ := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Or{
sq.Gt{"FileInfo.CreateAt": startTime},
sq.And{
sq.Eq{"FileInfo.CreateAt": startTime},
sq.Gt{"FileInfo.Id": startFileID},
},
}).
OrderBy("FileInfo.CreateAt ASC, FileInfo.Id ASC").
Limit(uint64(limit)).
ToSql()
err := fs.GetSearchReplicaX().Select(&files, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Files")
}
return files, nil
}
func (fs SqlFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
query := fs.getQueryBuilder().
Select("COALESCE(SUM(Size), 0)").
From("FileInfo")
if !includeDeleted {
query = query.Where("DeleteAt = 0")
}
var size int64
err := fs.GetReplicaX().GetBuilder(&size, query)
if err != nil {
return int64(0), errors.Wrap(err, "failed to get storage usage")
}
return size, nil
}
// GetUptoNSizeFileTime returns the CreateAt time of the last accessible file with a running-total size upto n bytes.
func (fs *SqlFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
if n <= 0 {
return 0, errors.New("n can't be less than 1")
}
var sizeSubQuery sq.SelectBuilder
// Separate query for MySql, as current min-version 5.x doesn't support window-functions
if fs.DriverName() == model.DatabaseDriverMysql {
sizeSubQuery = sq.
Select("(@runningSum := @runningSum + fi.Size) RunningTotal", "fi.CreateAt").
From("FileInfo fi").
Join("(SELECT @runningSum := 0) as tmp").
Where(sq.Eq{"fi.DeleteAt": 0}).
OrderBy("fi.CreateAt DESC, fi.Id")
} else {
sizeSubQuery = sq.
Select("SUM(fi.Size) OVER(ORDER BY CreateAt DESC, fi.Id) RunningTotal", "fi.CreateAt").
From("FileInfo fi").
Where(sq.Eq{"fi.DeleteAt": 0})
}
builder := fs.getQueryBuilder().
Select("fi2.CreateAt").
FromSelect(sizeSubQuery, "fi2").
Where(sq.LtOrEq{"fi2.RunningTotal": n}).
OrderBy("fi2.CreateAt").
Limit(1)
query, queryArgs, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "GetUptoNSizeFileTime_tosql")
}
var createAt int64
if err := fs.GetReplicaX().Get(&createAt, query, queryArgs...); err != nil {
if err == sql.ErrNoRows {
return 0, store.NewErrNotFound("File", "none")
}
return 0, errors.Wrapf(err, "failed to get the File for size upto=%d", n)
}
return createAt, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type selectType int
const (
selectGroups selectType = iota
selectCountGroups
)
type groupTeam struct {
model.GroupSyncable
TeamId string
}
type groupChannel struct {
model.GroupSyncable
ChannelId string
}
type groupTeamJoin struct {
groupTeam
TeamDisplayName string
TeamType string
}
type groupChannelJoin struct {
groupChannel
ChannelDisplayName string
TeamDisplayName string
TeamType string
ChannelType string
TeamId string
}
type SqlGroupStore struct {
*SqlStore
}
func newSqlGroupStore(sqlStore *SqlStore) store.GroupStore {
return &SqlGroupStore{SqlStore: sqlStore}
}
func (s *SqlGroupStore) Create(group *model.Group) (*model.Group, error) {
if group.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", group.Id)
}
if err := group.IsValidForCreate(); err != nil {
return nil, err
}
group.Id = model.NewId()
group.CreateAt = model.GetMillis()
group.UpdateAt = group.CreateAt
if _, err := s.GetMasterX().NamedExec(`INSERT INTO UserGroups
(Id, Name, DisplayName, Description, Source, RemoteId, CreateAt, UpdateAt, DeleteAt, AllowReference)
VALUES
(:Id, :Name, :DisplayName, :Description, :Source, :RemoteId, :CreateAt, :UpdateAt, :DeleteAt, :AllowReference)`, group); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, errors.Wrapf(err, "Group with name %s already exists", *group.Name)
}
return nil, errors.Wrap(err, "failed to save Group")
}
return group, nil
}
func (s *SqlGroupStore) CreateWithUserIds(g *model.GroupWithUserIds) (_ *model.Group, err error) {
if g.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", g.Id)
}
// Check if group values are formatted correctly
if appErr := g.IsValidForCreate(); appErr != nil {
return nil, appErr
}
// Check Users exist
if err = s.checkUsersExist(g.UserIds); err != nil {
return nil, err
}
g.Id = model.NewId()
g.CreateAt = model.GetMillis()
g.UpdateAt = g.CreateAt
groupInsertQuery, groupInsertArgs, err := s.getQueryBuilder().
Insert("UserGroups").
Columns("Id", "Name", "DisplayName", "Description", "Source", "RemoteId", "CreateAt", "UpdateAt", "DeleteAt", "AllowReference").
Values(g.Id, g.Name, g.DisplayName, g.Description, g.Source, g.RemoteId, g.CreateAt, g.UpdateAt, 0, g.AllowReference).
ToSql()
if err != nil {
return nil, err
}
usersInsertQuery, usersInsertArgs, err := s.buildInsertGroupUsersQuery(g.Id, g.UserIds)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Create a new usergroup
if _, err = txn.Exec(groupInsertQuery, groupInsertArgs...); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to save Group")
}
// Insert the Group Members
if _, err = executePossiblyEmptyQuery(txn, usersInsertQuery, usersInsertArgs...); err != nil {
return nil, err
}
// Get the new Group along with the member count
groupGroupQuery := `
SELECT
UserGroups.*,
A.Count AS MemberCount
FROM
UserGroups
INNER JOIN (
SELECT
UserGroups.Id,
COUNT(GroupMembers.UserId) AS Count
FROM
UserGroups
LEFT JOIN GroupMembers ON UserGroups.Id = GroupMembers.GroupId
WHERE
UserGroups.Id = ?
GROUP BY
UserGroups.Id
ORDER BY
UserGroups.DisplayName,
UserGroups.Id
LIMIT
? OFFSET ?
) AS A ON UserGroups.Id = A.Id
ORDER BY
UserGroups.CreateAt DESC`
var newGroup group
if err = txn.Get(&newGroup, groupGroupQuery, g.Id, 1, 0); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return newGroup.ToModel(), nil
}
func (s *SqlGroupStore) checkUsersExist(userIDs []string) error {
if len(userIDs) == 0 {
return nil
}
usersSelectQuery, usersSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Users").
Where(sq.Eq{"Id": userIDs, "DeleteAt": 0}).
ToSql()
if err != nil {
return err
}
var rows []string
err = s.GetReplicaX().Select(&rows, usersSelectQuery, usersSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(userIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, userID := range rows {
retrievedIDs[userID] = true
}
for _, userID := range userIDs {
if _, ok := retrievedIDs[userID]; !ok {
return store.NewErrNotFound("User", userID)
}
}
return nil
}
func (s *SqlGroupStore) buildInsertGroupUsersQuery(groupId string, userIds []string) (query string, args []any, err error) {
if len(userIds) > 0 {
builder := s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
for _, userId := range userIds {
builder = builder.Values(groupId, userId, model.GetMillis(), 0)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlGroupStore) Get(groupId string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ?", groupId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupId)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupId)
}
return &group, nil
}
func (s *SqlGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
var group model.Group
query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Name": name})
if opts.FilterAllowReference {
query = query.Where("AllowReference = true")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_name_tosql")
}
if err := s.GetReplicaX().Get(&group, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get Group with name=%s", name)
}
return &group, nil
}
func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
groups := []*model.Group{}
query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Id": groupIDs})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_ids_tosql")
}
if err := s.GetReplicaX().Select(&groups, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Groups by ids")
}
return groups, nil
}
func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE RemoteId = ? AND Source = ?", remoteID, groupSource); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("remoteId=%s", remoteID))
}
return nil, errors.Wrapf(err, "failed to get Group with remoteId=%s", remoteID)
}
return &group, nil
}
func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
groups := []*model.Group{}
if err := s.GetReplicaX().Select(&groups, "SELECT * from UserGroups WHERE DeleteAt = 0 AND Source = ?", groupSource); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups by groupSource=%v", groupSource)
}
return groups, nil
}
func (s *SqlGroupStore) GetByUser(userId string) ([]*model.Group, error) {
groups := []*model.Group{}
query := `
SELECT
UserGroups.*
FROM
GroupMembers
JOIN UserGroups ON UserGroups.Id = GroupMembers.GroupId
WHERE
GroupMembers.DeleteAt = 0
AND UserId = ?`
if err := s.GetReplicaX().Select(&groups, query, userId); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with userId=%s", userId)
}
return groups, nil
}
func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, error) {
var retrievedGroup model.Group
if err := s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", group.Id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", group.Id)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", group.Id)
}
// If updating DeleteAt it can only be to 0
if group.DeleteAt != retrievedGroup.DeleteAt && group.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
group.CreateAt = retrievedGroup.CreateAt
group.UpdateAt = model.GetMillis()
if err := group.IsValidForUpdate(); err != nil {
return nil, err
}
res, err := s.GetMasterX().NamedExec(`UPDATE UserGroups
SET Name=:Name, DisplayName=:DisplayName, Description=:Description, Source=:Source,
RemoteId=:RemoteId, CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, AllowReference=:AllowReference
WHERE Id=:Id`, group)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to update Group")
}
rowsChanged, _ := res.RowsAffected()
if rowsChanged > 1 {
return nil, errors.Wrapf(err, "multiple Groups were update: %d", rowsChanged)
}
return group, nil
}
func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt = 0", groupID); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
SET DeleteAt=?, UpdateAt=?
WHERE Id=? AND DeleteAt=0`, time, time, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt != 0", groupID); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
SET DeleteAt=0, UpdateAt=?
WHERE Id=? AND DeleteAt!=0`, time, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) GetMember(groupID, userID string) (*model.GroupMember, error) {
query, args, err := s.getQueryBuilder().
Select("*").
From("GroupMembers").
Where(sq.Eq{"UserId": userID}).
Where(sq.Eq{"GroupId": groupID}).
Where(sq.Eq{"DeleteAt": 0}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_member_query")
}
var groupMember model.GroupMember
err = s.GetReplicaX().Get(&groupMember, query, args...)
if err != nil {
return nil, errors.Wrap(err, "GetMember")
}
return &groupMember, nil
}
func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
AND GroupId = ?`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
return s.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (s *SqlGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
groupMembers := []*model.User{}
userQuery := s.getQueryBuilder().
Select(`u.*`).
From("GroupMembers").
Join("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"u.DeleteAt": 0}).
Where(sq.Eq{"GroupId": groupID})
userQuery = applyViewRestrictionsFilter(userQuery, viewRestrictions, true)
queryString, args, err := userQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
orderQuery := s.getQueryBuilder().
Select("u.*").
From("(" + queryString + ") AS u")
if teammateNameDisplay == model.ShowNicknameFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN u.Nickname != '' THEN u.Nickname
WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName)
WHEN u.FirstName != '' THEN u.FirstName
WHEN u.LastName != '' THEN u.LastName
ELSE u.Username
END`)
} else if teammateNameDisplay == model.ShowFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName)
WHEN u.FirstName != '' THEN u.FirstName
WHEN u.LastName != '' THEN u.LastName
ELSE u.Username
END`)
} else {
orderQuery = orderQuery.OrderBy("u.Username")
}
orderQuery = orderQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
queryString, _, err = orderQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
groupMembers := []*model.User{}
if err := s.GetReplicaX().Get(&model.Group{}, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil {
return nil, errors.Wrap(err, "GetNonMemberUsersPage")
}
query := s.getQueryBuilder().
Select("u.*").
From("Users u").
LeftJoin("GroupMembers ON (GroupMembers.UserId = u.Id AND GroupMembers.GroupId = ?)", groupID).
Where(sq.Eq{"u.DeleteAt": 0}).
Where("(GroupMembers.UserID IS NULL OR GroupMembers.DeleteAt != 0)").
Limit(uint64(perPage)).
Offset(uint64(page * perPage)).
OrderBy("u.Username ASC")
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) {
return s.GetMemberCountWithRestrictions(groupID, nil)
}
func (s *SqlGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(DISTINCT u.Id)").
From("GroupMembers").
Join("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.GroupId": groupID}).
Where(sq.Eq{"u.DeleteAt": 0}).
Where(sq.Eq{"GroupMembers.DeleteAt": 0})
query = applyViewRestrictionsFilter(query, viewRestrictions, false)
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count member Users for Group with id=%s", groupID)
}
return count, nil
}
func (s *SqlGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupId = ?
AND GroupMembers.UserId IN (
SELECT TeamMembers.UserId
FROM TeamMembers
JOIN Teams ON Teams.Id = ?
WHERE TeamMembers.TeamId = Teams.Id
AND TeamMembers.DeleteAt = 0
)
AND GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID, teamID); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and teamId=%s", groupID, teamID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupId = ?
AND GroupMembers.UserId NOT IN (
SELECT ChannelMembers.UserId
FROM ChannelMembers
WHERE ChannelMembers.ChannelId = ?
)
AND GroupMembers.UserId IN (
SELECT TeamMembers.UserId
FROM TeamMembers
JOIN Channels ON Channels.Id = ?
JOIN Teams ON Teams.Id = Channels.TeamId
WHERE TeamMembers.TeamId = Teams.Id
AND TeamMembers.DeleteAt = 0
)
AND GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID, channelID, channelID); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and channelId!=%s", groupID, channelID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, args, err := s.buildUpsertMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members[0], nil
}
func (s *SqlGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, args, err := s.buildDeleteMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update GroupMember with groupId=%s and userId=%s", groupID, userID)
}
return members[0], nil
}
func (s *SqlGroupStore) PermanentDeleteMembersByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM GroupMembers WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to permanent delete GroupMember with userId=%s", userId)
}
return nil
}
func (s *SqlGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// Reset values that shouldn't be updatable by parameter
groupSyncable.DeleteAt = 0
groupSyncable.CreateAt = model.GetMillis()
groupSyncable.UpdateAt = groupSyncable.CreateAt
var insertErr error
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
if _, err := s.Team().Get(groupSyncable.SyncableId); err != nil {
return nil, err
}
_, insertErr = s.GetMasterX().NamedExec(`INSERT INTO GroupTeams
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, TeamId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :TeamId)`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
var channel *model.Channel
channel, err := s.Channel().Get(groupSyncable.SyncableId, false)
if err != nil {
return nil, err
}
_, insertErr = s.GetMasterX().NamedExec(`INSERT INTO GroupChannels
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, ChannelId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :ChannelId)`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if insertErr != nil {
return nil, errors.Wrap(insertErr, "unable to insert GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
return groupSyncable, nil
}
func (s *SqlGroupStore) getGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
var err error
var result any
switch syncableType {
case model.GroupSyncableTypeTeam:
var team groupTeam
err = s.GetReplicaX().Get(&team, `SELECT * FROM GroupTeams WHERE GroupId=? AND TeamId=?`, groupID, syncableID)
result = &team
case model.GroupSyncableTypeChannel:
var ch groupChannel
err = s.GetReplicaX().Get(&ch, `SELECT * FROM GroupChannels WHERE GroupId=? AND ChannelId=?`, groupID, syncableID)
result = &ch
}
if err != nil {
return nil, err
}
if result == nil {
return nil, sql.ErrNoRows
}
groupSyncable := model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
groupTeam := result.(*groupTeam)
groupSyncable.SyncableId = groupTeam.TeamId
groupSyncable.GroupId = groupTeam.GroupId
groupSyncable.AutoAdd = groupTeam.AutoAdd
groupSyncable.CreateAt = groupTeam.CreateAt
groupSyncable.DeleteAt = groupTeam.DeleteAt
groupSyncable.UpdateAt = groupTeam.UpdateAt
groupSyncable.Type = syncableType
case model.GroupSyncableTypeChannel:
groupChannel := result.(*groupChannel)
groupSyncable.SyncableId = groupChannel.ChannelId
groupSyncable.GroupId = groupChannel.GroupId
groupSyncable.AutoAdd = groupChannel.AutoAdd
groupSyncable.CreateAt = groupChannel.CreateAt
groupSyncable.DeleteAt = groupChannel.DeleteAt
groupSyncable.UpdateAt = groupChannel.UpdateAt
groupSyncable.Type = syncableType
default:
return nil, fmt.Errorf("unable to convert syncableType: %s", syncableType.String())
}
return &groupSyncable, nil
}
func (s *SqlGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
groupSyncables := []*model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
sqlQuery := `
SELECT
GroupTeams.*,
Teams.DisplayName AS TeamDisplayName,
Teams.Type AS TeamType
FROM
GroupTeams
JOIN Teams ON Teams.Id = GroupTeams.TeamId
WHERE
GroupId = ? AND GroupTeams.DeleteAt = 0`
results := []*groupTeamJoin{}
err := s.GetReplicaX().Select(&results, sqlQuery, groupID)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupTeams with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.TeamId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
case model.GroupSyncableTypeChannel:
sqlQuery := `
SELECT
GroupChannels.*,
Channels.DisplayName AS ChannelDisplayName,
Teams.DisplayName AS TeamDisplayName,
Channels.Type As ChannelType,
Teams.Type As TeamType,
Teams.Id AS TeamId
FROM
GroupChannels
JOIN Channels ON Channels.Id = GroupChannels.ChannelId
JOIN Teams ON Teams.Id = Channels.TeamId
WHERE
GroupId = ? AND GroupChannels.DeleteAt = 0`
results := []*groupChannelJoin{}
err := s.GetReplicaX().Select(&results, sqlQuery, groupID)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupChannels with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.ChannelId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
ChannelDisplayName: result.ChannelDisplayName,
ChannelType: result.ChannelType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
TeamID: result.TeamID,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
}
return groupSyncables, nil
}
func (s *SqlGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
retrievedGroupSyncable, err := s.getGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)), "GroupSyncable not found")
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
}
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// If updating DeleteAt it can only be to 0
if groupSyncable.DeleteAt != retrievedGroupSyncable.DeleteAt && groupSyncable.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
groupSyncable.CreateAt = retrievedGroupSyncable.CreateAt
groupSyncable.UpdateAt = model.GetMillis()
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
// We need to get the TeamId so redux can manage channels when teams are unlinked
var channel *model.Channel
channel, channelErr := s.Channel().Get(groupSyncable.SyncableId, false)
if channelErr != nil {
return nil, channelErr
}
_, err = s.GetMasterX().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
if groupSyncable.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("GroupSyncable", "<groupId, syncableId, syncableType>", fmt.Sprintf("<%s, %s, %s>", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type))
}
time := model.GetMillis()
groupSyncable.DeleteAt = time
groupSyncable.UpdateAt = time
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupTeams.TeamId TeamID").
From("GroupMembers").
Join("GroupTeams ON GroupTeams.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Teams ON Teams.Id = GroupTeams.TeamId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupTeams.DeleteAt": 0,
"GroupTeams.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Teams.DeleteAt": 0,
})
if !includeRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN TeamMembers ON TeamMembers.TeamId = GroupTeams.TeamId AND TeamMembers.UserId = GroupMembers.UserId").
Where(sq.Eq{"TeamMembers.UserId": nil}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupTeams.UpdateAt": since},
})
}
if teamID != nil {
builder = builder.Where(sq.Eq{"Teams.Id": *teamID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_to_add_tosql")
}
teamMembers := []*model.UserTeamIDPair{}
err = s.GetMasterX().Select(&teamMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find UserTeamIDPairs")
}
return teamMembers, nil
}
func (s *SqlGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupChannels.ChannelId ChannelID").
From("GroupMembers").
Join("GroupChannels ON GroupChannels.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Channels ON Channels.Id = GroupChannels.ChannelId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupChannels.DeleteAt": 0,
"GroupChannels.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Channels.DeleteAt": 0,
})
if !includeRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN ChannelMemberHistory ON ChannelMemberHistory.ChannelId = GroupChannels.ChannelId AND ChannelMemberHistory.UserId = GroupMembers.UserId").
Where(sq.Eq{
"ChannelMemberHistory.UserId": nil,
"ChannelMemberHistory.LeaveTime": nil,
}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupChannels.UpdateAt": since},
})
}
if channelID != nil {
builder = builder.Where(sq.Eq{"Channels.Id": *channelID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_to_add_tosql")
}
channelMembers := []*model.UserChannelIDPair{}
err = s.GetMasterX().Select(&channelMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find UserChannelIDPairs")
}
return channelMembers, nil
}
func groupSyncableToGroupTeam(groupSyncable *model.GroupSyncable) *groupTeam {
return &groupTeam{
GroupSyncable: *groupSyncable,
TeamId: groupSyncable.SyncableId,
}
}
func groupSyncableToGroupChannel(groupSyncable *model.GroupSyncable) *groupChannel {
return &groupChannel{
GroupSyncable: *groupSyncable,
ChannelId: groupSyncable.SyncableId,
}
}
func (s *SqlGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
whereStmt := `
(TeamMembers.TeamId,
TeamMembers.UserId)
NOT IN (
SELECT
Teams.Id AS TeamId,
GroupMembers.UserId
FROM
Teams
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Teams.GroupConstrained = TRUE
AND GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Teams.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Teams.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"TeamMembers.TeamId",
"TeamMembers.UserId",
"TeamMembers.Roles",
"TeamMembers.DeleteAt",
"TeamMembers.SchemeUser",
"TeamMembers.SchemeAdmin",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) AS SchemeGuest",
).
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
Where(sq.Eq{"TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0, "Teams.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if teamID != nil {
builder = builder.Where(sq.Eq{"TeamMembers.TeamId": *teamID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_to_remove_tosql")
}
teamMembers := []*model.TeamMember{}
err = s.GetReplicaX().Select(&teamMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return teamMembers, nil
}
func (s *SqlGroupStore) CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, error) {
countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectCountGroups, channelId, opts)
countQueryString, args, err := countQuery.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_groups_by_channel_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, countQueryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups by channel with channelId=%s", channelId)
}
return count, nil
}
type group struct {
Id string
Name *string
DisplayName string
Description string
Source model.GroupSource
RemoteId *string
CreateAt int64
UpdateAt int64
DeleteAt int64
HasSyncables bool
MemberCount *int
AllowReference bool
ChannelMemberCount *int
ChannelMemberTimezonesCount *int
}
func (g group) ToModel() *model.Group {
return &model.Group{
Id: g.Id,
Name: g.Name,
DisplayName: g.DisplayName,
Description: g.Description,
Source: g.Source,
RemoteId: g.RemoteId,
CreateAt: g.CreateAt,
UpdateAt: g.UpdateAt,
DeleteAt: g.DeleteAt,
HasSyncables: g.HasSyncables,
AllowReference: g.AllowReference,
MemberCount: g.MemberCount,
ChannelMemberCount: g.ChannelMemberCount,
ChannelMemberTimezonesCount: g.ChannelMemberTimezonesCount,
}
}
type groups []*group
func (groups groups) ToModel() []*model.Group {
res := make([]*model.Group, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupWithSchemeAdmin struct {
group
SyncableSchemeAdmin *bool
}
func (g groupWithSchemeAdmin) ToModel() *model.GroupWithSchemeAdmin {
if g.SyncableSchemeAdmin == nil {
g.SyncableSchemeAdmin = model.NewBool(false)
}
res := &model.GroupWithSchemeAdmin{
Group: *g.group.ToModel(),
SchemeAdmin: g.SyncableSchemeAdmin,
}
return res
}
type groupsWithSchemeAdmin []*groupWithSchemeAdmin
func (groups groupsWithSchemeAdmin) ToModel() []*model.GroupWithSchemeAdmin {
res := make([]*model.GroupWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupAssociatedToChannelWithSchemeAdmin struct {
groupWithSchemeAdmin
ChannelId string
}
func (g groupAssociatedToChannelWithSchemeAdmin) ToModel() *model.GroupsAssociatedToChannelWithSchemeAdmin {
withSchemeAdmin := g.groupWithSchemeAdmin.ToModel()
return &model.GroupsAssociatedToChannelWithSchemeAdmin{
ChannelId: g.ChannelId,
SchemeAdmin: withSchemeAdmin.SchemeAdmin,
Group: withSchemeAdmin.Group,
}
}
type groupsAssociatedToChannelWithSchemeAdmin []groupAssociatedToChannelWithSchemeAdmin
func (groups groupsAssociatedToChannelWithSchemeAdmin) ToModel() []*model.GroupsAssociatedToChannelWithSchemeAdmin {
res := make([]*model.GroupsAssociatedToChannelWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
func (s *SqlGroupStore) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectGroups, channelId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_by_channel_tosql")
}
groups := groupsWithSchemeAdmin{}
err = s.GetReplicaX().Select(&groups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with channelId=%s", channelId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
whereStmt := `
(ChannelMembers.ChannelId,
ChannelMembers.UserId)
NOT IN (
SELECT
Channels.Id AS ChannelId,
GroupMembers.UserId
FROM
Channels
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Channels.GroupConstrained = TRUE
AND GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Channels.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Channels.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) AS SchemeGuest",
).
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
Where(sq.Eq{"Channels.DeleteAt": 0, "Channels.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if channelID != nil {
builder = builder.Where(sq.Eq{"ChannelMembers.ChannelId": *channelID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_to_remove_tosql")
}
channelMembers := []*model.ChannelMember{}
err = s.GetReplicaX().Select(&channelMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
return channelMembers, nil
}
func (s *SqlGroupStore) groupsBySyncableBaseQuery(st model.GroupSyncableType, t selectType, syncableID string, opts model.GroupSearchOpts) sq.SelectBuilder {
selectStrs := map[selectType]string{
selectGroups: "ug.*, gs.SchemeAdmin AS SyncableSchemeAdmin",
selectCountGroups: "COUNT(*)",
}
var table string
var idCol string
if st == model.GroupSyncableTypeTeam {
table = "GroupTeams"
idCol = "TeamId"
} else {
table = "GroupChannels"
idCol = "ChannelId"
}
query := s.getQueryBuilder().
Select(selectStrs[t]).
From(fmt.Sprintf("%s gs", table)).
LeftJoin("UserGroups ug ON gs.GroupId = ug.Id").
Where(fmt.Sprintf("ug.DeleteAt = 0 AND gs.%s = ? AND gs.DeleteAt = 0", idCol), syncableID)
if opts.IncludeMemberCount && t == selectGroups {
query = s.getQueryBuilder().
Select(fmt.Sprintf("ug.*, coalesce(Members.MemberCount, 0) AS MemberCount, Group%ss.SchemeAdmin AS SyncableSchemeAdmin", st)).
From("UserGroups ug").
LeftJoin("(SELECT GroupMembers.GroupId, COUNT(*) AS MemberCount FROM GroupMembers LEFT JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.DeleteAt = 0 AND Users.DeleteAt = 0 GROUP BY GroupId) AS Members ON Members.GroupId = ug.Id").
LeftJoin(fmt.Sprintf("%[1]s ON %[1]s.GroupId = ug.Id", table)).
Where(fmt.Sprintf("ug.DeleteAt = 0 AND %[1]s.DeleteAt = 0 AND %[1]s.%[2]s = ?", table, idCol), syncableID).
OrderBy("ug.DisplayName")
}
if opts.FilterAllowReference && t == selectGroups {
query = query.Where("ug.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(ug.Name %[1]s ? OR ug.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) getGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) sq.SelectBuilder {
query := s.getQueryBuilder().
Select("gc.ChannelId, ug.*, gc.SchemeAdmin AS SyncableSchemeAdmin").
From("UserGroups ug").
LeftJoin(`
(SELECT
GroupChannels.GroupId, GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = ug.Id`, teamID).
Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("ug.DisplayName")
if opts.IncludeMemberCount {
query = s.getQueryBuilder().
Select("gc.ChannelId, ug.*, coalesce(Members.MemberCount, 0) AS MemberCount, gc.SchemeAdmin AS SyncableSchemeAdmin").
From("UserGroups ug").
LeftJoin(`
(SELECT
GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.GroupId, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = ug.Id`, teamID).
LeftJoin(`(
SELECT
GroupMembers.GroupId, COUNT(*) AS MemberCount
FROM
GroupMembers
LEFT JOIN
Users ON Users.Id = GroupMembers.UserId
WHERE
GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
GROUP BY GroupId) AS Members
ON Members.GroupId = ug.Id`).
Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("ug.DisplayName")
}
if opts.FilterAllowReference {
query = query.Where("ug.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(ug.Name %[1]s ? OR ug.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, error) {
countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectCountGroups, teamId, opts)
countQueryString, args, err := countQuery.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_groups_by_team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, countQueryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups with teamId=%s", teamId)
}
return count, nil
}
func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectGroups, teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_by_team_tosql")
}
groups := groupsWithSchemeAdmin{}
err = s.GetReplicaX().Select(&groups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
query := s.getGroupsAssociatedToChannelsByTeam(teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_associated_to_channel_by_team_tosql")
}
tgroups := groupsAssociatedToChannelWithSchemeAdmin{}
err = s.GetReplicaX().Select(&tgroups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
groups := map[string][]*model.GroupWithSchemeAdmin{}
for _, tgroup := range tgroups {
group := tgroup.groupWithSchemeAdmin.ToModel()
groups[tgroup.ChannelId] = append(groups[tgroup.ChannelId], group)
}
return groups, nil
}
func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
groupsVar := groups{}
selectQuery := []string{"g.*"}
if opts.IncludeMemberCount {
selectQuery = append(selectQuery, "coalesce(Members.MemberCount, 0) AS MemberCount")
}
if opts.IncludeChannelMemberCount != "" {
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberCount, 0) AS ChannelMemberCount")
if opts.IncludeTimezones {
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberTimezonesCount, 0) AS ChannelMemberTimezonesCount")
}
}
groupsQuery := s.getQueryBuilder().Select(strings.Join(selectQuery, ", "))
if opts.IncludeMemberCount {
countQuery := s.getQueryBuilder().
Select("GroupMembers.GroupId, COUNT(DISTINCT u.Id) AS MemberCount").
From("GroupMembers").
LeftJoin("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"u.DeleteAt": 0}).
GroupBy("GroupId")
countQuery = applyViewRestrictionsFilter(countQuery, viewRestrictions, false)
countString, params, err := countQuery.PlaceholderFormat(sq.Question).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_tosql")
}
groupsQuery = groupsQuery.
LeftJoin("("+countString+") AS Members ON Members.GroupId = g.Id", params...)
}
if opts.IncludeChannelMemberCount != "" {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
joinStr := ""
if opts.IncludeTimezones {
if s.DriverName() == model.DatabaseDriverMysql {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'true' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.automaticTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.automaticTimezone')
WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'false' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.manualTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.manualTimezone')
END
)) AS ChannelMemberTimezonesCount`
} else if s.DriverName() == model.DatabaseDriverPostgres {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
THEN Timezone->>'automaticTimezone'
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
THEN Timezone->>'manualTimezone'
END
)) AS ChannelMemberTimezonesCount`
}
joinStr = "LEFT JOIN Users ON Users.Id = GroupMembers.UserId"
}
groupsQuery = groupsQuery.
LeftJoin("(SELECT "+selectStr+" FROM ChannelMembers LEFT JOIN GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0 "+joinStr+" WHERE ChannelMembers.ChannelId = ? GROUP BY GroupId) AS ChannelMembers ON ChannelMembers.GroupId = g.Id", opts.IncludeChannelMemberCount)
}
if opts.FilterHasMember != "" {
groupsQuery = groupsQuery.
LeftJoin("GroupMembers ON GroupMembers.GroupId = g.Id").
Where("GroupMembers.UserId = ?", opts.FilterHasMember).
Where("GroupMembers.DeleteAt = 0")
}
groupsQuery = groupsQuery.
From("UserGroups g").
OrderBy("g.DisplayName")
if opts.Since > 0 {
groupsQuery = groupsQuery.Where(sq.Gt{
"g.UpdateAt": opts.Since,
})
} else {
groupsQuery = groupsQuery.Where("g.DeleteAt = 0")
}
if perPage != 0 {
groupsQuery = groupsQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
}
if opts.FilterAllowReference {
groupsQuery = groupsQuery.Where("g.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
groupsQuery = groupsQuery.Where(fmt.Sprintf("(g.Name %[1]s ? OR g.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
if len(opts.NotAssociatedToTeam) == 26 {
groupsQuery = groupsQuery.Where(`
g.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupTeams ON GroupTeams.GroupId = UserGroups.Id
WHERE
GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupTeams.TeamId = ?
)
`, opts.NotAssociatedToTeam)
}
if len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
g.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupChannels ON GroupChannels.GroupId = UserGroups.Id
WHERE
GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupChannels.ChannelId = ?
)
`, opts.NotAssociatedToChannel)
}
if opts.FilterParentTeamPermitted && len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
CASE
WHEN (
SELECT
Teams.GroupConstrained
FROM
Teams
JOIN Channels ON Channels.TeamId = Teams.Id
WHERE
Channels.Id = ?
) THEN g.Id IN (
SELECT
GroupId
FROM
GroupTeams
WHERE
GroupTeams.DeleteAt = 0
AND GroupTeams.TeamId = (
SELECT
TeamId
FROM
Channels
WHERE
Id = ?
)
)
ELSE TRUE
END
`, opts.NotAssociatedToChannel, opts.NotAssociatedToChannel)
}
if opts.Source != "" {
groupsQuery = groupsQuery.Where("g.Source = ?", opts.Source)
}
queryString, args, err := groupsQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_tosql")
}
if err = s.GetReplicaX().Select(&groupsVar, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Groups")
}
return groupsVar.ToModel(), nil
}
func (s *SqlGroupStore) teamMembersMinusGroupMembersQuery(teamID string, groupIDs []string, isCount bool) sq.SelectBuilder {
var selectStr string
if isCount {
selectStr = "count(DISTINCT Users.Id)"
} else {
tmpl := "Users.*, coalesce(TeamMembers.SchemeGuest, false) SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser, %s AS GroupIDs"
if s.DriverName() == model.DatabaseDriverMysql {
selectStr = fmt.Sprintf(tmpl, "group_concat(UserGroups.Id)")
} else {
selectStr = fmt.Sprintf(tmpl, "string_agg(UserGroups.Id, ',')")
}
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder := s.getQueryBuilder().Select(selectStr).
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
Join("Users ON Users.Id = TeamMembers.UserId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("TeamMembers.DeleteAt = 0").
Where("Teams.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Teams.Id = ?", teamID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, TeamMembers.SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser")
}
return builder
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
func (s *SqlGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
query := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false)
query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_minus_group_members")
}
users := []*model.UserWithGroups{}
if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountTeamMembersMinusGroupMembers returns the count of the set of users on the given team minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
queryString, args, err := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_team_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers minus GroupMembers")
}
return count, nil
}
func (s *SqlGroupStore) channelMembersMinusGroupMembersQuery(channelID string, groupIDs []string, isCount bool) sq.SelectBuilder {
var selectStr string
if isCount {
selectStr = "count(DISTINCT Users.Id)"
} else {
tmpl := "Users.*, coalesce(ChannelMembers.SchemeGuest, false) SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser, %s AS GroupIDs"
if s.DriverName() == model.DatabaseDriverMysql {
selectStr = fmt.Sprintf(tmpl, "group_concat(UserGroups.Id)")
} else {
selectStr = fmt.Sprintf(tmpl, "string_agg(UserGroups.Id, ',')")
}
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder := s.getQueryBuilder().Select(selectStr).
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
Join("Users ON Users.Id = ChannelMembers.UserId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("Channels.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Channels.Id = ?", channelID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, ChannelMembers.SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser")
}
return builder
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
func (s *SqlGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
query := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, false)
query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_minus_group_members_tosql")
}
users := []*model.UserWithGroups{}
if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountChannelMembersMinusGroupMembers returns the count of the set of users in the given channel minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
queryString, args, err := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_channel_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count ChannelMembers")
}
return count, nil
}
func (s *SqlGroupStore) AdminRoleGroupsForSyncableMember(userID, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
var groupIds []string
query := fmt.Sprintf(`
SELECT
GroupMembers.GroupId
FROM
GroupMembers
INNER JOIN
Group%[1]ss ON Group%[1]ss.GroupId = GroupMembers.GroupId
WHERE
GroupMembers.UserId = ?
AND GroupMembers.DeleteAt = 0
AND %[1]sId = ?
AND Group%[1]ss.DeleteAt = 0
AND Group%[1]ss.SchemeAdmin = TRUE`, syncableType)
err := s.GetReplicaX().Select(&groupIds, query, userID, syncableID)
if err != nil {
return nil, errors.Wrap(err, "failed to find Group ids")
}
return groupIds, nil
}
func (s *SqlGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
builder := s.getQueryBuilder().Select("UserId").
From(fmt.Sprintf("Group%ss", syncableType)).
Join(fmt.Sprintf("GroupMembers ON GroupMembers.GroupId = Group%ss.GroupId AND Group%[1]ss.SchemeAdmin = TRUE AND GroupMembers.DeleteAt = 0", syncableType.String())).Where(fmt.Sprintf("Group%[1]ss.%[1]sId = ?", syncableType.String()), syncableID)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "permitted_syncable_admins_tosql")
}
var userIDs []string
if err = s.GetMasterX().Select(&userIDs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find User ids")
}
return userIDs, nil
}
func (s *SqlGroupStore) GroupCount() (int64, error) {
return s.countTable("UserGroups")
}
func (s *SqlGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"Source": source, "DeleteAt": 0})
}
func (s *SqlGroupStore) GroupTeamCount() (int64, error) {
return s.countTable("GroupTeams")
}
func (s *SqlGroupStore) GroupChannelCount() (int64, error) {
return s.countTable("GroupChannels")
}
func (s *SqlGroupStore) GroupMemberCount() (int64, error) {
return s.countTable("GroupMembers")
}
func (s *SqlGroupStore) DistinctGroupMemberCount() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(DISTINCT UserId)", "GroupMembers", nil)
}
func (s *SqlGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
builder := s.getQueryBuilder().
Select("COUNT(DISTINCT GroupMembers.UserId)").
From("GroupMembers").
Join("UserGroups ON GroupMembers.GroupId = UserGroups.Id").
Where(sq.Eq{"UserGroups.Source": source, "GroupMembers.DeleteAt": 0})
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "distinct_group_member_count_for_source_tosql")
}
var count int64
if err = s.GetReplicaX().Get(&count, query, args...); err != nil {
return 0, errors.Wrapf(err, "failed to select distinct groupmember count for source %q", source)
}
return count, nil
}
func (s *SqlGroupStore) GroupCountWithAllowReference() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"AllowReference": true, "DeleteAt": 0})
}
func (s *SqlGroupStore) countTable(tableName string) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", tableName, nil)
}
func (s *SqlGroupStore) countTableWithSelectAndWhere(selectStr, tableName string, whereStmt map[string]any) (int64, error) {
if whereStmt == nil {
whereStmt = sq.Eq{"DeleteAt": 0}
}
query := s.getQueryBuilder().Select(selectStr).From(tableName).Where(whereStmt)
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_table_with_select_and_where_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count from table %s", tableName)
}
return count, nil
}
func (s *SqlGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, args, err := s.buildUpsertMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members, err
}
func (s *SqlGroupStore) buildUpsertMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []any, err error) {
var retrievedGroup model.Group
// Check Group exists
if err = s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil {
err = errors.Wrapf(err, "failed to get UserGroup with groupId=%s", groupID)
return
}
// Check Users exist
if err = s.checkUsersExist(userIDs); err != nil {
return
}
builder := s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
members = make([]*model.GroupMember, 0, len(userIDs))
createAt := model.GetMillis()
for _, userId := range userIDs {
member := &model.GroupMember{
GroupId: groupID,
UserId: userId,
CreateAt: createAt,
DeleteAt: 0,
}
builder = builder.Values(member.GroupId, member.UserId, member.CreateAt, member.DeleteAt)
members = append(members, member)
}
if s.DriverName() == model.DatabaseDriverMysql {
builder = builder.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE CreateAt = ?, DeleteAt = ?", createAt, 0))
} else if s.DriverName() == model.DatabaseDriverPostgres {
builder = builder.SuffixExpr(sq.Expr("ON CONFLICT (groupid, userid) DO UPDATE SET CreateAt = ?, DeleteAt = ?", createAt, 0))
}
query, args, err = builder.ToSql()
return
}
func (s *SqlGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, args, err := s.buildDeleteMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to delete GroupMembers")
}
return members, err
}
func (s *SqlGroupStore) buildDeleteMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []any, err error) {
membersSelectQuery, membersSelectArgs, err := s.getQueryBuilder().
Select("*").
From("GroupMembers").
Where(sq.And{
sq.Eq{"GroupId": groupID},
sq.Eq{"UserId": userIDs},
sq.Eq{"DeleteAt": 0},
}).
ToSql()
if err != nil {
return
}
err = s.GetReplicaX().Select(&members, membersSelectQuery, membersSelectArgs...)
if err != nil {
return
}
if len(members) != len(userIDs) {
retrievedRecords := make(map[string]bool)
for _, member := range members {
retrievedRecords[member.UserId] = true
}
for _, userID := range userIDs {
if _, ok := retrievedRecords[userID]; !ok {
err = store.NewErrNotFound("User", userID)
return
}
}
}
deleteAt := model.GetMillis()
for _, member := range members {
member.DeleteAt = deleteAt
}
builder := s.getQueryBuilder().
Update("GroupMembers").
Set("DeleteAt", deleteAt).
Where(sq.And{
sq.Eq{"GroupId": groupID},
sq.Eq{"UserId": userIDs},
})
query, args, err = builder.ToSql()
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type relationalCheckConfig struct {
parentName string
parentIdAttr string
childName string
childIdAttr string
canParentIdBeEmpty bool
sortRecords bool
filter any
}
func getOrphanedRecords(ss *SqlStore, cfg relationalCheckConfig) ([]model.OrphanedRecord, error) {
records := []model.OrphanedRecord{}
sub := ss.getQueryBuilder().
Select("TRUE").
From(cfg.parentName + " AS PT").
Prefix("NOT EXISTS (").
Suffix(")").
Where("PT.id = CT." + cfg.parentIdAttr)
main := ss.getQueryBuilder().
Select().
Column("CT." + cfg.parentIdAttr + " AS ParentId").
From(cfg.childName + " AS CT").
Where(sub)
if cfg.childIdAttr != "" {
main = main.Column("CT." + cfg.childIdAttr + " AS ChildId")
}
if cfg.canParentIdBeEmpty {
main = main.Where(sq.NotEq{"CT." + cfg.parentIdAttr: ""})
}
if cfg.filter != nil {
main = main.Where(cfg.filter)
}
if cfg.sortRecords {
main = main.OrderBy("CT." + cfg.parentIdAttr)
}
query, args, err := main.ToSql()
if err != nil {
return nil, err
}
err = ss.GetMasterX().Select(&records, query, args...)
return records, err
}
func checkParentChildIntegrity(ss *SqlStore, config relationalCheckConfig) model.IntegrityCheckResult {
var result model.IntegrityCheckResult
var data model.RelationalIntegrityCheckData
config.sortRecords = true
data.Records, result.Err = getOrphanedRecords(ss, config)
if result.Err != nil {
mlog.Error("Error while getting orphaned records", mlog.Err(result.Err))
return result
}
data.ParentName = config.parentName
data.ChildName = config.childName
data.ParentIdAttr = config.parentIdAttr
data.ChildIdAttr = config.childIdAttr
result.Data = data
return result
}
func checkChannelsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "ChannelMemberHistory",
childIdAttr: "",
})
}
func checkChannelsChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "ChannelMembers",
childIdAttr: "",
})
}
func checkChannelsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "Posts",
childIdAttr: "Id",
})
}
func checkChannelsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkCommandsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Commands",
parentIdAttr: "CommandId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkPostsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "PostId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkPostsPostsRootIdIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "RootId",
childName: "Posts",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkPostsReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "PostId",
childName: "Reactions",
childIdAttr: "",
})
}
func checkSchemesChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Schemes",
parentIdAttr: "SchemeId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkSchemesTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Schemes",
parentIdAttr: "SchemeId",
childName: "Teams",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkSessionsAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Sessions",
parentIdAttr: "SessionId",
childName: "Audits",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkTeamsChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
res1 := checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Channels",
childIdAttr: "Id",
filter: sq.NotEq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
})
res2 := checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
filter: sq.Eq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
})
data1 := res1.Data.(model.RelationalIntegrityCheckData)
data2 := res2.Data.(model.RelationalIntegrityCheckData)
data1.Records = append(data1.Records, data2.Records...)
res1.Data = data1
return res1
}
func checkTeamsCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Commands",
childIdAttr: "Id",
})
}
func checkTeamsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkTeamsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkTeamsTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "TeamMembers",
childIdAttr: "",
})
}
func checkUsersAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Audits",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkUsersCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkUsersChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "ChannelMemberHistory",
childIdAttr: "",
})
}
func checkUsersChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "ChannelMembers",
childIdAttr: "",
})
}
func checkUsersChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkUsersCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Commands",
childIdAttr: "Id",
})
}
func checkUsersCompliancesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Compliances",
childIdAttr: "Id",
})
}
func checkUsersEmojiIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Emoji",
childIdAttr: "Id",
})
}
func checkUsersFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkUsersIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkUsersOAuthAccessDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "OAuthAccessData",
childIdAttr: "Token",
})
}
func checkUsersOAuthAppsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "OAuthApps",
childIdAttr: "Id",
})
}
func checkUsersOAuthAuthDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "OAuthAuthData",
childIdAttr: "Code",
})
}
func checkUsersOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkUsersPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Posts",
childIdAttr: "Id",
})
}
func checkUsersPreferencesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Preferences",
childIdAttr: "",
})
}
func checkUsersReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Reactions",
childIdAttr: "",
})
}
func checkUsersSessionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Sessions",
childIdAttr: "Id",
})
}
func checkUsersStatusIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Status",
childIdAttr: "",
})
}
func checkUsersTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "TeamMembers",
childIdAttr: "",
})
}
func checkUsersUserAccessTokensIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "UserAccessTokens",
childIdAttr: "Id",
})
}
func checkChannelsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkChannelsCommandWebhooksIntegrity(ss)
results <- checkChannelsChannelMemberHistoryIntegrity(ss)
results <- checkChannelsChannelMembersIntegrity(ss)
results <- checkChannelsIncomingWebhooksIntegrity(ss)
results <- checkChannelsOutgoingWebhooksIntegrity(ss)
results <- checkChannelsPostsIntegrity(ss)
results <- checkChannelsFileInfoIntegrity(ss)
}
func checkCommandsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkCommandsCommandWebhooksIntegrity(ss)
}
func checkPostsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkPostsFileInfoIntegrity(ss)
results <- checkPostsPostsRootIdIntegrity(ss)
results <- checkPostsReactionsIntegrity(ss)
results <- checkThreadsTeamsIntegrity(ss)
}
func checkSchemesIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkSchemesChannelsIntegrity(ss)
results <- checkSchemesTeamsIntegrity(ss)
}
func checkSessionsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkSessionsAuditsIntegrity(ss)
}
func checkTeamsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkTeamsChannelsIntegrity(ss)
results <- checkTeamsCommandsIntegrity(ss)
results <- checkTeamsIncomingWebhooksIntegrity(ss)
results <- checkTeamsOutgoingWebhooksIntegrity(ss)
results <- checkTeamsTeamMembersIntegrity(ss)
}
func checkUsersIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkUsersAuditsIntegrity(ss)
results <- checkUsersCommandWebhooksIntegrity(ss)
results <- checkUsersChannelMemberHistoryIntegrity(ss)
results <- checkUsersChannelMembersIntegrity(ss)
results <- checkUsersChannelsIntegrity(ss)
results <- checkUsersCommandsIntegrity(ss)
results <- checkUsersCompliancesIntegrity(ss)
results <- checkUsersEmojiIntegrity(ss)
results <- checkUsersFileInfoIntegrity(ss)
results <- checkUsersIncomingWebhooksIntegrity(ss)
results <- checkUsersOAuthAccessDataIntegrity(ss)
results <- checkUsersOAuthAppsIntegrity(ss)
results <- checkUsersOAuthAuthDataIntegrity(ss)
results <- checkUsersOutgoingWebhooksIntegrity(ss)
results <- checkUsersPostsIntegrity(ss)
results <- checkUsersPreferencesIntegrity(ss)
results <- checkUsersReactionsIntegrity(ss)
results <- checkUsersSessionsIntegrity(ss)
results <- checkUsersStatusIntegrity(ss)
results <- checkUsersTeamMembersIntegrity(ss)
results <- checkUsersUserAccessTokensIntegrity(ss)
}
func checkThreadsTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "ThreadTeamId",
childName: "Threads",
childIdAttr: "PostId",
canParentIdBeEmpty: false,
})
}
func CheckRelationalIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
mlog.Info("Starting relational integrity checks...")
checkChannelsIntegrity(ss, results)
checkCommandsIntegrity(ss, results)
checkPostsIntegrity(ss, results)
checkSchemesIntegrity(ss, results)
checkSessionsIntegrity(ss, results)
checkTeamsIntegrity(ss, results)
checkUsersIntegrity(ss, results)
mlog.Info("Done with relational integrity checks")
close(results)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
jobsCleanupDelay = 100 * time.Millisecond
)
type SqlJobStore struct {
*SqlStore
}
func newSqlJobStore(sqlStore *SqlStore) store.JobStore {
return &SqlJobStore{sqlStore}
}
func (jss SqlJobStore) Save(job *model.Job) (*model.Job, error) {
jsonData, err := json.Marshal(job.Data)
if err != nil {
return nil, errors.Wrap(err, "failed marshalling job data")
}
if jss.IsBinaryParamEnabled() {
jsonData = AppendBinaryFlag(jsonData)
}
query := jss.getQueryBuilder().
Insert("Jobs").
Columns("Id", "Type", "Priority", "CreateAt", "StartAt", "LastActivityAt", "Status", "Progress", "Data").
Values(job.Id, job.Type, job.Priority, job.CreateAt, job.StartAt, job.LastActivityAt, job.Status, job.Progress, jsonData)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = jss.GetMasterX().Exec(queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to save Preference")
}
return job, nil
}
func (jss SqlJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
dataJSON, jsonErr := json.Marshal(job.Data)
if jsonErr != nil {
return false, errors.Wrap(jsonErr, "failed to encode job's data to JSON")
}
if jss.IsBinaryParamEnabled() {
dataJSON = AppendBinaryFlag(dataJSON)
}
query, args, err := jss.getQueryBuilder().
Update("Jobs").
Set("LastActivityAt", model.GetMillis()).
Set("Status", job.Status).
Set("Data", dataJSON).
Set("Progress", job.Progress).
Where(sq.Eq{"Id": job.Id, "Status": currentStatus}).ToSql()
if err != nil {
return false, errors.Wrap(err, "job_tosql")
}
sqlResult, err := jss.GetMasterX().Exec(query, args...)
if err != nil {
return false, errors.Wrap(err, "failed to update Job")
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
}
if rows != 1 {
return false, nil
}
return true, nil
}
func (jss SqlJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
job := &model.Job{
Id: id,
Status: status,
LastActivityAt: model.GetMillis(),
}
if _, err := jss.GetMasterX().NamedExec(`UPDATE Jobs
SET Status=:Status, LastActivityAt=:LastActivityAt
WHERE Id=:Id`, job); err != nil {
return nil, errors.Wrapf(err, "failed to update Job with id=%s", id)
}
return job, nil
}
func (jss SqlJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
builder := jss.getQueryBuilder().
Update("Jobs").
Set("LastActivityAt", model.GetMillis()).
Set("Status", newStatus).
Where(sq.Eq{"Id": id, "Status": currentStatus})
if newStatus == model.JobStatusInProgress {
builder = builder.Set("StartAt", model.GetMillis())
}
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrap(err, "job_tosql")
}
sqlResult, err := jss.GetMasterX().Exec(query, args...)
if err != nil {
return false, errors.Wrapf(err, "failed to update Job with id=%s", id)
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
}
if rows != 1 {
return false, nil
}
return true, nil
}
func (jss SqlJobStore) Get(id string) (*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Id": id}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var status model.Job
if err = jss.GetReplicaX().Get(&status, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Job", id)
}
return nil, errors.Wrapf(err, "failed to get Job with id=%s", id)
}
return &status, nil
}
func (jss SqlJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Jobs")
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobTypes}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var jobs []*model.Job
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with types")
}
return jobs, nil
}
func (jss SqlJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType}).
OrderBy("CreateAt DESC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType, "Status": status}).
OrderBy("CreateAt DESC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
jobs := []*model.Job{}
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return jobs, nil
}
func (jss SqlJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
statuses := []*model.Job{}
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Status": status}).
OrderBy("CreateAt ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with status=%s", status)
}
return statuses, nil
}
func (jss SqlJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
return jss.GetNewestJobByStatusesAndType([]string{status}, jobType)
}
func (jss SqlJobStore) GetNewestJobByStatusesAndType(status []string, jobType string) (*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Status": status, "Type": jobType}).
OrderBy("CreateAt DESC").
Limit(1).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var job model.Job
if err = jss.GetReplicaX().Get(&job, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Job", fmt.Sprintf("<status, type>=<%s, %s>", strings.Join(status, ","), jobType))
}
return nil, errors.Wrapf(err, "failed to find Job with statuses=%s and type=%s", strings.Join(status, ","), jobType)
}
return &job, nil
}
func (jss SqlJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
query, args, err := jss.getQueryBuilder().
Select("COUNT(*)").
From("Jobs").
Where(sq.Eq{"Status": status, "Type": jobType}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "job_tosql")
}
var count int64
err = jss.GetReplicaX().Get(&count, query, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Jobs with status=%s and type=%s", status, jobType)
}
return count, nil
}
func (jss SqlJobStore) Delete(id string) (string, error) {
query, args, err := jss.getQueryBuilder().
Delete("Jobs").
Where(sq.Eq{"Id": id}).ToSql()
if err != nil {
return "", errors.Wrap(err, "job_tosql")
}
if _, err = jss.GetMasterX().Exec(query, args...); err != nil {
return "", errors.Wrapf(err, "failed to delete Job with id=%s", id)
}
return id, nil
}
func (jss SqlJobStore) Cleanup(expiryTime int64, batchSize int) error {
var query string
if jss.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Jobs WHERE Id IN (SELECT Id FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?)"
} else {
query = "DELETE FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?"
}
var rowsAffected int64 = 1
for rowsAffected > 0 {
sqlResult, err := jss.GetMasterX().Exec(query,
expiryTime, model.JobStatusInProgress, model.JobStatusPending, batchSize)
if err != nil {
return errors.Wrap(err, "unable to delete jobs")
}
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete jobs")
}
time.Sleep(jobsCleanupDelay)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// SqlLicenseStore encapsulates the database writes and reads for
// model.LicenseRecord objects.
type SqlLicenseStore struct {
*SqlStore
}
func newSqlLicenseStore(sqlStore *SqlStore) store.LicenseStore {
return &SqlLicenseStore{sqlStore}
}
// Save validates and stores the license instance in the database. The Id
// and Bytes fields are mandatory. The Bytes field is limited to a maximum
// of 10000 bytes. If the license ID matches an existing license in the
// database it returns the license stored in the database. If not, it saves the
// new database and returns the created license with the CreateAt field
// updated.
func (ls SqlLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
license.PreSave()
if err := license.IsValid(); err != nil {
return nil, err
}
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses").
Where(sq.Eq{"Id": license.Id})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_tosql")
}
var storedLicense model.LicenseRecord
if err := ls.GetReplicaX().Get(&storedLicense, queryString, args...); err != nil {
// Only insert if not exists
query, args, err := ls.getQueryBuilder().
Insert("Licenses").
Columns("Id", "CreateAt", "Bytes").
Values(license.Id, license.CreateAt, license.Bytes).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_record_tosql")
}
if _, err := ls.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get License with licenseId=%s", license.Id)
}
return license, nil
}
return &storedLicense, nil
}
// Get obtains the license with the provided id parameter from the database.
// If the license doesn't exist it returns a model.AppError with
// http.StatusNotFound in the StatusCode field.
func (ls SqlLicenseStore) Get(id string) (*model.LicenseRecord, error) {
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses").
Where(sq.Eq{"Id": id})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_record_tosql")
}
license := &model.LicenseRecord{}
if err := ls.GetReplicaX().Get(license, queryString, args...); err != nil {
return nil, store.NewErrNotFound("License", id)
}
return license, nil
}
func (ls SqlLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses")
queryString, _, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_tosql")
}
licenses := []*model.LicenseRecord{}
if err := ls.GetReplicaX().Select(&licenses, queryString); err != nil {
return nil, errors.Wrap(err, "failed to fetch licenses")
}
return licenses, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlLinkMetadataStore struct {
*SqlStore
}
func newSqlLinkMetadataStore(sqlStore *SqlStore) store.LinkMetadataStore {
return &SqlLinkMetadataStore{sqlStore}
}
func (s SqlLinkMetadataStore) Save(metadata *model.LinkMetadata) (*model.LinkMetadata, error) {
if err := metadata.IsValid(); err != nil {
return nil, err
}
metadata.PreSave()
metadataBytes, err := json.Marshal(metadata.Data)
if err != nil {
return nil, errors.Wrap(err, "could not serialize metadataBytes to JSON")
}
if s.IsBinaryParamEnabled() {
metadataBytes = AppendBinaryFlag(metadataBytes)
}
query := s.getQueryBuilder().
Insert("LinkMetadata").
Columns("Hash", "URL", "Timestamp", "Type", "Data").
Values(metadata.Hash, metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (hash) DO UPDATE SET URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
}
q, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "metadata_tosql")
}
_, err = s.GetMasterX().Exec(q, args...)
if err != nil && !IsUniqueConstraintError(err, []string{"PRIMARY", "linkmetadata_pkey"}) {
return nil, errors.Wrap(err, "could not save link metadata")
}
return metadata, nil
}
func (s SqlLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
var metadata model.LinkMetadata
query, args, err := s.getQueryBuilder().
Select("*").
From("LinkMetadata").
Where(sq.Eq{"URL": url, "Timestamp": timestamp}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not create query with querybuilder")
}
err = s.GetReplicaX().Get(&metadata, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("LinkMetadata", "url="+url)
}
return nil, errors.Wrapf(err, "could not get metadata with selectone: url=%s", url)
}
err = metadata.DeserializeDataToConcreteType()
if err != nil {
return nil, errors.Wrapf(err, "could not deserialize metadata to concrete type for url=%s", url)
}
return &metadata, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlNotifyAdminStore struct {
*SqlStore
}
func newSqlNotifyAdminStore(sqlStore *SqlStore) store.NotifyAdminStore {
return &SqlNotifyAdminStore{sqlStore}
}
func (s SqlNotifyAdminStore) insert(data *model.NotifyAdminData) (sql.Result, error) {
query := `INSERT INTO NotifyAdmin (UserId, CreateAt, RequiredPlan, RequiredFeature, Trial) VALUES (:UserId, :CreateAt, :RequiredPlan, :RequiredFeature, :Trial)`
return s.GetMasterX().NamedExec(query, data)
}
func (s SqlNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
if err := data.IsValid(); err != nil {
return nil, err
}
data.PreSave()
_, err := s.insert(data)
if err != nil {
return nil, errors.Wrap(err, "failed to save Notify Admin data")
}
return data, nil
}
func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
data := []*model.NotifyAdminData{}
query, args, err := s.getQueryBuilder().
Select("*").
From("NotifyAdmin").
Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notification data by user id and required feature")
}
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("NotifyAdmin", fmt.Sprintf("user id: %s and required feature: %s", userId, feature))
}
return nil, errors.Wrapf(err, "notifcation data by user id: %s and required feature: %s", userId, feature)
}
return data, nil
}
func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
data := []*model.NotifyAdminData{}
query, args, err := s.getQueryBuilder().
Select("*").
From("NotifyAdmin").
Where(sq.Eq{"Trial": trial}).
Where("(SentAt IS NULL)").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data")
}
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "notifcation data")
}
return data, nil
}
func (s SqlNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
if _, err := s.GetMasterX().Exec("DELETE FROM NotifyAdmin WHERE Trial = ? AND CreateAt < ? AND SentAt IS NULL", trial, now); err != nil {
return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial)
}
return nil
}
func (s SqlNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
if _, err := s.GetMasterX().Exec("UPDATE NotifyAdmin SET SentAt = ? WHERE UserId = ? AND RequiredPlan = ? AND RequiredFeature = ?", now, userId, requiredPlan, requiredFeature); err != nil {
return errors.Wrapf(err, "failed to update SentAt for userId=%s and requiredPlan=%s", userId, requiredPlan)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlOAuthStore struct {
*SqlStore
}
func newSqlOAuthStore(sqlStore *SqlStore) store.OAuthStore {
return &SqlOAuthStore{sqlStore}
}
func (as SqlOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
if app.Id != "" {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
app.PreSave()
if err := app.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthApps
(Id, CreatorId, CreateAt, UpdateAt, ClientSecret, Name, Description, IconURL, CallbackUrls, Homepage, IsTrusted, MattermostAppID)
VALUES
(:Id, :CreatorId, :CreateAt, :UpdateAt, :ClientSecret, :Name, :Description, :IconURL, :CallbackUrls, :Homepage, :IsTrusted, :MattermostAppID)`, app); err != nil {
return nil, errors.Wrap(err, "failed to save OAuthApp")
}
return app, nil
}
func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
app.PreUpdate()
if err := app.IsValid(); err != nil {
return nil, err
}
var oldApp model.OAuthApp
err := as.GetMasterX().Get(&oldApp, `SELECT * FROM OAuthApps
WHERE id=?`, app.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", app.Id)
}
if oldApp.Id == "" {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
app.CreateAt = oldApp.CreateAt
app.CreatorId = oldApp.CreatorId
res, err := as.GetMasterX().NamedExec(`UPDATE OAuthApps
SET UpdateAt=:UpdateAt, ClientSecret=:ClientSecret, Name=:Name,
Description=:Description, IconURL=:IconURL, CallbackUrls=:CallbackUrls,
Homepage=:Homepage, IsTrusted=:IsTrusted, MattermostAppID=:MattermostAppID
WHERE Id=:Id`, app)
if err != nil {
return nil, errors.Wrapf(err, "failed to update OAuthApp with id=%s", app.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
return app, nil
}
func (as SqlOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
var app model.OAuthApp
if err := as.GetReplicaX().Get(&app, `SELECT * FROM OAuthApps WHERE Id=?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("OAuthApp", id)
}
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", id)
}
if app.Id == "" {
return nil, store.NewErrNotFound("OAuthApp", id)
}
return &app, nil
}
func (as SqlOAuthStore) GetAppByUser(userId string, offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = ? LIMIT ? OFFSET ?", userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
}
return apps, nil
}
func (as SqlOAuthStore) GetApps(offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps LIMIT ? OFFSET ?", limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find OAuthApps")
}
return apps, nil
}
func (as SqlOAuthStore) GetAuthorizedApps(userId string, offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps,
`SELECT o.* FROM OAuthApps AS o INNER JOIN
Preferences AS p ON p.Name=o.Id AND p.UserId=? LIMIT ? OFFSET ?`, userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
}
return apps, nil
}
func (as SqlOAuthStore) DeleteApp(id string) (err error) {
// wrap in a transaction so that if one fails, everything fails
transaction, err := as.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := as.deleteApp(transaction, id); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
if err := accessData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAccessData
(ClientId, UserId, Token, RefreshToken, RedirectUri, ExpiresAt, Scope)
VALUES
(:ClientId, :UserId, :Token, :RefreshToken, :RedirectUri, :ExpiresAt, :Scope)`, accessData); err != nil {
return nil, errors.Wrap(err, "failed to save AccessData")
}
return accessData, nil
}
func (as SqlOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = ?", token); err != nil {
return nil, errors.Wrapf(err, "failed to get OAuthAccessData with token=%s", token)
}
return &accessData, nil
}
func (as SqlOAuthStore) GetAccessDataByUserForApp(userID, clientID string) ([]*model.AccessData, error) {
accessData := []*model.AccessData{}
if err := as.GetReplicaX().Select(&accessData,
"SELECT * FROM OAuthAccessData WHERE UserId = ? AND ClientId = ?", userID, clientID); err != nil {
return nil, errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s and clientId=%s", userID, clientID)
}
return accessData, nil
}
func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE RefreshToken = ?", token); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthAccessData with refreshToken=%s", token)
}
return &accessData, nil
}
func (as SqlOAuthStore) GetPreviousAccessData(userID, clientID string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE ClientId = ? AND UserId = ?", clientID, userID); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to get AccessData with clientId=%s and userId=%s", clientID, userID)
}
return &accessData, nil
}
func (as SqlOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
if err := accessData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec("UPDATE OAuthAccessData SET Token = :Token, ExpiresAt = :ExpiresAt, RefreshToken = :RefreshToken WHERE ClientId = :ClientId AND UserID = :UserId", accessData); err != nil {
return nil, errors.Wrapf(err, "failed to update OAuthAccessData with userId=%s and clientId=%s", accessData.UserId, accessData.ClientId)
}
return accessData, nil
}
func (as SqlOAuthStore) RemoveAccessData(token string) error {
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE Token = ?", token); err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with token=%s", token)
}
return nil
}
func (as SqlOAuthStore) RemoveAllAccessData() error {
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData"); err != nil {
return errors.Wrap(err, "failed to delete OAuthAccessData")
}
return nil
}
func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
authData.PreSave()
if err := authData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAuthData
(ClientId, UserId, Code, ExpiresIn, CreateAt, RedirectUri, State, Scope)
VALUES
(:ClientId, :UserId, :Code, :ExpiresIn, :CreateAt, :RedirectUri, :State, :Scope)`, authData); err != nil {
return nil, errors.Wrap(err, "failed to save AuthData")
}
return authData, nil
}
func (as SqlOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
var authData model.AuthData
err := as.GetReplicaX().Get(&authData, `SELECT * FROM OAuthAuthData WHERE Code=?`, code)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
}
return nil, errors.Wrapf(err, "failed to get AuthData with code=%s", code)
}
if authData.Code == "" {
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
}
return &authData, nil
}
func (as SqlOAuthStore) RemoveAuthData(code string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE Code = ?", code)
if err != nil {
return errors.Wrapf(err, "failed to delete AuthData with code=%s", code)
}
return nil
}
func (as SqlOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE ClientId = ? and UserId = ?", clientId, userId)
if err != nil {
return errors.Wrapf(err, "failed to delete AuthData with clientId=%s and userId=%s", clientId, userId)
}
return nil
}
func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s", userId)
}
return nil
}
func (as SqlOAuthStore) deleteApp(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec("DELETE FROM OAuthApps WHERE Id = ?", clientId); err != nil {
return errors.Wrapf(err, "failed to delete OAuthApp with id=%s", clientId)
}
return as.deleteOAuthAppSessions(transaction, clientId)
}
func (as SqlOAuthStore) deleteOAuthAppSessions(transaction *sqlxTxWrapper, clientId string) error {
query := ""
if as.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING OAuthAccessData o WHERE o.Token = s.Token AND o.ClientId = ?"
} else if as.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN OAuthAccessData o ON o.Token = s.Token WHERE o.ClientId = ?"
}
if _, err := transaction.Exec(query, clientId); err != nil {
return errors.Wrapf(err, "failed to delete Session with OAuthAccessData.Id=%s", clientId)
}
return as.deleteOAuthTokens(transaction, clientId)
}
func (as SqlOAuthStore) deleteOAuthTokens(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec("DELETE FROM OAuthAccessData WHERE ClientId = ?", clientId); err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with id=%s", clientId)
}
return as.deleteAppExtras(transaction, clientId)
}
func (as SqlOAuthStore) deleteAppExtras(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec(
`DELETE FROM
Preferences
WHERE
Category = ?
AND Name = ?`, model.PreferenceCategoryAuthorizedOAuthApp, clientId); err != nil {
return errors.Wrapf(err, "failed to delete Preferences with name=%s", clientId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
defaultPluginKeyFetchLimit = 10
)
type SqlPluginStore struct {
*SqlStore
}
func newSqlPluginStore(sqlStore *SqlStore) store.PluginStore {
return &SqlPluginStore{sqlStore}
}
func (ps SqlPluginStore) SaveOrUpdate(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
if err := kv.IsValid(); err != nil {
return nil, err
}
if kv.Value == nil {
// Setting a key to nil is the same as removing it
err := ps.Delete(kv.PluginId, kv.Key)
if err != nil {
return nil, err
}
return kv, nil
}
query := ps.getQueryBuilder().
Insert("PluginKeyValueStore").
Columns("PluginId", "PKey", "PValue", "ExpireAt").
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt)
if ps.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (pluginid, pkey) DO UPDATE SET PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
} else if ps.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to upsert PluginKeyValue")
}
return kv, nil
}
func (ps SqlPluginStore) CompareAndSet(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
if err := kv.IsValid(); err != nil {
return false, err
}
if kv.Value == nil {
// Setting a key to nil is the same as removing it
return ps.CompareAndDelete(kv, oldValue)
}
if oldValue == nil {
// Delete any existing, expired value.
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.NotEq{"ExpireAt": int(0)}).
Where(sq.Lt{"ExpireAt": model.GetMillis()})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
if _, err = ps.GetMasterX().Exec(queryString, args...); err != nil {
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
}
// Insert if oldValue is nil
queryString, args, err = ps.getQueryBuilder().
Insert("PluginKeyValueStore").
Columns("PluginId", "PKey", "PValue", "ExpireAt").
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt).ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
// If the error is from unique constraints violation, it's the result of a
// race condition, return false and no error. Otherwise we have a real error and
// need to return it.
if IsUniqueConstraintError(err, []string{"PRIMARY", "PluginId", "Key", "PKey", "pkey"}) {
return false, nil
}
return false, errors.Wrap(err, "failed to insert PluginKeyValue")
}
} else {
currentTime := model.GetMillis()
// Update if oldValue is not nil
query := ps.getQueryBuilder().
Update("PluginKeyValueStore").
Set("PValue", kv.Value).
Set("ExpireAt", kv.ExpireAt).
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": oldValue}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": currentTime},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
updateResult, err := ps.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to update PluginKeyValue")
}
if rowsAffected, err := updateResult.RowsAffected(); err != nil {
// Failed to update
return false, errors.Wrap(err, "unable to get rows affected")
} else if rowsAffected == 0 {
if ps.DriverName() == model.DatabaseDriverMysql && bytes.Equal(oldValue, kv.Value) {
// ROW_COUNT on MySQL is zero even if the row existed but no changes to the row were required.
// Check if the row exists with the required value to distinguish this case. Strictly speaking,
// this isn't a good use of CompareAndSet anyway, since there's no corresponding guarantee of
// atomicity. Nevertheless, let's return results consistent with Postgres and with what might
// be expected in this case.
query := ps.getQueryBuilder().
Select("COUNT(*)").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": kv.Value}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": currentTime},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
var count int64
err = ps.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return false, errors.Wrapf(err, "failed to count PluginKeyValue with pluginId=%s and key=%s", kv.PluginId, kv.Key)
}
if count == 0 {
return false, nil
} else if count == 1 {
return true, nil
} else {
return false, errors.Wrapf(err, "got too many rows when counting PluginKeyValue with pluginId=%s, key=%s, rows=%d", kv.PluginId, kv.Key, count)
}
}
// No rows were affected by the update, where condition was not satisfied,
// return false, but no error.
return false, nil
}
}
return true, nil
}
func (ps SqlPluginStore) CompareAndDelete(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
if err := kv.IsValid(); err != nil {
return false, err
}
if oldValue == nil {
// nil can't be stored. Return showing that we didn't do anything
return false, nil
}
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": oldValue}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": model.GetMillis()},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
deleteResult, err := ps.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
}
if rowsAffected, err := deleteResult.RowsAffected(); err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
} else if rowsAffected == 0 {
return false, nil
}
return true, nil
}
func (ps SqlPluginStore) SetWithOptions(pluginId string, key string, value []byte, opt model.PluginKVSetOptions) (bool, error) {
if err := opt.IsValid(); err != nil {
return false, err
}
kv, err := model.NewPluginKeyValueFromOptions(pluginId, key, value, opt)
if err != nil {
return false, err
}
if opt.Atomic {
return ps.CompareAndSet(kv, opt.OldValue)
}
savedKv, nErr := ps.SaveOrUpdate(kv)
if nErr != nil {
return false, nErr
}
return savedKv != nil, nil
}
func (ps SqlPluginStore) Get(pluginId, key string) (*model.PluginKeyValue, error) {
currentTime := model.GetMillis()
query := ps.getQueryBuilder().Select("PluginId, PKey, PValue, ExpireAt").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Eq{"PKey": key}).
Where(sq.Or{sq.Eq{"ExpireAt": 0}, sq.Gt{"ExpireAt": currentTime}})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
row := ps.GetReplicaX().QueryRowx(queryString, args...)
var kv model.PluginKeyValue
if err := row.Scan(&kv.PluginId, &kv.Key, &kv.Value, &kv.ExpireAt); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("PluginKeyValue", fmt.Sprintf("pluginId=%s, key=%s", pluginId, key))
}
return nil, errors.Wrapf(err, "failed to get PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
}
return &kv, nil
}
func (ps SqlPluginStore) Delete(pluginId, key string) error {
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Eq{"Pkey": key})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to delete PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
}
return nil
}
func (ps SqlPluginStore) DeleteAllForPlugin(pluginId string) error {
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to get all PluginKeyValues with pluginId=%s ", pluginId)
}
return nil
}
func (ps SqlPluginStore) DeleteAllExpired() error {
currentTime := model.GetMillis()
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.NotEq{"ExpireAt": 0}).
Where(sq.Lt{"ExpireAt": currentTime})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to delete all expired PluginKeyValues")
}
return nil
}
func (ps SqlPluginStore) List(pluginId string, offset int, limit int) ([]string, error) {
if limit <= 0 {
limit = defaultPluginKeyFetchLimit
}
if offset <= 0 {
offset = 0
}
query := ps.getQueryBuilder().
Select("Pkey").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": model.GetMillis()},
}).
OrderBy("PKey").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
keys := []string{}
err = ps.GetReplicaX().Select(&keys, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PluginKeyValues with pluginId=%s", pluginId)
}
return keys, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlPostAcknowledgementStore struct {
*SqlStore
}
func newSqlPostAcknowledgementStore(sqlStore *SqlStore) store.PostAcknowledgementStore {
return &SqlPostAcknowledgementStore{sqlStore}
}
func (s *SqlPostAcknowledgementStore) Get(postID, userID string) (*model.PostAcknowledgement, error) {
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postID},
sq.Eq{"UserId": userID},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgement model.PostAcknowledgement
err := s.GetReplicaX().GetBuilder(&acknowledgement, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("PostAcknowledgement", postID)
}
return nil, err
}
return &acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
if acknowledgedAt == 0 {
acknowledgedAt = model.GetMillis()
}
acknowledgement := &model.PostAcknowledgement{
UserId: userID,
PostId: postID,
AcknowledgedAt: acknowledgedAt,
}
if err := acknowledgement.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.getQueryBuilder().
Insert("PostAcknowledgements").
Columns("PostId", "UserId", "AcknowledgedAt").
Values(acknowledgement.PostId, acknowledgement.UserId, acknowledgement.AcknowledgedAt)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
}
_, err = transaction.ExecBuilder(query)
if err != nil {
return nil, err
}
err = updatePost(transaction, acknowledgement.PostId)
if err != nil {
return nil, err
}
err = transaction.Commit()
if err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.getQueryBuilder().
Update("PostAcknowledgements").
Set("AcknowledgedAt", 0).
Where(sq.And{
sq.Eq{"PostId": acknowledgement.PostId},
sq.Eq{"UserId": acknowledgement.UserId},
})
_, err = transaction.ExecBuilder(query)
if err != nil {
return err
}
err = updatePost(transaction, acknowledgement.PostId)
if err != nil {
return err
}
err = transaction.Commit()
if err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.NotEq{"AcknowledgedAt": 0},
sq.Eq{"PostId": postID},
})
err := s.GetReplicaX().SelectBuilder(&acknowledgements, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for postID=%s", postID)
}
return acknowledgements, nil
}
func (s *SqlPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
perPage := 200
for i := 0; i < len(postIds); i += perPage {
j := i + perPage
if len(postIds) < j {
j = len(postIds)
}
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postIds[i:j]},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgementsBatch []*model.PostAcknowledgement
err := s.GetReplicaX().SelectBuilder(&acknowledgementsBatch, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for post list")
}
acknowledgements = append(acknowledgements, acknowledgementsBatch...)
}
return acknowledgements, nil
}
func updatePost(transaction *sqlxTxWrapper, postId string) error {
_, err := transaction.Exec(
`UPDATE
Posts
SET
UpdateAt = ?
WHERE
Id = ?`,
model.GetMillis(),
postId,
)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlPostPriorityStore struct {
*SqlStore
}
func newSqlPostPriorityStore(sqlStore *SqlStore) store.PostPriorityStore {
return &SqlPostPriorityStore{
SqlStore: sqlStore,
}
}
func (s *SqlPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
query := s.getQueryBuilder().
Select("Priority", "RequestedAck", "PersistentNotifications").
From("PostsPriority").
Where(sq.Eq{"PostId": postId})
var postPriority model.PostPriority
err := s.GetReplicaX().GetBuilder(&postPriority, query)
if err != nil {
return nil, err
}
return &postPriority, nil
}
func (s *SqlPostPriorityStore) GetForPosts(postIds []string) ([]*model.PostPriority, error) {
var priority []*model.PostPriority
perPage := 200
for i := 0; i < len(postIds); i += perPage {
j := i + perPage
if len(postIds) < j {
j = len(postIds)
}
query := s.getQueryBuilder().
Select("PostId", "Priority", "RequestedAck", "PersistentNotifications").
From("PostsPriority").
Where(sq.Eq{"PostId": postIds[i:j]})
var priorityBatch []*model.PostPriority
err := s.GetReplicaX().SelectBuilder(&priority, query)
if err != nil {
return nil, err
}
priority = append(priority, priorityBatch...)
}
return priority, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlPostStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
maxPostSizeOnce sync.Once
maxPostSizeCached int
}
type postWithExtra struct {
ThreadReplyCount int64
IsFollowing *bool
ThreadParticipants model.StringArray
model.Post
}
func (s *SqlPostStore) ClearCaches() {
}
func postSliceColumnsWithTypes() []struct {
Name string
Type reflect.Kind
} {
return []struct {
Name string
Type reflect.Kind
}{
{"Id", reflect.String},
{"CreateAt", reflect.Int64},
{"UpdateAt", reflect.Int64},
{"EditAt", reflect.Int64},
{"DeleteAt", reflect.Int64},
{"IsPinned", reflect.Bool},
{"UserId", reflect.String},
{"ChannelId", reflect.String},
{"RootId", reflect.String},
{"OriginalId", reflect.String},
{"Message", reflect.String},
{"Type", reflect.String},
{"Props", reflect.Map},
{"Hashtags", reflect.String},
{"Filenames", reflect.Slice},
{"FileIds", reflect.Slice},
{"HasReactions", reflect.Bool},
{"RemoteId", reflect.String},
}
}
func postToSlice(post *model.Post) []any {
return []any{
post.Id,
post.CreateAt,
post.UpdateAt,
post.EditAt,
post.DeleteAt,
post.IsPinned,
post.UserId,
post.ChannelId,
post.RootId,
post.OriginalId,
post.Message,
post.Type,
model.StringInterfaceToJSON(post.Props),
post.Hashtags,
model.ArrayToJSON(post.Filenames),
model.ArrayToJSON(post.FileIds),
post.HasReactions,
post.RemoteId,
}
}
func postSliceColumns() []string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
cols[i] = colInfo.Name
}
return cols
}
func postSliceCoalesceQuery() string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
var defaultValue string
switch colInfo.Type {
case reflect.String:
defaultValue = "''"
case reflect.Int64:
defaultValue = "0"
case reflect.Bool:
defaultValue = "false"
case reflect.Map:
defaultValue = "'{}'"
case reflect.Slice:
defaultValue = "'[]'"
}
cols[i] = "COALESCE(Posts." + colInfo.Name + "," + defaultValue + ") AS " + colInfo.Name
}
return strings.Join(cols, ",")
}
func newSqlPostStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.PostStore {
return &SqlPostStore{
SqlStore: sqlStore,
metrics: metrics,
maxPostSizeCached: model.PostMessageMaxRunesV1,
}
}
func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
channelNewPosts := make(map[string]int)
channelNewRootPosts := make(map[string]int)
maxDateNewPosts := make(map[string]int64)
maxDateNewRootPosts := make(map[string]int64)
rootIds := make(map[string]int)
maxDateRootIds := make(map[string]int64)
for idx, post := range posts {
if post.Id != "" && !post.IsRemote() {
return nil, idx, store.NewErrInvalidInput("Post", "id", post.Id)
}
post.PreSave()
maxPostSize := s.GetMaxPostSize()
if err := post.IsValid(maxPostSize); err != nil {
return nil, idx, err
}
if currentChannelCount, ok := channelNewPosts[post.ChannelId]; !ok {
if post.IsJoinLeaveMessage() {
channelNewPosts[post.ChannelId] = 0
} else {
channelNewPosts[post.ChannelId] = 1
}
maxDateNewPosts[post.ChannelId] = post.CreateAt
} else {
if !post.IsJoinLeaveMessage() {
channelNewPosts[post.ChannelId] = currentChannelCount + 1
}
if post.CreateAt > maxDateNewPosts[post.ChannelId] {
maxDateNewPosts[post.ChannelId] = post.CreateAt
}
}
if post.RootId == "" {
if currentChannelCount, ok := channelNewRootPosts[post.ChannelId]; !ok {
if post.IsJoinLeaveMessage() {
channelNewRootPosts[post.ChannelId] = 0
} else {
channelNewRootPosts[post.ChannelId] = 1
}
maxDateNewRootPosts[post.ChannelId] = post.CreateAt
} else {
if !post.IsJoinLeaveMessage() {
channelNewRootPosts[post.ChannelId] = currentChannelCount + 1
}
if post.CreateAt > maxDateNewRootPosts[post.ChannelId] {
maxDateNewRootPosts[post.ChannelId] = post.CreateAt
}
}
continue
}
if currentRootCount, ok := rootIds[post.RootId]; !ok {
rootIds[post.RootId] = 1
maxDateRootIds[post.RootId] = post.CreateAt
} else {
rootIds[post.RootId] = currentRootCount + 1
if post.CreateAt > maxDateRootIds[post.RootId] {
maxDateRootIds[post.RootId] = post.CreateAt
}
}
}
builder := s.getQueryBuilder().Insert("Posts").Columns(postSliceColumns()...)
for _, post := range posts {
builder = builder.Values(postToSlice(post)...)
}
query, args, err := builder.ToSql()
if err != nil {
return nil, -1, errors.Wrap(err, "post_tosql")
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return posts, -1, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if _, err = transaction.Exec(query, args...); err != nil {
return nil, -1, errors.Wrap(err, "failed to save Post")
}
if err = s.updateThreadsFromPosts(transaction, posts); err != nil {
return nil, -1, errors.Wrap(err, "update thread from posts failed")
}
if err = s.savePostsPriority(transaction, posts); err != nil {
return nil, -1, errors.Wrap(err, "failed to save PostPriority")
}
if err = transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return posts, -1, errors.Wrap(err, "commit_transaction")
}
for channelId, count := range channelNewPosts {
countRoot := channelNewRootPosts[channelId]
if _, err = s.GetMasterX().NamedExec(`UPDATE Channels
SET LastPostAt = GREATEST(:lastpostat, LastPostAt),
LastRootPostAt = GREATEST(:lastrootpostat, LastRootPostAt),
TotalMsgCount = TotalMsgCount + :count,
TotalMsgCountRoot = TotalMsgCountRoot + :countroot
WHERE Id = :channelid`, map[string]any{
"lastpostat": maxDateNewPosts[channelId],
"lastrootpostat": maxDateNewRootPosts[channelId],
"channelid": channelId,
"count": count,
"countroot": countRoot,
}); err != nil {
mlog.Warn("Error updating Channel LastPostAt.", mlog.Err(err))
}
}
for rootId := range rootIds {
if _, err = s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", maxDateRootIds[rootId], rootId); err != nil {
mlog.Warn("Error updating Post UpdateAt.", mlog.Err(err))
}
}
var unknownRepliesPosts []*model.Post
for _, post := range posts {
if post.RootId == "" {
count, ok := rootIds[post.Id]
if ok {
post.ReplyCount += int64(count)
}
} else {
unknownRepliesPosts = append(unknownRepliesPosts, post)
}
}
if len(unknownRepliesPosts) > 0 {
if err := s.populateReplyCount(unknownRepliesPosts); err != nil {
mlog.Warn("Unable to populate the reply count in some posts.", mlog.Err(err))
}
}
return posts, -1, nil
}
func (s *SqlPostStore) Save(post *model.Post) (*model.Post, error) {
posts, _, err := s.SaveMultiple([]*model.Post{post})
if err != nil {
return nil, err
}
return posts[0], nil
}
func (s *SqlPostStore) populateReplyCount(posts []*model.Post) error {
rootIds := []string{}
for _, post := range posts {
rootIds = append(rootIds, post.RootId)
}
countList := []struct {
RootId string
Count int64
}{}
query := s.getQueryBuilder().
Select("RootId, COUNT(Id) AS Count").
From("Posts").
Where(sq.Eq{"RootId": rootIds}).
Where(sq.Eq{"Posts.DeleteAt": 0}).
GroupBy("RootId")
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "post_tosql")
}
err = s.GetMasterX().Select(&countList, queryString, args...)
if err != nil {
return errors.Wrap(err, "failed to count Posts")
}
counts := map[string]int64{}
for _, count := range countList {
counts[count.RootId] = count.Count
}
for _, post := range posts {
count, ok := counts[post.RootId]
if !ok {
post.ReplyCount = 0
}
post.ReplyCount = count
}
return nil
}
func (s *SqlPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
newPost.UpdateAt = model.GetMillis()
newPost.PreCommit()
oldPost.DeleteAt = newPost.UpdateAt
oldPost.UpdateAt = newPost.UpdateAt
oldPost.OriginalId = oldPost.Id
oldPost.Id = model.NewId()
oldPost.PreCommit()
maxPostSize := s.GetMaxPostSize()
if err := newPost.IsValid(maxPostSize); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`UPDATE Posts
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
EditAt=:EditAt,
DeleteAt=:DeleteAt,
IsPinned=:IsPinned,
UserId=:UserId,
ChannelId=:ChannelId,
RootId=:RootId,
OriginalId=:OriginalId,
Message=:Message,
Type=:Type,
Props=:Props,
Hashtags=:Hashtags,
Filenames=:Filenames,
FileIds=:FileIds,
HasReactions=:HasReactions,
RemoteId=:RemoteId
WHERE
Id=:Id
`, newPost); err != nil {
return nil, errors.Wrapf(err, "failed to update Post with id=%s", newPost.Id)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ? AND LastPostAt < ?", time, newPost.ChannelId, time); err != nil {
return nil, errors.Wrap(err, "failed to update lastpostat of channels")
}
if newPost.RootId != "" {
if _, err := s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ? AND UpdateAt < ?", time, newPost.RootId, time); err != nil {
return nil, errors.Wrap(err, "failed to update updateAt of posts")
}
}
// mark the old post as deleted
builder := s.getQueryBuilder().
Insert("Posts").
Columns(postSliceColumns()...).
Values(postToSlice(oldPost)...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to insert the old post")
}
return newPost, nil
}
func (s *SqlPostStore) OverwriteMultiple(posts []*model.Post) (_ []*model.Post, _ int, err error) {
updateAt := model.GetMillis()
maxPostSize := s.GetMaxPostSize()
for idx, post := range posts {
post.UpdateAt = updateAt
if appErr := post.IsValid(maxPostSize); appErr != nil {
return nil, idx, appErr
}
}
tx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, -1, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
for idx, post := range posts {
if _, err2 := tx.NamedExec(`UPDATE Posts
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
EditAt=:EditAt,
DeleteAt=:DeleteAt,
IsPinned=:IsPinned,
UserId=:UserId,
ChannelId=:ChannelId,
RootId=:RootId,
OriginalId=:OriginalId,
Message=:Message,
Type=:Type,
Props=:Props,
Hashtags=:Hashtags,
Filenames=:Filenames,
FileIds=:FileIds,
HasReactions=:HasReactions,
RemoteId=:RemoteId
WHERE
Id=:Id
`, post); err2 != nil {
return nil, idx, errors.Wrapf(err2, "failed to update Post with id=%s", post.Id)
}
if post.RootId != "" {
if _, err2 := tx.Exec("UPDATE Threads SET LastReplyAt = ? WHERE PostId = ?", updateAt, post.Id); err2 != nil {
return nil, idx, errors.Wrapf(err2, "failed to update Threads with postid=%s", post.Id)
}
}
}
err = tx.Commit()
if err != nil {
return nil, -1, errors.Wrap(err, "commit_transaction")
}
return posts, -1, nil
}
func (s *SqlPostStore) Overwrite(post *model.Post) (*model.Post, error) {
posts, _, err := s.OverwriteMultiple([]*model.Post{post})
if err != nil {
return nil, err
}
return posts[0], nil
}
func (s *SqlPostStore) GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, "", "", offset, limit)
}
func (s *SqlPostStore) GetFlaggedPostsForTeam(userId, teamId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, "", teamId, offset, limit)
}
func (s *SqlPostStore) GetFlaggedPostsForChannel(userId, channelId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, channelId, "", offset, limit)
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) getFlaggedPosts(userId, channelId, teamId string, offset int, limit int) (*model.PostList, error) {
pl := model.NewPostList()
posts := []*model.Post{}
query := `
SELECT
A.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN A.RootId = '' THEN A.Id ELSE A.RootId END) AND Posts.DeleteAt = 0) as ReplyCount
FROM
(SELECT
*
FROM
Posts
WHERE
Id
IN
(
SELECT
Name
FROM
Preferences
WHERE
UserId = ?
AND Category = ?
)
CHANNEL_FILTER
AND Posts.DeleteAt = 0
) as A
INNER JOIN Channels as B
ON B.Id = A.ChannelId
WHERE
ChannelId IN (
SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId
AND UserId = ?
)
TEAM_FILTER
ORDER BY CreateAt DESC
LIMIT ? OFFSET ?`
queryParams := []any{userId, model.PreferenceCategoryFlaggedPost}
var channelClause, teamClause string
channelClause, queryParams = s.buildFlaggedPostChannelFilterClause(channelId, queryParams)
query = strings.Replace(query, "CHANNEL_FILTER", channelClause, 1)
queryParams = append(queryParams, userId)
teamClause, queryParams = s.buildFlaggedPostTeamFilterClause(teamId, queryParams)
query = strings.Replace(query, "TEAM_FILTER", teamClause, 1)
queryParams = append(queryParams, limit, offset)
if err := s.GetReplicaX().Select(&posts, query, queryParams...); err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
for _, post := range posts {
pl.AddPost(post)
pl.AddOrder(post.Id)
}
return pl, nil
}
func (s *SqlPostStore) buildFlaggedPostTeamFilterClause(teamId string, queryParams []any) (string, []any) {
if teamId == "" {
return "", queryParams
}
return "AND B.TeamId = ? OR B.TeamId = ''", append(queryParams, teamId)
}
func (s *SqlPostStore) buildFlaggedPostChannelFilterClause(channelId string, queryParams []any) (string, []any) {
if channelId == "" {
return "", queryParams
}
return "AND ChannelId = ?", append(queryParams, channelId)
}
func (s *SqlPostStore) getPostWithCollapsedThreads(id, userID string, opts model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
if id == "" {
return nil, store.NewErrInvalidInput("Post", "id", id)
}
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var post postWithExtra
postFetchQuery, args, err := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", userID).
Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.Id": id}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostWithCollapsedThreads_ToSql2")
}
err = s.GetReplicaX().Get(&post, postFetchQuery, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
posts := []*model.Post{}
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{
"Posts.RootId": id,
"Posts.DeleteAt": 0,
})
var sort string
if opts.Direction != "" {
if opts.Direction == "up" {
sort = "DESC"
} else if opts.Direction == "down" {
sort = "ASC"
}
}
if sort != "" {
query = query.OrderBy("CreateAt " + sort + ", Id " + sort)
}
if opts.FromCreateAt != 0 {
if opts.Direction == "down" {
direction := sq.Gt{"Posts.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"Posts.CreateAt": opts.FromCreateAt},
sq.Gt{"Posts.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
} else {
direction := sq.Lt{"Posts.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"Posts.CreateAt": opts.FromCreateAt},
sq.Lt{"Posts.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
}
}
if opts.PerPage != 0 {
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostWithCollapsedThreads_Tosql2")
}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts for thread %s", id)
}
var hasNext bool
if opts.PerPage != 0 {
if len(posts) == opts.PerPage+1 {
hasNext = true
}
}
if hasNext {
// Shave off the last item.
posts = posts[:len(posts)-1]
}
list, err := s.prepareThreadedResponse([]*postWithExtra{&post}, opts.CollapsedThreadsExtended, false, sanitizeOptions)
if err != nil {
return nil, err
}
for _, p := range posts {
list.AddPost(p)
list.AddOrder(p.Id)
}
list.HasNext = hasNext
return list, nil
}
func (s *SqlPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
if opts.CollapsedThreads {
return s.getPostWithCollapsedThreads(id, userID, opts, sanitizeOptions)
}
pl := model.NewPostList()
if id == "" {
return nil, store.NewErrInvalidInput("Post", "id", id)
}
var post model.Post
postFetchQuery := "SELECT p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id = ? AND p.DeleteAt = 0"
err := s.DBXFromContext(ctx).Get(&post, postFetchQuery, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
pl.AddPost(&post)
pl.AddOrder(id)
if !opts.SkipFetchThreads {
rootId := post.RootId
if rootId == "" {
rootId = post.Id
}
if rootId == "" {
return nil, errors.Wrapf(err, "invalid rootId with value=%s", rootId)
}
query := s.getQueryBuilder().
Select("p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount").
From("Posts p").
Where(sq.Or{
sq.Eq{"p.Id": rootId},
sq.Eq{"p.RootId": rootId},
}).
Where(sq.Eq{"p.DeleteAt": 0})
var sort string
if opts.Direction != "" {
if opts.Direction == "up" {
sort = "DESC"
} else if opts.Direction == "down" {
sort = "ASC"
}
}
if sort != "" {
query = query.OrderBy("CreateAt " + sort + ", Id " + sort)
}
if opts.FromCreateAt != 0 {
if opts.Direction == "down" {
direction := sq.Gt{"p.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"p.CreateAt": opts.FromCreateAt},
sq.Gt{"p.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
} else {
direction := sq.Lt{"p.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"p.CreateAt": opts.FromCreateAt},
sq.Lt{"p.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
}
}
if opts.PerPage != 0 {
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Get_Tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
var hasNext bool
if opts.PerPage != 0 {
if len(posts) == opts.PerPage+1 {
hasNext = true
}
}
if hasNext {
// Shave off the last item
posts = posts[:len(posts)-1]
}
for _, p := range posts {
if p.Id == id {
// Based on the conditions above such as sq.Or{ sq.Eq{"p.Id": rootId}, sq.Eq{"p.RootId": rootId}, }
// posts may contain the "id" post which has already been fetched and added in the "pl"
// So, skip the "id" to avoid duplicate entry of the post
continue
}
pl.AddPost(p)
pl.AddOrder(p.Id)
}
pl.HasNext = hasNext
}
return pl, nil
}
func (s *SqlPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
query := s.getQueryBuilder().
Select("p.*").
From("Posts p").
Where(sq.Eq{"p.Id": id})
replyCountSubQuery := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0"))
if !inclDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getsingleincldeleted_tosql")
}
var post model.Post
err = s.GetReplicaX().Get(&post, queryString, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
return &post, nil
}
type etagPosts struct {
Id string
UpdateAt int64
}
//nolint:unparam
func (s *SqlPostStore) InvalidateLastPostTimeCache(channelId string) {
}
//nolint:unparam
func (s *SqlPostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string {
q := s.getQueryBuilder().Select("Id", "UpdateAt").From("Posts").Where(sq.Eq{"ChannelId": channelId}).OrderBy("UpdateAt DESC").Limit(1)
if collapsedThreads {
q.Where(sq.Eq{"RootId": ""})
}
sql, args := q.MustSql()
var et etagPosts
err := s.GetReplicaX().Get(&et, sql, args...)
var result string
if err != nil {
result = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
} else {
result = fmt.Sprintf("%v.%v", model.CurrentVersion, et.UpdateAt)
}
return result
}
// Soft deletes a post
// and cleans up the thread if it's a comment
func (s *SqlPostStore) Delete(postID string, time int64, deleteByID string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
id := postIds{}
// TODO: change this to later delete thread directly from postID
err = transaction.Get(&id, "SELECT RootId, UserId FROM Posts WHERE Id = ?", postID)
if err != nil {
if err == sql.ErrNoRows {
return store.NewErrNotFound("Post", postID)
}
return errors.Wrapf(err, "failed to delete Post with id=%s", postID)
}
if s.DriverName() == model.DatabaseDriverPostgres {
_, err = transaction.Exec(`UPDATE Posts
SET DeleteAt = $1,
UpdateAt = $1,
Props = jsonb_set(Props, $2, $3)
WHERE Id = $4 OR RootId = $4`, time, jsonKeyPath(model.PostPropsDeleteBy), jsonStringVal(deleteByID), postID)
} else {
// We use ORDER BY clause for MySQL
// to trigger filesort optimization in the index_merge.
// Without it, MySQL does a temporary sort.
// See: https://dev.mysql.com/doc/refman/8.0/en/order-by-optimization.html#order-by-filesort.
_, err = transaction.Exec(`UPDATE Posts
SET DeleteAt = ?,
UpdateAt = ?,
Props = JSON_SET(Props, ?, ?)
Where Id = ? OR RootId = ?
ORDER BY Id`, time, time, "$."+model.PostPropsDeleteBy, deleteByID, postID, postID)
}
if err != nil {
return errors.Wrap(err, "failed to update Posts")
}
if id.RootId == "" {
err = s.deleteThread(transaction, postID, time)
} else {
err = s.updateThreadAfterReplyDeletion(transaction, id.RootId, id.UserId)
}
if err != nil {
return errors.Wrapf(err, "failed to cleanup Thread with postid=%s", id.RootId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) permanentDelete(postId string) (err error) {
var post model.Post
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Get(&post, "SELECT * FROM Posts WHERE Id = ?", postId)
if err != nil && err != sql.ErrNoRows {
return errors.Wrapf(err, "failed to get Post with id=%s", postId)
}
if err = s.permanentDeleteThreads(transaction, post.Id); err != nil {
return errors.Wrapf(err, "failed to cleanup threads for Post with id=%s", postId)
}
if _, err = transaction.NamedExec("DELETE FROM Posts WHERE Id = :id OR RootId = :rootid", map[string]any{"id": postId, "rootid": postId}); err != nil {
return errors.Wrapf(err, "failed to delete Post with id=%s", postId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
type postIds struct {
Id string
RootId string
UserId string
}
func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) (err error) {
results := []postIds{}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Select(&results, "Select Id, RootId FROM Posts WHERE UserId = ? AND RootId != ''", userId)
if err != nil {
return errors.Wrapf(err, "failed to fetch Posts with userId=%s", userId)
}
_, err = transaction.Exec("DELETE FROM Posts WHERE UserId = ? AND RootId != ''", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId)
}
for _, ids := range results {
if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil {
return err
}
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
// Permanently deletes all comments by user,
// cleans up threads (removes said user from participants and decreases reply count),
// permanent delete all root posts by user,
// and delete threads and thread memberships for those root posts
func (s *SqlPostStore) PermanentDeleteByUser(userId string) error {
// First attempt to delete all the comments for a user
if err := s.permanentDeleteAllCommentByUser(userId); err != nil {
return err
}
// Now attempt to delete all the root posts for a user. This will also
// delete all the comments for each post
found := true
count := 0
for found {
var ids []string
err := s.GetMasterX().Select(&ids, "SELECT Id FROM Posts WHERE UserId = ? LIMIT 1000", userId)
if err != nil {
return errors.Wrapf(err, "failed to find Posts with userId=%s", userId)
}
found = false
for _, id := range ids {
found = true
if err = s.permanentDelete(id); err != nil {
return err
}
}
// This is a fail safe, give up if more than 10k messages
count++
if count >= 10 {
return errors.Wrapf(err, "too many Posts to delete with userId=%s", userId)
}
}
return nil
}
// Permanent deletes all channel root posts and comments,
// deletes all threads and thread memberships
// no thread comment cleanup needed, since we are deleting threads and thread memberships
func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
results := []postIds{}
err = transaction.Select(&results, "SELECT Id, RootId, UserId FROM Posts WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId)
}
for _, ids := range results {
if err = s.permanentDeleteThreads(transaction, ids.Id); err != nil {
return err
}
}
if _, err = transaction.Exec("DELETE FROM Posts WHERE ChannelId = ?", channelId); err != nil {
return errors.Wrapf(err, "failed to delete Posts with channelId=%s", channelId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) prepareThreadedResponse(posts []*postWithExtra, extended, reversed bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
list := model.NewPostList()
var userIds []string
userIdMap := map[string]bool{}
for _, thread := range posts {
for _, participantId := range thread.ThreadParticipants {
if _, ok := userIdMap[participantId]; !ok {
userIdMap[participantId] = true
userIds = append(userIds, participantId)
}
}
}
// usersMap is the global profile map of all participants from all threads.
usersMap := make(map[string]*model.User, len(userIds))
if extended {
users, err := s.User().GetProfileByIds(context.Background(), userIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, err
}
for _, user := range users {
user.SanitizeProfile(sanitizeOptions)
usersMap[user.Id] = user
}
} else {
for _, userId := range userIds {
usersMap[userId] = &model.User{Id: userId}
}
}
processPost := func(p *postWithExtra) error {
p.Post.ReplyCount = p.ThreadReplyCount
if p.IsFollowing != nil {
p.Post.IsFollowing = model.NewBool(*p.IsFollowing)
}
for _, userID := range p.ThreadParticipants {
participant, ok := usersMap[userID]
if !ok {
return errors.New("cannot find thread participant with id=" + userID)
}
p.Post.Participants = append(p.Post.Participants, participant)
}
return nil
}
l := len(posts)
for i := range posts {
idx := i
// We need to flip the order if we selected backwards
if reversed {
idx = l - i - 1
}
if err := processPost(posts[idx]); err != nil {
return nil, err
}
post := &posts[idx].Post
list.AddPost(post)
list.AddOrder(posts[idx].Id)
}
return list, nil
}
func (s *SqlPostStore) getPostsCollapsedThreads(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var posts []*postWithExtra
offset := options.PerPage * options.Page
postFetchQuery, args, _ := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Eq{"Posts.RootId": ""}).
Limit(uint64(options.PerPage)).
Offset(uint64(offset)).
OrderBy("Posts.CreateAt DESC").ToSql()
err := s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false, sanitizeOptions)
}
func (s *SqlPostStore) GetPosts(options model.GetPostsOptions, _ bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.PerPage > 1000 {
return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage)
}
if options.CollapsedThreads {
return s.getPostsCollapsedThreads(options, sanitizeOptions)
}
offset := options.PerPage * options.Page
rpc := make(chan store.StoreResult, 1)
go func() {
posts, err := s.getRootPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads, options.IncludeDeleted)
rpc <- store.StoreResult{Data: posts, NErr: err}
close(rpc)
}()
cpc := make(chan store.StoreResult, 1)
go func() {
posts, err := s.getParentsPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads, options.IncludeDeleted)
cpc <- store.StoreResult{Data: posts, NErr: err}
close(cpc)
}()
list := model.NewPostList()
rpr := <-rpc
if rpr.NErr != nil {
return nil, rpr.NErr
}
cpr := <-cpc
if cpr.NErr != nil {
return nil, cpr.NErr
}
posts := rpr.Data.([]*model.Post)
parents := cpr.Data.([]*model.Post)
for _, p := range posts {
list.AddPost(p)
list.AddOrder(p.Id)
}
for _, p := range parents {
list.AddPost(p)
}
list.MakeNonNil()
return list, nil
}
func (s *SqlPostStore) getPostsSinceCollapsedThreads(options model.GetPostsSinceOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var posts []*postWithExtra
postFetchQuery, args, err := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Gt{"Posts.UpdateAt": options.Time}).
Where(sq.Eq{"Posts.RootId": ""}).
OrderBy("Posts.CreateAt DESC").
Limit(1000).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getPostsSinceCollapsedThreads_ToSql")
}
err = s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false, sanitizeOptions)
}
//nolint:unparam
func (s *SqlPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.CollapsedThreads {
return s.getPostsSinceCollapsedThreads(options, sanitizeOptions)
}
posts := []*model.Post{}
order := "DESC"
if options.SortAscending {
order = "ASC"
}
replyCountQuery1 := ""
replyCountQuery2 := ""
if options.SkipFetchThreads {
replyCountQuery1 = `, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p1.RootId = '' THEN p1.Id ELSE p1.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
replyCountQuery2 = `, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN cte.RootId = '' THEN cte.Id ELSE cte.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
}
var query string
var params []any
// union of IDs and then join to get full posts is faster in mysql
if s.DriverName() == model.DatabaseDriverMysql {
query = `SELECT *` + replyCountQuery1 + ` FROM Posts p1 JOIN (
(SELECT
Id
FROM
Posts p2
WHERE
(UpdateAt > ?
AND ChannelId = ?)
LIMIT 1000)
UNION
(SELECT
Id
FROM
Posts p3
WHERE
Id
IN
(SELECT * FROM (SELECT
RootId
FROM
Posts
WHERE
UpdateAt > ?
AND ChannelId = ?
LIMIT 1000) temp_tab))
) j ON p1.Id = j.Id
ORDER BY CreateAt ` + order
params = []any{options.Time, options.ChannelId, options.Time, options.ChannelId}
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = `WITH cte AS (SELECT
*
FROM
Posts
WHERE
UpdateAt > ? AND ChannelId = ?
LIMIT 1000)
(SELECT *` + replyCountQuery2 + ` FROM cte)
UNION
(SELECT *` + replyCountQuery1 + ` FROM Posts p1 WHERE id in (SELECT rootid FROM cte))
ORDER BY CreateAt ` + order
params = []any{options.Time, options.ChannelId}
}
err := s.GetReplicaX().Select(&posts, query, params...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
list := model.NewPostList()
for _, p := range posts {
list.AddPost(p)
if p.UpdateAt > options.Time {
list.AddOrder(p.Id)
}
}
return list, nil
}
func (s *SqlPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
query := `
SELECT EXISTS (SELECT 1
FROM
Posts
WHERE
UpdateAt >= ?
AND
ChannelId = ?
AND
UserId = ?
AND
Type = ?
LIMIT 1)`
var exist bool
err := s.GetReplicaX().Get(&exist, query, options.Time, options.ChannelId, userId, model.PostTypeAutoResponder)
if err != nil {
return false, errors.Wrapf(err,
"failed to check if autoresponse posts in channelId=%s for userId=%s since %s", options.ChannelId, userId, model.GetTimeForMillis(options.Time))
}
return exist, nil
}
func (s *SqlPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Or{sq.Gt{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.And{sq.Eq{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.Gt{"Posts.Id": cursor.LastPostId}}}).
OrderBy("Posts.UpdateAt", "Id").
Limit(uint64(limit))
if options.ChannelId != "" {
query = query.Where(sq.Eq{"Posts.ChannelId": options.ChannelId})
}
if !options.IncludeDeleted {
query = query.Where(sq.Eq{"Posts.DeleteAt": 0})
}
if options.ExcludeRemoteId != "" {
query = query.Where(sq.NotEq{"COALESCE(Posts.RemoteId,'')": options.ExcludeRemoteId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, cursor, errors.Wrap(err, "getpostssinceforsync_tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, cursor, errors.Wrapf(err, "error getting Posts with channelId=%s", options.ChannelId)
}
if len(posts) != 0 {
cursor.LastPostUpdateAt = posts[len(posts)-1].UpdateAt
cursor.LastPostId = posts[len(posts)-1].Id
}
return posts, cursor, nil
}
func (s *SqlPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
return s.getPostsAround(true, options, sanitizeOptions)
}
func (s *SqlPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
return s.getPostsAround(false, options, sanitizeOptions)
}
func (s *SqlPostStore) GetPostsByThread(threadId string, since int64) ([]*model.Post, error) {
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{"RootId": threadId}).
Where(sq.Eq{"DeleteAt": 0}).
Where(sq.GtOrEq{"CreateAt": since})
result := []*model.Post{}
err := s.GetReplicaX().SelectBuilder(&result, query)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch thread posts")
}
return result, nil
}
func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.Page < 0 {
return nil, store.NewErrInvalidInput("Post", "<options.Page>", options.Page)
}
if options.PerPage < 0 {
return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage)
}
offset := options.Page * options.PerPage
posts := []*postWithExtra{}
parents := []*model.Post{}
var direction string
var sort string
if before {
direction = "<"
sort = "DESC"
} else {
direction = ">"
sort = "ASC"
}
table := "Posts p"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
columns := []string{"p.*"}
if options.CollapsedThreads {
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
}
query := s.getQueryBuilder().Select(columns...)
replyCountSubQuery := s.getQueryBuilder().Select("COUNT(*)").From("Posts").Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)"))
conditions := sq.And{
sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId),
sq.Eq{"p.ChannelId": options.ChannelId},
}
if !options.IncludeDeleted {
replyCountSubQuery = replyCountSubQuery.Where(sq.Expr("Posts.DeleteAt = 0"))
conditions = append(conditions, sq.Eq{"p.DeleteAt": int(0)})
}
if options.CollapsedThreads {
conditions = append(conditions, sq.Eq{"RootId": ""})
query = query.LeftJoin("Threads ON Threads.PostId = p.Id").LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = p.Id AND ThreadMemberships.UserId=?", options.UserId)
} else {
query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
}
query = query.From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-24170.
OrderBy("p.ChannelId", "p.DeleteAt", "p.CreateAt "+sort).
Limit(uint64(options.PerPage)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
if !options.CollapsedThreads && len(posts) > 0 {
rootIds := []string{}
for _, post := range posts {
rootIds = append(rootIds, post.Id)
if post.RootId != "" {
rootIds = append(rootIds, post.RootId)
}
}
rootQuery := s.getQueryBuilder().Select("p.*")
idQuery := sq.Or{
sq.Eq{"Id": rootIds},
}
rootQuery = rootQuery.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
if !options.SkipFetchThreads {
idQuery = append(idQuery, sq.Eq{"RootId": rootIds}) // preserve original behaviour
}
rootQuery = rootQuery.From("Posts p").
Where(sq.And{
idQuery,
sq.Eq{"p.ChannelId": options.ChannelId},
}).
OrderBy("CreateAt DESC")
if !options.IncludeDeleted {
rootQuery = rootQuery.Where(sq.Eq{"p.DeleteAt": 0})
}
rootQueryString, rootArgs, nErr := rootQuery.ToSql()
if nErr != nil {
return nil, errors.Wrap(nErr, "post_tosql")
}
nErr = s.GetReplicaX().Select(&parents, rootQueryString, rootArgs...)
if nErr != nil {
return nil, errors.Wrapf(nErr, "failed to find Posts with channelId=%s", options.ChannelId)
}
}
list, err := s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, !before, sanitizeOptions)
if err != nil {
return nil, err
}
for _, p := range parents {
list.AddPost(p)
}
return list, nil
}
func (s *SqlPostStore) GetPostIdBeforeTime(channelId string, time int64, collapsedThreads bool) (string, error) {
return s.getPostIdAroundTime(channelId, time, true, collapsedThreads)
}
func (s *SqlPostStore) GetPostIdAfterTime(channelId string, time int64, collapsedThreads bool) (string, error) {
return s.getPostIdAroundTime(channelId, time, false, collapsedThreads)
}
func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before bool, collapsedThreads bool) (string, error) {
var direction sq.Sqlizer
var sort string
if before {
direction = sq.Lt{"CreateAt": time}
sort = "DESC"
} else {
direction = sq.Gt{"CreateAt": time}
sort = "ASC"
}
table := "Posts"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
conditions := sq.And{
direction,
sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"Posts.DeleteAt": int(0)},
}
if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"Posts.RootId": ""}}
}
query := s.getQueryBuilder().
Select("Id").
From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369.
OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt "+sort).
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return "", errors.Wrap(err, "post_tosql")
}
var postId string
if err := s.GetMasterX().Get(&postId, queryString, args...); err != nil {
if err != sql.ErrNoRows {
return "", errors.Wrapf(err, "failed to get Post id with channelId=%s", channelId)
}
}
return postId, nil
}
func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64, collapsedThreads bool) (*model.Post, error) {
table := "Posts"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
conditions := sq.And{
sq.Gt{"Posts.CreateAt": time},
sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"Posts.DeleteAt": int(0)},
}
if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"RootId": ""}}
}
query := s.getQueryBuilder().
Select("*").
From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369.
OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt ASC").
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
var post model.Post
if err := s.GetMasterX().Get(&post, queryString, args...); err != nil {
if err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get Post with channelId=%s", channelId)
}
}
return &post, nil
}
func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
posts := []*model.Post{}
var fetchQuery string
if skipFetchThreads {
fetchQuery = "SELECT p.*, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)) as ReplyCount FROM Posts p WHERE p.ChannelId = ? ORDER BY p.CreateAt DESC LIMIT ? OFFSET ?"
if !includeDeleted {
fetchQuery = "SELECT p.*, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.ChannelId = ? AND p.DeleteAt = 0 ORDER BY p.CreateAt DESC LIMIT ? OFFSET ?"
}
} else {
fetchQuery = "SELECT * FROM Posts WHERE Posts.ChannelId = ? ORDER BY Posts.CreateAt DESC LIMIT ? OFFSET ?"
if !includeDeleted {
fetchQuery = "SELECT * FROM Posts WHERE Posts.ChannelId = ? AND Posts.DeleteAt = 0 ORDER BY Posts.CreateAt DESC LIMIT ? OFFSET ?"
}
}
err := s.GetReplicaX().Select(&posts, fetchQuery, channelId, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
if s.DriverName() == model.DatabaseDriverPostgres {
return s.getParentsPostsPostgreSQL(channelId, offset, limit, skipFetchThreads, includeDeleted)
}
deleteAtCondition := "AND DeleteAt = 0"
if includeDeleted {
deleteAtCondition = ""
}
// query parent Ids first
roots := []string{}
rootQuery := `
SELECT DISTINCT
q.RootId
FROM
(SELECT
Posts.RootId
FROM
Posts
WHERE
ChannelId = ? ` + deleteAtCondition + `
ORDER BY CreateAt DESC
LIMIT ? OFFSET ?) q
WHERE q.RootId != ''`
err := s.GetReplicaX().Select(&roots, rootQuery, channelId, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(roots) == 0 {
return nil, nil
}
cols := []string{"p.*"}
var where sq.Sqlizer
where = sq.Eq{"p.Id": roots}
if skipFetchThreads {
col := "(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)) as ReplyCount"
if !includeDeleted {
col = "(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount"
}
cols = append(cols, col)
} else {
where = sq.Or{
where,
sq.Eq{"p.RootId": roots},
}
}
query := s.getQueryBuilder().
Select(cols...).
From("Posts p").
Where(sq.And{
where,
sq.Eq{"p.ChannelId": channelId},
}).
OrderBy("p.CreateAt")
if !includeDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "ParentPosts_Tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) getParentsPostsPostgreSQL(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
posts := []*model.Post{}
replyCountQuery := ""
onStatement := "q1.RootId = q2.Id"
if skipFetchThreads {
replyCountQuery = ` ,(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END)) as ReplyCount`
if !includeDeleted {
replyCountQuery = ` ,(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
}
} else {
onStatement += " OR q1.RootId = q2.RootId"
}
deleteAtQueryCondition := "AND q2.DeleteAt = 0"
deleteAtSubQueryCondition := "AND Posts.DeleteAt = 0"
if includeDeleted {
deleteAtQueryCondition, deleteAtSubQueryCondition = "", ""
}
err := s.GetReplicaX().Select(&posts,
`SELECT q2.*`+replyCountQuery+`
FROM
Posts q2
INNER JOIN
(SELECT DISTINCT
q3.RootId
FROM
(SELECT
Posts.RootId
FROM
Posts
WHERE
Posts.ChannelId = ? `+deleteAtSubQueryCondition+`
ORDER BY Posts.CreateAt DESC
LIMIT ? OFFSET ?) q3
WHERE q3.RootId != '') q1
ON `+onStatement+`
WHERE
q2.ChannelId = ? `+deleteAtQueryCondition+`
ORDER BY q2.CreateAt`, channelId, limit, offset, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId)
}
return posts, nil
}
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
func (s *SqlPostStore) GetNthRecentPostTime(n int64) (int64, error) {
if n <= 0 {
return 0, errors.New("n can't be less than 1")
}
builder := s.getQueryBuilder().
Select("CreateAt").
From("Posts p").
// Consider users posts only for cloud limit
Where(sq.And{
sq.Eq{"p.Type": ""},
sq.Expr("p.UserId NOT IN (SELECT UserId FROM Bots)"),
}).
OrderBy("p.CreateAt DESC").
Limit(1).
Offset(uint64(n - 1))
query, queryArgs, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "GetNthRecentPostTime_tosql")
}
var createAt int64
if err := s.GetMasterX().Get(&createAt, query, queryArgs...); err != nil {
if err == sql.ErrNoRows {
return 0, store.NewErrNotFound("Post", "none")
}
return 0, errors.Wrapf(err, "failed to get the Nth Post=%d", n)
}
return createAt, nil
}
func (s *SqlPostStore) buildCreateDateFilterClause(params *model.SearchParams, builder sq.SelectBuilder) sq.SelectBuilder {
// handle after: before: on: filters
if params.OnDate != "" {
onDateStart, onDateEnd := params.GetOnDateMillis()
// between `on date` start of day and end of day
builder = builder.Where("CreateAt BETWEEN ? AND ?", onDateStart, onDateEnd)
return builder
}
if params.ExcludedDate != "" {
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
builder = builder.Where("CreateAt NOT BETWEEN ? AND ?", excludedDateStart, excludedDateEnd)
}
if params.AfterDate != "" {
afterDate := params.GetAfterDateMillis()
// greater than `after date`
builder = builder.Where("CreateAt >= ?", afterDate)
}
if params.BeforeDate != "" {
beforeDate := params.GetBeforeDateMillis()
// less than `before date`
builder = builder.Where("CreateAt <= ?", beforeDate)
}
if params.ExcludedAfterDate != "" {
afterDate := params.GetExcludedAfterDateMillis()
builder = builder.Where("CreateAt < ?", afterDate)
}
if params.ExcludedBeforeDate != "" {
beforeDate := params.GetExcludedBeforeDateMillis()
builder = builder.Where("CreateAt > ?", beforeDate)
}
return builder
}
func (s *SqlPostStore) buildSearchTeamFilterClause(teamId string, builder sq.SelectBuilder) sq.SelectBuilder {
if teamId == "" {
return builder
}
return builder.Where(sq.Or{
sq.Eq{"TeamId": teamId},
sq.Eq{"TeamId": ""},
})
}
func (s *SqlPostStore) buildSearchChannelFilterClause(channels []string, exclusion bool, byName bool, builder sq.SelectBuilder) sq.SelectBuilder {
if len(channels) == 0 {
return builder
}
if byName {
if exclusion {
return builder.Where(sq.NotEq{"Name": channels})
}
return builder.Where(sq.Eq{"Name": channels})
}
if exclusion {
return builder.Where(sq.NotEq{"Id": channels})
}
return builder.Where(sq.Eq{"Id": channels})
}
func (s *SqlPostStore) buildSearchUserFilterClause(users []string, exclusion bool, byUsername bool, builder sq.SelectBuilder) sq.SelectBuilder {
if len(users) == 0 {
return builder
}
if byUsername {
if exclusion {
return builder.Where(sq.NotEq{"Username": users})
}
return builder.Where(sq.Eq{"Username": users})
}
if exclusion {
return builder.Where(sq.NotEq{"Id": users})
}
return builder.Where(sq.Eq{"Id": users})
}
func (s *SqlPostStore) buildSearchPostFilterClause(teamID string, fromUsers []string, excludedUsers []string, userByUsername bool, builder sq.SelectBuilder) (sq.SelectBuilder, error) {
if len(fromUsers) == 0 && len(excludedUsers) == 0 {
return builder, nil
}
// Sub-query builder.
sb := s.getSubQueryBuilder().
Select("Id").
From("Users, TeamMembers").
Where(sq.Expr("Users.Id = TeamMembers.UserId"))
if teamID != "" {
sb = sb.Where(sq.Eq{"TeamMembers.TeamId": teamID})
}
sb = s.buildSearchUserFilterClause(fromUsers, false, userByUsername, sb)
sb = s.buildSearchUserFilterClause(excludedUsers, true, userByUsername, sb)
subQuery, subQueryArgs, err := sb.ToSql()
if err != nil {
return sq.SelectBuilder{}, err
}
/*
* Squirrel does not support a sub-query in the WHERE condition.
* https://github.com/Masterminds/squirrel/issues/299
*/
return builder.Where("UserId IN ("+subQuery+")", subQueryArgs...), nil
}
func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, error) {
return s.search(teamId, userId, params, true, true)
}
func (s *SqlPostStore) search(teamId string, userId string, params *model.SearchParams, channelsByName bool, userByUsername bool) (*model.PostList, error) {
list := model.NewPostList()
if params.Terms == "" && params.ExcludedTerms == "" &&
len(params.InChannels) == 0 && len(params.ExcludedChannels) == 0 &&
len(params.FromUsers) == 0 && len(params.ExcludedUsers) == 0 &&
params.OnDate == "" && params.AfterDate == "" && params.BeforeDate == "" {
return list, nil
}
baseQuery := s.getQueryBuilder().Select(
"*",
"(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount",
).From("Posts q2").
Where("q2.DeleteAt = 0").
Where(fmt.Sprintf("q2.Type NOT LIKE '%s%%'", model.PostSystemMessagePrefix)).
OrderByClause("q2.CreateAt DESC").
Limit(100)
var err error
baseQuery, err = s.buildSearchPostFilterClause(teamId, params.FromUsers, params.ExcludedUsers, userByUsername, baseQuery)
if err != nil {
return nil, errors.Wrap(err, "failed to build search post filter clause")
}
baseQuery = s.buildCreateDateFilterClause(params, baseQuery)
termMap := map[string]bool{}
terms := params.Terms
excludedTerms := params.ExcludedTerms
searchType := "Message"
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[strings.ToUpper(term)] = true
}
}
for _, c := range s.specialSearchChars() {
terms = strings.Replace(terms, c, " ", -1)
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
}
if terms == "" && excludedTerms == "" {
// we've already confirmed that we have a channel or user to search for
} else if s.DriverName() == model.DatabaseDriverPostgres {
// Parse text for wildcards
var wildcard *regexp.Regexp
if wildcard, err = regexp.Compile(`\*($| )`); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
}
var termsClause string
if params.OrTerms {
termsClause = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else if strings.HasPrefix(terms, `"`) && strings.HasSuffix(terms, `"`) {
termsClause = "(" + strings.Join(strings.Fields(terms), " <-> ") + ")" + excludeClause
} else {
termsClause = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
}
searchClause := fmt.Sprintf("to_tsvector('%[1]s', %[2]s) @@ to_tsquery('%[1]s', ?)", s.pgDefaultTextSearchConfig, searchType)
baseQuery = baseQuery.Where(searchClause, termsClause)
} else if s.DriverName() == model.DatabaseDriverMysql {
if searchType == "Message" {
terms, err = removeMysqlStopWordsFromTerms(terms)
if err != nil {
return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms")
}
if terms == "" {
return list, nil
}
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " -(" + excludedTerms + ")"
}
var termsClause string
if params.OrTerms {
termsClause = terms + excludeClause
} else {
splitTerms := []string{}
for _, t := range strings.Fields(terms) {
splitTerms = append(splitTerms, "+"+t)
}
termsClause = strings.Join(splitTerms, " ") + excludeClause
}
searchClause := fmt.Sprintf("MATCH (%s) AGAINST (? IN BOOLEAN MODE)", searchType)
baseQuery = baseQuery.Where(searchClause, termsClause)
}
inQuery := s.getSubQueryBuilder().Select("Id").
From("Channels, ChannelMembers").
Where("Id = ChannelId")
if !params.IncludeDeletedChannels {
inQuery = inQuery.Where("Channels.DeleteAt = 0")
}
if !params.SearchWithoutUserId {
inQuery = inQuery.Where("ChannelMembers.UserId = ?", userId)
}
inQuery = s.buildSearchTeamFilterClause(teamId, inQuery)
inQuery = s.buildSearchChannelFilterClause(params.InChannels, false, channelsByName, inQuery)
inQuery = s.buildSearchChannelFilterClause(params.ExcludedChannels, true, channelsByName, inQuery)
inQueryClause, inQueryClauseArgs, err := inQuery.ToSql()
if err != nil {
return nil, err
}
baseQuery = baseQuery.Where(fmt.Sprintf("ChannelId IN (%s)", inQueryClause), inQueryClauseArgs...)
searchQuery, searchQueryArgs, err := baseQuery.ToSql()
if err != nil {
return nil, err
}
var posts []*model.Post
if err := s.GetSearchReplicaX().Select(&posts, searchQuery, searchQueryArgs...); err != nil {
mlog.Warn("Query error searching posts.", mlog.Err(err))
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
} else {
for _, p := range posts {
if searchType == "Hashtags" {
exactMatch := false
for _, tag := range strings.Split(p.Hashtags, " ") {
if termMap[strings.ToUpper(tag)] {
exactMatch = true
break
}
}
if !exactMatch {
continue
}
}
list.AddPost(p)
list.AddOrder(p.Id)
}
}
list.MakeNonNil()
return list, nil
}
func removeMysqlStopWordsFromTerms(terms string) (string, error) {
stopWords := make([]string, len(searchlayer.MySQLStopWords))
copy(stopWords, searchlayer.MySQLStopWords)
re, err := regexp.Compile(fmt.Sprintf(`^(%s)$`, strings.Join(stopWords, "|")))
if err != nil {
return "", err
}
newTerms := make([]string, 0)
separatedTerms := strings.Fields(terms)
for _, term := range separatedTerms {
term = strings.TrimSpace(term)
if term = re.ReplaceAllString(term, ""); term != "" {
newTerms = append(newTerms, term)
}
}
return strings.Join(newTerms, " "), nil
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) (model.AnalyticsRows, error) {
var args []any
query :=
`SELECT DISTINCT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts`
if teamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{teamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt >= ? AND Posts.CreateAt <= ?
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if s.DriverName() == model.DatabaseDriverPostgres {
query =
`SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts`
if teamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{teamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt >= ? AND Posts.CreateAt <= ?
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31)))
args = append(args, start, end)
rows := model.AnalyticsRows{}
err := s.GetReplicaX().Select(
&rows,
query,
args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", teamId)
}
return rows, nil
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
var args []any
query :=
`SELECT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
COUNT(Posts.Id) AS Value
FROM Posts`
if options.BotsOnly {
query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid"
}
if options.TeamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{options.TeamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt <= ?
AND Posts.CreateAt >= ?
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if s.DriverName() == model.DatabaseDriverPostgres {
query =
`SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, Count(Posts.Id) AS Value
FROM Posts`
if options.BotsOnly {
query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid"
}
if options.TeamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{options.TeamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt <= ?
AND Posts.CreateAt >= ?
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31)))
if options.YesterdayOnly {
start = utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -1)))
}
args = append(args, end, start)
rows := model.AnalyticsRows{}
err := s.GetReplicaX().Select(
&rows,
query,
args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", options.TeamId)
}
return rows, nil
}
func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*) AS Value").
From("Posts p")
if options.TeamId != "" {
query = query.
Join("Channels c ON (c.Id = p.ChannelId)").
Where(sq.Eq{"c.TeamId": options.TeamId})
}
if options.UsersPostsOnly {
query = query.Where(sq.And{
sq.Eq{"p.Type": ""},
sq.Expr("p.UserId NOT IN (SELECT UserId FROM Bots)"),
})
}
if options.MustHaveFile {
query = query.Where(sq.Or{sq.NotEq{"p.FileIds": "[]"}, sq.NotEq{"p.Filenames": "[]"}})
}
if options.MustHaveHashtag {
query = query.Where(sq.NotEq{"p.Hashtags": ""})
}
if options.ExcludeDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
if options.ExcludeSystemPosts {
query = query.Where("p.Type NOT LIKE 'system_%'")
}
if options.SinceUpdateAt > 0 {
query = query.Where(sq.Or{
sq.Gt{"p.UpdateAt": options.SinceUpdateAt},
sq.And{
sq.Eq{"p.UpdateAt": options.SinceUpdateAt},
sq.Gt{"p.Id": options.SincePostID},
},
})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "post_tosql")
}
var v int64
err = s.GetReplicaX().Get(&v, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Posts")
}
return v, nil
}
func (s *SqlPostStore) GetPostsCreatedAt(channelId string, time int64) ([]*model.Post, error) {
query := `SELECT * FROM Posts WHERE CreateAt = ? AND ChannelId = ?`
posts := []*model.Post{}
err := s.GetReplicaX().Select(&posts, query, time, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId)
}
return posts, nil
}
func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
baseQuery := s.getQueryBuilder().Select("p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount").
From("Posts p").
Where(sq.Eq{"p.Id": postIds}).
OrderBy("CreateAt DESC")
query, args, err := baseQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostsByIds_tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(posts) == 0 {
return nil, store.NewErrNotFound("Post", fmt.Sprintf("postIds=%v", postIds))
}
return posts, nil
}
func (s *SqlPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
builder := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{"Posts.OriginalId": postId}).
OrderBy("Posts.EditAt DESC")
queryString, args, err := builder.ToSql()
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", postId)
}
return nil, errors.Wrap(err, "failed to find post history")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "error getting posts edit history with postId=%s", postId)
}
if len(posts) == 0 {
return nil, store.NewErrNotFound("failed to find post history", postId)
}
return posts, nil
}
func (s *SqlPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
posts := []*model.PostForIndexing{}
table := "Posts"
// We force this index to avoid any chances of index merge intersection.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_create_at_id)"
}
query := `SELECT
Posts.*, Channels.TeamId
FROM ` + table + `
LEFT JOIN
Channels
ON
Posts.ChannelId = Channels.Id
WHERE
Posts.CreateAt > ?
OR
(Posts.CreateAt = ? AND Posts.Id > ?)
ORDER BY
Posts.CreateAt ASC, Posts.Id ASC
LIMIT
?`
err := s.GetSearchReplicaX().Select(&posts, query, startTime, startTime, startPostID, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("Posts.Id").
From("Posts")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "Posts",
TimeColumn: "CreateAt",
PrimaryKeys: []string{"Id"},
ChannelIDTable: "Posts",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes entries from Posts when a corresponding channel no longer exists.
func (s *SqlPostStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var query string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Posts table. More details on the PR with
// this commit.
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
WHERE Posts.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX (PRIMARY))
LIMIT ?
) AS A
)`
} else {
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
LEFT JOIN Channels ON Posts.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if s.DriverName() == "postgres" {
query = "DELETE from Posts WHERE Id = any (array (SELECT Id FROM Posts WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from Posts WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := s.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Posts")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to delete Posts")
}
return rowsAffected, nil
}
func (s *SqlPostStore) GetOldest() (*model.Post, error) {
var post model.Post
err := s.GetReplicaX().Get(&post, "SELECT * FROM Posts ORDER BY CreateAt LIMIT 1")
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", "none")
}
return nil, errors.Wrap(err, "failed to get oldest Post")
}
return &post, nil
}
func (s *SqlPostStore) determineMaxPostSize() int {
var maxPostSizeBytes int32
if s.DriverName() == model.DatabaseDriverPostgres {
// The Post.Message column in Postgres has historically been VARCHAR(4000), but
// may be manually enlarged to support longer posts.
if err := s.GetReplicaX().Get(&maxPostSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
table_name = 'posts'
AND column_name = 'message'
`); err != nil {
mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err))
}
} else if s.DriverName() == model.DatabaseDriverMysql {
// The Post.Message column in MySQL has historically been TEXT, with a maximum
// limit of 65535.
if err := s.GetReplicaX().Get(&maxPostSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND table_name = 'Posts'
AND column_name = 'Message'
LIMIT 0, 1
`); err != nil {
mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err))
}
} else {
mlog.Warn("No implementation found to determine the maximum supported post size")
}
// Assume a worst-case representation of four bytes per rune.
maxPostSize := int(maxPostSizeBytes) / 4
// To maintain backwards compatibility, don't yield a maximum post
// size smaller than the previous limit, even though it wasn't
// actually possible to store 4000 runes in all cases.
if maxPostSize < model.PostMessageMaxRunesV1 {
maxPostSize = model.PostMessageMaxRunesV1
}
mlog.Info("Post.Message has size restrictions", mlog.Int("max_characters", maxPostSize), mlog.Int32("max_bytes", maxPostSizeBytes))
return maxPostSize
}
// GetMaxPostSize returns the maximum number of runes that may be stored in a post.
func (s *SqlPostStore) GetMaxPostSize() int {
s.maxPostSizeOnce.Do(func() {
s.maxPostSizeCached = s.determineMaxPostSize()
})
return s.maxPostSizeCached
}
func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*model.PostForExport, error) {
for {
rootIds := []string{}
err := s.GetReplicaX().Select(&rootIds,
`SELECT
Id
FROM
Posts
WHERE
Posts.Id > ?
AND Posts.RootId = ''
AND Posts.DeleteAt = 0
ORDER BY Posts.Id
LIMIT ?`,
afterId, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
postsForExport := []*model.PostForExport{}
if len(rootIds) == 0 {
return postsForExport, nil
}
builder := s.getQueryBuilder().
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName").
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
InnerJoin("Channels ON p1.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
InnerJoin("Users ON p1.UserId = Users.Id").
Where(sq.And{
sq.Eq{"Channels.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
OrderBy("p1.Id")
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "postsForExport_toSql")
}
err = s.GetSearchReplicaX().Select(&postsForExport, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(postsForExport) == 0 {
// All of the posts were in channels or teams that were deleted.
// Update the afterId and try again.
afterId = rootIds[len(rootIds)-1]
continue
}
return postsForExport, nil
}
}
func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) {
posts := []*model.ReplyForExport{}
err := s.GetSearchReplicaX().Select(&posts, `
SELECT
Posts.*,
Users.Username as Username
FROM
Posts
INNER JOIN
Users ON Posts.UserId = Users.Id
WHERE
Posts.RootId = ?
AND Posts.DeleteAt = 0
ORDER BY
Posts.Id`, rootId)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string) ([]*model.DirectPostForExport, error) {
query := s.getQueryBuilder().
Select("p.*", "Users.Username as User").
From("Posts p").
Join("Channels ON p.ChannelId = Channels.Id").
Join("Users ON p.UserId = Users.Id").
Where(sq.And{
sq.Gt{"p.Id": afterId},
sq.Eq{"p.RootId": ""},
sq.Eq{"p.DeleteAt": 0},
sq.Eq{"Channels.DeleteAt": 0},
sq.Eq{"Users.DeleteAt": 0},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}).
OrderBy("p.Id").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
posts := []*model.DirectPostForExport{}
if err2 := s.GetReplicaX().Select(&posts, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find Posts")
}
var channelIds []string
for _, post := range posts {
channelIds = append(channelIds, post.ChannelId)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
From("ChannelMembers cm").
Join("Users u ON ( u.Id = cm.UserId )").
Where(sq.Eq{
"cm.ChannelId": channelIds,
})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
channelMembers := []*model.ChannelMemberForExport{}
if err := s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
// Build a map of channels and their posts
postsChannelMap := make(map[string][]*model.DirectPostForExport)
for _, post := range posts {
post.ChannelMembers = &[]string{}
postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post)
}
// Build a map of channels and their members
channelMembersMap := make(map[string][]string)
for _, member := range channelMembers {
channelMembersMap[member.ChannelId] = append(channelMembersMap[member.ChannelId], member.Username)
}
// Populate each post ChannelMembers extracting it from the channelMembersMap
for channelId := range channelMembersMap {
for _, post := range postsChannelMap[channelId] {
*post.ChannelMembers = channelMembersMap[channelId]
}
}
return posts, nil
}
//nolint:unparam
func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamId string, page, perPage int) (*model.PostSearchResults, error) {
// Since we don't support paging for DB search, we just return nothing for later pages
if page > 0 {
return model.MakePostSearchResults(model.NewPostList(), nil), nil
}
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
now := model.GetMillis()
pchan := make(chan store.StoreResult, len(paramsList))
var wg sync.WaitGroup
for _, params := range paramsList {
// Deliberately keeping non-alphanumeric characters to
// prevent surprises in UI.
buf, err := json.Marshal(params)
if err != nil {
return nil, err
}
err = s.LogRecentSearch(userID, buf, now)
if err != nil {
return nil, err
}
// remove any unquoted term that contains only non-alphanumeric chars
// ex: abcd "**" && abc >> abcd "**" abc
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
wg.Add(1)
go func(params *model.SearchParams) {
defer wg.Done()
postList, err := s.search(teamId, userID, params, false, false)
pchan <- store.StoreResult{Data: postList, NErr: err}
}(params)
}
wg.Wait()
close(pchan)
posts := model.NewPostList()
for result := range pchan {
if result.NErr != nil {
return nil, result.NErr
}
data := result.Data.(*model.PostList)
posts.Extend(data)
}
posts.SortByCreateAt()
return model.MakePostSearchResults(posts, nil), nil
}
const lastSearchesLimit = 5
func (s *SqlPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
var lastSearchPointer int
var queryStr string
// get search_pointer
// We coalesce to -1 because we want to start from 0
if s.DriverName() == model.DatabaseDriverPostgres {
queryStr = `SELECT COALESCE((props->>'last_search_pointer')::integer, -1)
FROM Users
WHERE Id=?`
} else {
queryStr = `SELECT COALESCE(CAST(JSON_EXTRACT(Props, '$.last_search_pointer') as unsigned), -1)
FROM Users
WHERE Id=?`
}
err = transaction.Get(&lastSearchPointer, queryStr, userID)
if err != nil {
return errors.Wrapf(err, "failed to find last_search_pointer for user=%s", userID)
}
// (ptr+1)%lastSearchesLimit
lastSearchPointer = (lastSearchPointer + 1) % lastSearchesLimit
if s.IsBinaryParamEnabled() {
searchQuery = AppendBinaryFlag(searchQuery)
}
// insert at pointer
query := s.getQueryBuilder().
Insert("RecentSearches").
Columns("UserId", "SearchPointer", "Query", "CreateAt").
Values(userID, lastSearchPointer, searchQuery, createAt)
if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, searchpointer) DO UPDATE SET Query = ?, CreateAt = ?", searchQuery, createAt))
} else {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Query = ?, CreateAt = ?", searchQuery, createAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "log_recent_search_tosql")
}
if _, err2 := transaction.Exec(queryString, args...); err2 != nil {
return errors.Wrapf(err2, "failed to upsert recent_search for user=%s", userID)
}
// write ptr on users prop
if s.DriverName() == model.DatabaseDriverPostgres {
_, err = transaction.Exec(`UPDATE Users
SET Props = jsonb_set(Props, $1, $2)
WHERE Id = $3`, jsonKeyPath("last_search_pointer"), jsonStringVal(strconv.Itoa(lastSearchPointer)), userID)
} else {
_, err = transaction.Exec(`UPDATE Users
SET Props = JSON_SET(Props, ?, ?)
WHERE Id = ?`, "$.last_search_pointer", strconv.Itoa(lastSearchPointer), userID)
}
if err != nil {
return errors.Wrapf(err, "failed to update last_search_pointer for user=%s", userID)
}
if err2 := transaction.Commit(); err2 != nil {
return errors.Wrap(err2, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
params := [][]byte{}
err := s.GetReplicaX().Select(¶ms, `SELECT query
FROM RecentSearches
WHERE UserId=?
ORDER BY CreateAt DESC`, userID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get recent searches for user=%s", userID)
}
res := make([]*model.SearchParams, len(params))
for i, param := range params {
err = json.Unmarshal(param, &res[i])
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal recent search query for user=%s", userID)
}
}
return res, nil
}
func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) {
query := s.getQueryBuilder().Select("MIN(min_createat) min_createat").
Suffix(`FROM (
(SELECT MIN(createat) min_createat FROM Posts)
UNION
(SELECT MIN(createat) min_createat FROM Users)
UNION
(SELECT MIN(createat) min_createat FROM Channels)
) entities`)
queryString, args, err := query.ToSql()
if err != nil {
return -1, errors.Wrap(err, "post_tosql")
}
var oldest int64
err = s.GetReplicaX().Get(&oldest, queryString, args...)
if err != nil {
return -1, errors.Wrap(err, "unable to scan oldest entity creation time")
}
return oldest, nil
}
// Deletes a thread and a thread membership if the postId is a root post
func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postId string) error {
if _, err := transaction.Exec("DELETE FROM Threads WHERE PostId = ?", postId); err != nil {
return errors.Wrap(err, "failed to delete Threads")
}
if _, err := transaction.Exec("DELETE FROM ThreadMemberships WHERE PostId = ?", postId); err != nil {
return errors.Wrap(err, "failed to delete ThreadMemberships")
}
return nil
}
// deleteThread marks a thread as deleted at the given time.
func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error {
queryString, args, err := s.getQueryBuilder().
Update("Threads").
Set("ThreadDeleteAt", deleteAtTime).
Where(sq.Eq{"PostId": postId}).
ToSql()
if err != nil {
return errors.Wrapf(err, "failed to create SQL query to mark thread for root post %s as deleted", postId)
}
_, err = transaction.Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to mark thread for root post %s as deleted", postId)
}
return nil
}
// updateThreadAfterReplyDeletion decrements the thread reply count and adjusts the participants
// list as necessary.
func (s *SqlPostStore) updateThreadAfterReplyDeletion(transaction *sqlxTxWrapper, rootId string, userId string) error {
if rootId != "" {
queryString, args, err := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": rootId},
sq.Eq{"Posts.UserId": userId},
sq.Eq{"Posts.DeleteAt": 0},
}).
ToSql()
if err != nil {
return errors.Wrap(err, "failed to create SQL query to count user's posts")
}
var count int64
err = transaction.Get(&count, queryString, args...)
if err != nil {
return errors.Wrap(err, "failed to count user's posts in thread")
}
// Updating replyCount, and reducing participants if this was the last post in the thread for the user
updateQuery := s.getQueryBuilder().Update("Threads")
if count == 0 {
if s.DriverName() == model.DatabaseDriverPostgres {
updateQuery = updateQuery.Set("Participants", sq.Expr("Participants - ?", userId))
} else {
updateQuery = updateQuery.
Set("Participants", sq.Expr(
`IFNULL(JSON_REMOVE(Participants, JSON_UNQUOTE(JSON_SEARCH(Participants, 'one', ?))), Participants)`, userId,
))
}
}
lastReplyAtSubquery := sq.Select("COALESCE(MAX(CreateAt), 0)").
From("Posts").
Where(sq.Eq{
"RootId": rootId,
"DeleteAt": 0,
})
lastReplyCountSubquery := sq.Select("Count(*)").
From("Posts").
Where(sq.Eq{
"RootId": rootId,
"DeleteAt": 0,
})
updateQueryString, updateArgs, err := updateQuery.
Set("LastReplyAt", lastReplyAtSubquery).
Set("ReplyCount", lastReplyCountSubquery).
Where(sq.And{
sq.Eq{"PostId": rootId},
sq.Gt{"ReplyCount": 0},
}).
ToSql()
if err != nil {
return errors.Wrap(err, "failed to create SQL query to update thread")
}
_, err = transaction.Exec(updateQueryString, updateArgs...)
if err != nil {
return errors.Wrap(err, "failed to update Threads")
}
}
return nil
}
func (s *SqlPostStore) savePostsPriority(transaction *sqlxTxWrapper, posts []*model.Post) error {
for _, post := range posts {
if post.GetPriority() != nil {
postPriority := &model.PostPriority{
PostId: post.Id,
ChannelId: post.ChannelId,
Priority: post.Metadata.Priority.Priority,
RequestedAck: post.Metadata.Priority.RequestedAck,
PersistentNotifications: post.Metadata.Priority.PersistentNotifications,
}
if _, err := transaction.NamedExec(`INSERT INTO PostsPriority (PostId, ChannelId, Priority, RequestedAck, PersistentNotifications) VALUES (:PostId, :ChannelId, :Priority, :RequestedAck, :PersistentNotifications)`, postPriority); err != nil {
return err
}
}
}
return nil
}
func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts []*model.Post) error {
postsByRoot := map[string][]*model.Post{}
var rootIds []string
for _, post := range posts {
// skip if post is not a part of a thread
if post.RootId == "" {
continue
}
rootIds = append(rootIds, post.RootId)
postsByRoot[post.RootId] = append(postsByRoot[post.RootId], post)
}
if len(rootIds) == 0 {
return nil
}
threadsByRootsSql, threadsByRootsArgs, err := s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS DeleteAt",
).
From("Threads").
Where(sq.Eq{"Threads.PostId": rootIds}).
ToSql()
if err != nil {
return errors.Wrap(err, "updateThreadsFromPosts_ToSql")
}
threadsByRoots := []*model.Thread{}
err = transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...)
if err != nil {
return err
}
threadByRoot := map[string]*model.Thread{}
for _, thread := range threadsByRoots {
threadByRoot[thread.PostId] = thread
}
teamIdByChannelId := map[string]string{}
for rootId, posts := range postsByRoot {
if thread, found := threadByRoot[rootId]; !found {
data := []struct {
UserId string
RepliedAt int64
}{}
// calculate participants
if err := transaction.Select(&data, "SELECT Posts.UserId, MAX(Posts.CreateAt) as RepliedAt FROM Posts WHERE Posts.RootId=? AND Posts.DeleteAt=0 GROUP BY Posts.UserId ORDER BY RepliedAt ASC", rootId); err != nil {
return err
}
var participants model.StringArray
for _, item := range data {
participants = append(participants, item.UserId)
}
// calculate reply count
var count int64
err := transaction.Get(&count, "SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId=? And Posts.DeleteAt=0", rootId)
if err != nil {
return err
}
// calculate last reply at
var lastReplyAt int64
err = transaction.Get(&lastReplyAt, "SELECT COALESCE(MAX(Posts.CreateAt), 0) FROM Posts WHERE Posts.RootID=? and Posts.DeleteAt=0", rootId)
if err != nil {
return err
}
channelId := posts[0].ChannelId
teamId, ok := teamIdByChannelId[channelId]
if !ok {
// get teamId for channel
err = transaction.Get(&teamId, "SELECT COALESCE(Channels.TeamId, '') FROM Channels WHERE Channels.Id=?", channelId)
if err != nil {
return err
}
// store teamId for channel for efficiency
teamIdByChannelId[channelId] = teamId
}
// no metadata entry, create one
if _, err := transaction.NamedExec(`INSERT INTO Threads
(PostId, ChannelId, ReplyCount, LastReplyAt, Participants, ThreadTeamId)
VALUES
(:PostId, :ChannelId, :ReplyCount, :LastReplyAt, :Participants, :TeamId)`, &model.Thread{
PostId: rootId,
ChannelId: channelId,
ReplyCount: count,
LastReplyAt: lastReplyAt,
Participants: participants,
TeamId: teamId,
}); err != nil {
return err
}
} else {
// metadata exists, update it
for _, post := range posts {
thread.ReplyCount += 1
if thread.Participants.Contains(post.UserId) {
thread.Participants = thread.Participants.Remove(post.UserId)
}
thread.Participants = append(thread.Participants, post.UserId)
if post.CreateAt > thread.LastReplyAt {
thread.LastReplyAt = post.CreateAt
}
}
if _, err := transaction.NamedExec(`UPDATE Threads
SET ChannelId = :ChannelId,
ReplyCount = :ReplyCount,
LastReplyAt = :LastReplyAt,
Participants = :Participants
WHERE PostId=:PostId`, thread); err != nil {
return err
}
}
}
return nil
}
func (s *SqlPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
var botsFilterExpr string
/*
Channel.Name is of the format userId1__userId2.
Using this, self dms, and bot dms can be filtered.
*/
if s.DriverName() == model.DatabaseDriverPostgres {
botsFilterExpr = `SPLIT_PART(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SPLIT_PART(Channels.Name, '__', 2) NOT IN (SELECT UserId FROM Bots)
`
} else if s.DriverName() == model.DatabaseDriverMysql {
botsFilterExpr = `SUBSTRING_INDEX(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SUBSTRING_INDEX(Channels.Name, '__', -1) NOT IN (SELECT UserId FROM Bots)
`
}
channelSelector := s.getQueryBuilder().Select("Id", "TotalMsgCount").From("Channels").Join("ChannelMembers as cm on cm.ChannelId = Channels.Id").
Where(sq.And{
sq.Expr("Channels.Type = 'D'"),
sq.Eq{"cm.UserId": userID},
sq.NotEq{"Channels.Name": fmt.Sprintf("%s__%s", userID, userID)},
sq.Expr(botsFilterExpr),
})
var aggregator string
if s.DriverName() == model.DatabaseDriverMysql {
aggregator = "group_concat(distinct cm.UserId) as Participants"
} else {
aggregator = "string_agg(distinct cm.UserId, ',') as Participants"
}
topDMsBuilder := s.getQueryBuilder().Select("count(p.Id) as MessageCount", aggregator, "vch.Id as ChannelId").FromSelect(channelSelector, "vch").
Join("ChannelMembers as cm on cm.ChannelId = vch.Id").
Join("Posts as p on p.ChannelId = vch.Id").
Where(sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
}).GroupBy("vch.id")
// following where clause filters out all archived DMs with "deleted" users, that has only 1 user-id in Participants column.
archivedDMsFilter := s.getQueryBuilder().Select("MessageCount", "Participants", "ChannelId").FromSelect(topDMsBuilder, "top_dms").
Where(sq.Expr("POSITION(',' IN Participants) > 0"))
archivedDMsFilter = archivedDMsFilter.OrderBy("MessageCount DESC").Limit(uint64(limit + 1)).Offset(uint64(offset))
topDMs := make([]*model.TopDM, 0)
sql, args, err := archivedDMsFilter.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_ToSql")
}
err = s.GetReplicaX().Select(&topDMs, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// fill SecondParticipant column
topDMs, err = postProcessTopDMs(s, userID, topDMs, since)
if err != nil {
return nil, err
}
return model.GetTopDMListWithPagination(topDMs, limit), nil
}
func postProcessTopDMs(s *SqlPostStore, userID string, topDMs []*model.TopDM, since int64) ([]*model.TopDM, error) {
var topDMsFiltered = []*model.TopDM{}
var secondParticipantIds []string
var channelIds []string
// identify second participant in a list of participants
for _, topDM := range topDMs {
participants := strings.Split(topDM.Participants, ",")
var secondParticipantId string
// divide message count by 2, because it's counted twice due to channel memberships being 2 for dms.
topDM.MessageCount = topDM.MessageCount / 2
if participants[0] == userID {
secondParticipantId = participants[1]
} else {
secondParticipantId = participants[0]
}
secondParticipantIds = append(secondParticipantIds, secondParticipantId)
channelIds = append(channelIds, topDM.ChannelId)
}
// get user profiles
users, err := s.User().GetProfileByIds(context.Background(), secondParticipantIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get second participants' information")
}
// get outgoing message count for userId
outgoingMessagesQuery := s.getQueryBuilder().Select("ch.Id as ChannelId, count(p.Id) as MessageCount").From("Channels as ch").
Join("Posts as p on p.ChannelId=ch.Id").Where(
sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
sq.Eq{
"ch.Id": channelIds,
},
sq.Eq{
"p.UserId": userID,
},
}).GroupBy("ch.Id")
outgoingMessages := make([]*model.OutgoingMessageQueryResult, 0)
sql, args, err := outgoingMessagesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_outgoingMessagesQuery_ToSql")
}
err = s.GetReplicaX().Select(&outgoingMessages, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// create map of channelId -> MessageCount
outgoingMessagesMap := make(map[string]int)
for _, outgoingMessage := range outgoingMessages {
outgoingMessagesMap[outgoingMessage.ChannelId] = outgoingMessage.MessageCount
}
// create map of userId -> User
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
for index, topDM := range topDMs {
if secondParticipantIds[index] == "-1" {
return nil, errors.Wrapf(err, "failed to find second user for topDM: %s", userID)
}
user := usersMap[secondParticipantIds[index]]
topDM.SecondParticipant = &model.TopDMInsightUserInformation{
InsightUserInformation: model.InsightUserInformation{
Id: user.Id,
LastPictureUpdate: user.LastPictureUpdate,
FirstName: user.FirstName,
LastName: user.LastName,
Username: user.Username,
NickName: user.Nickname,
},
Position: user.Position,
}
topDM.OutgoingMessageCount = int64(outgoingMessagesMap[topDM.ChannelId])
topDMsFiltered = append(topDMsFiltered, topDM)
}
return topDMsFiltered, nil
}
func (s *SqlPostStore) SetPostReminder(reminder *model.PostReminder) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
sql := `SELECT EXISTS (SELECT 1 FROM Posts WHERE Id=?)`
var exist bool
err = transaction.Get(&exist, sql, reminder.PostId)
if err != nil {
return errors.Wrap(err, "failed to check for post")
}
if !exist {
return store.NewErrNotFound("Post", reminder.PostId)
}
query := s.getQueryBuilder().
Insert("PostReminders").
Columns("PostId", "UserId", "TargetTime").
Values(reminder.PostId, reminder.UserId, reminder.TargetTime)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE TargetTime = ?", reminder.TargetTime))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET TargetTime = ?", reminder.TargetTime))
}
sql, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "setPostReminder_tosql")
}
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
return errors.Wrap(err2, "failed to insert post reminder")
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) GetPostReminders(now int64) (_ []*model.PostReminder, err error) {
reminders := []*model.PostReminder{}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Select(&reminders, `SELECT PostId, UserId
FROM PostReminders
WHERE TargetTime < ?`, now)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to get post reminders")
}
if err == sql.ErrNoRows {
// No need to execute delete statement if there's nothing to delete.
return reminders, nil
}
// Postgres supports RETURNING * in a DELETE statement, but MySQL doesn't.
// So we are stuck with 2 queries. Not taking separate paths for Postgres
// and MySQL for simplicity.
_, err = transaction.Exec(`DELETE from PostReminders WHERE TargetTime < ?`, now)
if err != nil {
return nil, errors.Wrap(err, "failed to delete post reminders")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return reminders, nil
}
func (s *SqlPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
meta := &store.PostReminderMetadata{}
err := s.GetReplicaX().Get(meta, `SELECT c.id as ChannelId,
COALESCE(t.name, '') as TeamName,
u.locale as UserLocale, u.username as Username
FROM Posts p
JOIN Channels c ON p.ChannelId=c.Id
LEFT JOIN Teams t ON c.TeamId=t.Id
JOIN Users u ON p.UserId=u.Id
AND p.Id=?`, postID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get post reminder metadata: postId %s", postID)
}
return meta, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlPreferenceStore struct {
*SqlStore
}
func newSqlPreferenceStore(sqlStore *SqlStore) store.PreferenceStore {
s := &SqlPreferenceStore{sqlStore}
return s
}
func (s SqlPreferenceStore) deleteUnusedFeatures() {
mlog.Debug("Deleting any unused pre-release features")
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"Category": model.PreferenceCategoryAdvancedSettings}).
Where(sq.Eq{"Value": "false"}).
Where(sq.Like{"Name": store.FeatureTogglePrefix + "%"}).ToSql()
if err != nil {
mlog.Warn("Could not build sql query to delete unused features", mlog.Err(err))
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
mlog.Warn("Failed to delete unused features", mlog.Err(err))
}
}
func (s SqlPreferenceStore) Save(preferences model.Preferences) (err error) {
// wrap in a transaction so that if one fails, everything fails
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if upsertErr := s.saveTx(transaction, &preference); upsertErr != nil {
return upsertErr
}
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlPreferenceStore) save(transaction *sqlxTxWrapper, preference *model.Preference) error {
preference.PreUpdate()
if err := preference.IsValid(); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("Preferences").
Columns("UserId", "Category", "Name", "Value").
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
} else {
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to save Preference")
}
return nil
}
func (s SqlPreferenceStore) saveTx(transaction *sqlxTxWrapper, preference *model.Preference) error {
preference.PreUpdate()
if err := preference.IsValid(); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("Preferences").
Columns("UserId", "Category", "Name", "Value").
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
} else {
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to save Preference")
}
return nil
}
func (s SqlPreferenceStore) Get(userId string, category string, name string) (*model.Preference, error) {
var preference model.Preference
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Get(&preference, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s, category=%s, name=%s", userId, category, name)
}
return &preference, nil
}
func (s SqlPreferenceStore) GetCategoryAndName(category string, name string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with category=%s, name=%s", category, name)
}
return preferences, nil
}
func (s SqlPreferenceStore) GetCategory(userId string, category string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s, category=%s", userId, category)
}
return preferences, nil
}
func (s SqlPreferenceStore) GetAll(userId string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s", userId)
}
return preferences, nil
}
func (s SqlPreferenceStore) PermanentDeleteByUser(userId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by user")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s", userId)
}
return nil
}
func (s SqlPreferenceStore) Delete(userId, category, name string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s, category=%s and name=%s", userId, category, name)
}
return nil
}
func (s SqlPreferenceStore) DeleteCategory(userId string, category string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by category")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s and category=%s", userId, category)
}
return nil
}
func (s SqlPreferenceStore) DeleteCategoryAndName(category string, name string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"Name": name}).
Where(sq.Eq{"Category": category}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by category and name")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with category=%s and name=%s", category, name)
}
return nil
}
// DeleteOrphanedRows removes entries from Preferences (flagged post) when a
// corresponding post no longer exists.
func (s *SqlPreferenceStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM Preferences WHERE Name IN (
SELECT * FROM (
SELECT Preferences.Name FROM Preferences
LEFT JOIN Posts ON Preferences.Name = Posts.Id
WHERE Posts.Id IS NULL AND Category = ?
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, model.PreferenceCategoryFlaggedPost, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s SqlPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
if limit < 0 {
// uint64 does not throw an error, it overflows if it is negative.
// it is better to manually check here, or change the function type to uint64
return int64(0), errors.Errorf("Received a negative limit")
}
nameInQ, nameInArgs, err := sq.Select("*").
FromSelect(
sq.Select("Preferences.Name").
From("Preferences").
LeftJoin("Posts ON Preferences.Name = Posts.Id").
Where(sq.Eq{"Preferences.Category": model.PreferenceCategoryFlaggedPost}).
Where(sq.Eq{"Posts.Id": nil}).
Limit(uint64(limit)),
"t").
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build nested sql query to delete preference")
}
query, args, err := s.getQueryBuilder().Delete("Preferences").
Where(sq.Eq{"Category": model.PreferenceCategoryFlaggedPost}).
Where(sq.Expr("name IN ("+nameInQ+")", nameInArgs...)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build sql query to delete preference")
}
sqlResult, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to delete Preference")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return int64(0), errors.Wrap(err, "unable to get rows affected")
}
return rowsAffected, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlProductNoticesStore struct {
*SqlStore
}
func newSqlProductNoticesStore(sqlStore *SqlStore) store.ProductNoticesStore {
return &SqlProductNoticesStore{sqlStore}
}
func (s SqlProductNoticesStore) Clear(notices []string) error {
sql, args, err := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.Eq{"NoticeId": notices}).ToSql()
if err != nil {
return errors.Wrap(err, "product_notice_view_state_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrap(err, "failed to delete records from ProductNoticeViewState")
}
return nil
}
func (s SqlProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
var notices []string
for _, currentNotice := range currentNotices {
notices = append(notices, currentNotice.ID)
}
sql, args, err := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.NotEq{"NoticeId": notices}).ToSql()
if err != nil {
return errors.Wrap(err, "product_notice_view_state_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete records from ProductNoticeViewState")
}
return nil
}
func (s SqlProductNoticesStore) View(userId string, notices []string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
noticeStates := []model.ProductNoticeViewState{}
sql, args, err := s.getQueryBuilder().
Select("*").
From("ProductNoticeViewState").
Where(sq.And{sq.Eq{"UserId": userId}, sq.Eq{"NoticeId": notices}}).
ToSql()
if err != nil {
return errors.Wrap(err, "View_ToSql")
}
if err := transaction.Select(¬iceStates, sql, args...); err != nil {
return errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
}
now := time.Now().UTC().Unix()
// update existing records
for i := range noticeStates {
noticeStates[i].Viewed += 1
noticeStates[i].Timestamp = now
if _, err := transaction.NamedExec(`UPDATE ProductNoticeViewState
SET Viewed=:Viewed, Timestamp=:Timestamp WHERE UserId=:UserId AND NoticeId=:NoticeId`, ¬iceStates[i]); err != nil {
return errors.Wrapf(err, "failed to update ProductNoticeViewState")
}
}
// add new ones
haveNoticeState := func(n string) bool {
for _, ns := range noticeStates {
if ns.NoticeId == n {
return true
}
}
return false
}
for _, noticeId := range notices {
if !haveNoticeState(noticeId) {
productNoticeViewState := &model.ProductNoticeViewState{
UserId: userId,
NoticeId: noticeId,
Viewed: 1,
Timestamp: now,
}
if _, err := transaction.NamedExec(`INSERT INTO ProductNoticeViewState (UserId, NoticeId, Viewed, Timestamp)
VALUES (:UserId, :NoticeId, :Viewed, :Timestamp)`, productNoticeViewState); err != nil {
return errors.Wrapf(err, "failed to insert ProductNoticeViewState")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
noticeStates := []model.ProductNoticeViewState{}
sql, args, err := s.getQueryBuilder().Select("*").From("ProductNoticeViewState").Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "product_notice_view_state_tosql")
}
if err := s.GetReplicaX().Select(¬iceStates, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
}
return noticeStates, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
type SqlReactionStore struct {
*SqlStore
}
func newSqlReactionStore(sqlStore *SqlStore) store.ReactionStore {
return &SqlReactionStore{sqlStore}
}
func (s *SqlReactionStore) Save(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreSave()
if err := reaction.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if reaction.ChannelId == "" {
// get channelId, if not already populated
var channelIds []string
var args []interface{}
query := "SELECT ChannelId from Posts where Id = ?"
args = append(args, reaction.PostId)
err = transaction.Select(&channelIds, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed while getting channelId from Posts")
}
reaction.ChannelId = channelIds[0]
}
err = s.saveReactionAndUpdatePost(transaction, reaction)
if err != nil {
// We don't consider duplicated save calls as an error
if !IsUniqueConstraintError(err, []string{"reactions_pkey", "PRIMARY"}) {
return nil, errors.Wrap(err, "failed while saving reaction or updating post")
}
} else {
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
}
return reaction, nil
}
func (s *SqlReactionStore) Delete(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreUpdate()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := deleteReactionAndUpdatePost(transaction, reaction); err != nil {
return nil, errors.Wrap(err, "deleteReactionAndUpdatePost")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return reaction, nil
}
// GetForPost returns all reactions associated with `postId` that are not deleted.
func (s *SqlReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
queryString, args, err := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId", "ChannelId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0}).
OrderBy("CreateAt").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "reactions_getforpost_tosql")
}
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Reactions with postId=%s", postId)
}
return reactions, nil
}
// GetForPostSince returns all reactions associated with `postId` updated after `since`.
func (s *SqlReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
query := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Gt{"UpdateAt": since})
if excludeRemoteId != "" {
query = query.Where(sq.NotEq{"COALESCE(RemoteId, '')": excludeRemoteId})
}
if !inclDeleted {
query = query.Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0})
}
query.OrderBy("CreateAt")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "reactions_getforpostsince_tosql")
}
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
placeholder, values := constructArrayArgs(postIds)
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId,
ChannelId
FROM
Reactions
WHERE
PostId IN `+placeholder+` AND COALESCE(DeleteAt, 0) = 0
ORDER BY
CreateAt`, values...); err != nil {
return nil, errors.Wrap(err, "failed to get Reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error {
var reactions []*model.Reaction
now := model.GetMillis()
if err := s.GetReplicaX().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId
FROM
Reactions
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, emojiName); err != nil {
return errors.Wrapf(err, "failed to get Reactions with emojiName=%s", emojiName)
}
_, err := s.GetMasterX().Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, now, now, emojiName)
if err != nil {
return errors.Wrapf(err, "failed to delete Reactions with emojiName=%s", emojiName)
}
for _, reaction := range reactions {
reaction := reaction
_, err := s.GetMasterX().Exec(UpdatePostHasReactionsOnDeleteQuery, now, reaction.PostId, reaction.PostId)
if err != nil {
mlog.Warn("Unable to update Post.HasReactions while removing reactions",
mlog.String("post_id", reaction.PostId),
mlog.Err(err))
}
}
return nil
}
// DeleteOrphanedRows removes entries from Reactions when a corresponding post no longer exists.
func (s *SqlReactionStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM Reactions WHERE PostId IN (
SELECT * FROM (
SELECT PostId FROM Reactions
LEFT JOIN Posts ON Reactions.PostId = Posts.Id
WHERE Posts.Id IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if s.DriverName() == "postgres" {
query = "DELETE from Reactions WHERE CreateAt = any (array (SELECT CreateAt FROM Reactions WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from Reactions WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := s.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Reactions")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "unable to get rows affected for deleted Reactions")
}
return rowsAffected, nil
}
// GetTopForTeamSince returns the instance counts of the following Reactions sets:
// a) those created by anyone in private channels in the given user's membership graph on the given team, and
// b) those created by anyone in public channels on the given team.
func (s *SqlReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
query := `
SELECT
EmojiName,
sum(EmojiCount) AS Count
FROM ((
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
ChannelMembers
INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id
INNER JOIN Reactions ON Channels.Id = Reactions.ChannelId
WHERE
ChannelMembers.UserId = ?
AND Channels.Type = 'P'
AND Channels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)
UNION ALL (
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
Reactions
INNER JOIN PublicChannels ON Reactions.ChannelId = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)) AS A
WHERE
DeleteAt = 0
AND CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
if err := s.GetReplicaX().Select(&reactions, query, userID, teamID, teamID, since, limit+1, offset); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
// GetTopForUserSince returns the instance counts of the following Reactions sets:
// a) those created by the given user in any channel type on the given team (across the workspace if no team is given), and
// b) those created by the given user in DM or group channels.
func (s *SqlReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
var args []any
var query string
if teamID != "" {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
INNER JOIN Channels ON Channels.Id = Reactions.ChannelId
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND (Channels.TeamId = ? OR Channels.Type = 'D' OR Channels.Type = 'G')
AND Reactions.CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, teamID, since, limit + 1, offset}
} else {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND Reactions.CreateAt > ?
GROUP BY
Reactions.EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, since, limit + 1, offset}
}
if err := s.GetReplicaX().Select(&reactions, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
func (s *SqlReactionStore) saveReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
reaction.DeleteAt = 0
if s.DriverName() == model.DatabaseDriverMysql {
if _, err := transaction.NamedExec(
`INSERT INTO
Reactions
(UserId, PostId, EmojiName, CreateAt, UpdateAt, DeleteAt, RemoteId, ChannelId)
VALUES
(:UserId, :PostId, :EmojiName, :CreateAt, :UpdateAt, :DeleteAt, :RemoteId, :ChannelId)
ON DUPLICATE KEY UPDATE
UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, RemoteId = :RemoteId, ChannelId = :ChannelId`, reaction); err != nil {
return err
}
} else if s.DriverName() == model.DatabaseDriverPostgres {
if _, err := transaction.NamedExec(
`INSERT INTO
Reactions
(UserId, PostId, EmojiName, CreateAt, UpdateAt, DeleteAt, RemoteId, ChannelId)
VALUES
(:UserId, :PostId, :EmojiName, :CreateAt, :UpdateAt, :DeleteAt, :RemoteId, :ChannelId)
ON CONFLICT (UserId, PostId, EmojiName)
DO UPDATE SET UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, RemoteId = :RemoteId, ChannelId = :ChannelId`, reaction); err != nil {
return err
}
}
return updatePostForReactionsOnInsert(transaction, reaction.PostId)
}
func deleteReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
if _, err := transaction.Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?, RemoteId = ?
WHERE
PostId = ? AND
UserId = ? AND
EmojiName = ?`, reaction.UpdateAt, reaction.UpdateAt, reaction.RemoteId, reaction.PostId, reaction.UserId, reaction.EmojiName); err != nil {
return err
}
return updatePostForReactionsOnDelete(transaction, reaction.PostId)
}
const (
UpdatePostHasReactionsOnDeleteQuery = `UPDATE
Posts
SET
UpdateAt = ?,
HasReactions = (SELECT count(0) > 0 FROM Reactions WHERE PostId = ? AND COALESCE(DeleteAt, 0) = 0)
WHERE
Id = ?`
)
func updatePostForReactionsOnDelete(transaction *sqlxTxWrapper, postId string) error {
updateAt := model.GetMillis()
_, err := transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, updateAt, postId, postId)
return err
}
func updatePostForReactionsOnInsert(transaction *sqlxTxWrapper, postId string) error {
_, err := transaction.Exec(
`UPDATE
Posts
SET
HasReactions = True,
UpdateAt = ?
WHERE
Id = ?`,
model.GetMillis(),
postId,
)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type sqlRemoteClusterStore struct {
*SqlStore
}
func newSqlRemoteClusterStore(sqlStore *SqlStore) store.RemoteClusterStore {
return &sqlRemoteClusterStore{sqlStore}
}
func (s sqlRemoteClusterStore) Save(remoteCluster *model.RemoteCluster) (*model.RemoteCluster, error) {
remoteCluster.PreSave()
if err := remoteCluster.IsValid(); err != nil {
return nil, err
}
query := `INSERT INTO RemoteClusters
(RemoteId, RemoteTeamId, Name, DisplayName, SiteURL, CreateAt,
LastPingAt, Token, RemoteToken, Topics, CreatorId)
VALUES
(:RemoteId, :RemoteTeamId, :Name, :DisplayName, :SiteURL, :CreateAt,
:LastPingAt, :Token, :RemoteToken, :Topics, :CreatorId)`
if _, err := s.GetMasterX().NamedExec(query, remoteCluster); err != nil {
return nil, errors.Wrap(err, "failed to save RemoteCluster")
}
return remoteCluster, nil
}
func (s sqlRemoteClusterStore) Update(remoteCluster *model.RemoteCluster) (*model.RemoteCluster, error) {
remoteCluster.PreUpdate()
if err := remoteCluster.IsValid(); err != nil {
return nil, err
}
query := `UPDATE RemoteClusters
SET Token = :Token,
RemoteTeamId = :RemoteTeamId,
CreateAt = :CreateAt,
LastPingAt = :LastPingAt,
RemoteToken = :RemoteToken,
CreatorId = :CreatorId,
DisplayName = :DisplayName,
SiteURL = :SiteURL,
Topics = :Topics
WHERE RemoteId = :RemoteId AND Name = :Name`
if _, err := s.GetMasterX().NamedExec(query, remoteCluster); err != nil {
return nil, errors.Wrap(err, "failed to update RemoteCluster")
}
return remoteCluster, nil
}
func (s sqlRemoteClusterStore) Delete(remoteId string) (bool, error) {
squery, args, err := s.getQueryBuilder().
Delete("RemoteClusters").
Where(sq.Eq{"RemoteId": remoteId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_remote_cluster_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete RemoteCluster")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
return count > 0, nil
}
func (s sqlRemoteClusterStore) Get(remoteId string) (*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("*").
From("RemoteClusters").
Where(sq.Eq{"RemoteId": remoteId})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_get_tosql")
}
var rc model.RemoteCluster
if err := s.GetReplicaX().Get(&rc, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find RemoteCluster")
}
return &rc, nil
}
func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("rc.*").
From("RemoteClusters rc")
if filter.InChannel != "" {
query = query.Where("rc.RemoteId IN (SELECT scr.RemoteId FROM SharedChannelRemotes scr WHERE scr.ChannelId = ?)", filter.InChannel)
}
if filter.NotInChannel != "" {
query = query.Where("rc.RemoteId NOT IN (SELECT scr.RemoteId FROM SharedChannelRemotes scr WHERE scr.ChannelId = ?)", filter.NotInChannel)
}
if filter.ExcludeOffline {
query = query.Where(sq.Gt{"rc.LastPingAt": model.GetMillis() - model.RemoteOfflineAfterMillis})
}
if filter.CreatorId != "" {
query = query.Where(sq.Eq{"rc.CreatorId": filter.CreatorId})
}
if filter.OnlyConfirmed {
query = query.Where(sq.NotEq{"rc.SiteURL": ""})
}
if filter.Topic != "" {
trimmed := strings.TrimSpace(filter.Topic)
if trimmed == "" || trimmed == "*" {
return nil, errors.New("invalid topic")
}
queryTopic := fmt.Sprintf("%% %s %%", trimmed)
query = query.Where(sq.Or{sq.Like{"rc.Topics": queryTopic}, sq.Eq{"rc.Topics": "*"}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_getall_tosql")
}
list := []*model.RemoteCluster{}
if err := s.GetReplicaX().Select(&list, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find RemoteClusters")
}
return list, nil
}
func (s sqlRemoteClusterStore) UpdateTopics(remoteClusterid string, topics string) (*model.RemoteCluster, error) {
rc, err := s.Get(remoteClusterid)
if err != nil {
return nil, err
}
rc.Topics = topics
rc.PreUpdate()
query := `UPDATE RemoteClusters
SET Topics = :Topics
WHERE RemoteId = :RemoteId`
if _, err = s.GetMasterX().NamedExec(query, rc); err != nil {
return nil, err
}
return rc, nil
}
func (s sqlRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
query := s.getQueryBuilder().
Update("RemoteClusters").
Set("LastPingAt", model.GetMillis()).
Where(sq.Eq{"RemoteId": remoteClusterId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "remote_cluster_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to update RemoteCluster")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlRetentionPolicyStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlRetentionPolicyStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.RetentionPolicyStore {
return &SqlRetentionPolicyStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
// executePossiblyEmptyQuery only executes the query if it is non-empty. This helps avoid
// having to check for MySQL, which, unlike Postgres, does not allow empty queries.
func executePossiblyEmptyQuery(txn *sqlxTxWrapper, query string, args ...any) (sql.Result, error) {
if query == "" {
return nil, nil
}
return txn.Exec(query, args...)
}
func (s *SqlRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
// Strategy:
// 1. Insert new policy
// 2. Insert new channels into policy
// 3. Insert new teams into policy
if err = s.checkTeamsExist(policy.TeamIDs); err != nil {
return nil, err
}
if err = s.checkChannelsExist(policy.ChannelIDs); err != nil {
return nil, err
}
policy.ID = model.NewId()
policyInsertQuery, policyInsertArgs, err := s.getQueryBuilder().
Insert("RetentionPolicies").
Columns("Id", "DisplayName", "PostDuration").
Values(policy.ID, policy.DisplayName, policy.PostDurationDays).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err := s.buildInsertRetentionPoliciesChannelsQuery(policy.ID, policy.ChannelIDs)
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err := s.buildInsertRetentionPoliciesTeamsQuery(policy.ID, policy.TeamIDs)
if err != nil {
return nil, err
}
queryString, args, err := s.buildGetPolicyQuery(policy.ID)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Create a new policy in RetentionPolicies
if _, err = txn.Exec(policyInsertQuery, policyInsertArgs...); err != nil {
return nil, err
}
// Insert the channel IDs into RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Insert the team IDs into RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the new policy (with team/channel counts) which we just created
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) checkTeamsExist(teamIDs []string) error {
if len(teamIDs) > 0 {
teamsSelectQuery, teamsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Teams").
Where(sq.Eq{"Id": teamIDs}).
ToSql()
if err != nil {
return err
}
rows := []*string{}
err = s.GetReplicaX().Select(&rows, teamsSelectQuery, teamsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(teamIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, teamID := range rows {
retrievedIDs[*teamID] = true
}
for _, teamID := range teamIDs {
if _, ok := retrievedIDs[teamID]; !ok {
return store.NewErrNotFound("Team", teamID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) checkChannelsExist(channelIDs []string) error {
if len(channelIDs) > 0 {
channelsSelectQuery, channelsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Channels").
Where(sq.Eq{"Id": channelIDs}).
ToSql()
if err != nil {
return err
}
rows := []*string{}
err = s.GetReplicaX().Select(&rows, channelsSelectQuery, channelsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(channelIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, channelID := range rows {
retrievedIDs[*channelID] = true
}
for _, channelID := range channelIDs {
if _, ok := retrievedIDs[channelID]; !ok {
return store.NewErrNotFound("Channel", channelID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesChannelsQuery(policyID string, channelIDs []string) (query string, args []any, err error) {
if len(channelIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("PolicyId", "ChannelId")
for _, channelID := range channelIDs {
builder = builder.Values(policyID, channelID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesTeamsQuery(policyID string, teamIDs []string) (query string, args []any, err error) {
if len(teamIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamID := range teamIDs {
builder = builder.Values(policyID, teamID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
// Strategy:
// 1. Update policy attributes
// 2. Delete existing channels from policy
// 3. Insert new channels into policy
// 4. Delete existing teams from policy
// 5. Insert new teams into policy
// 6. Read new policy
if err = s.checkTeamsExist(patch.TeamIDs); err != nil {
return nil, err
}
if err = s.checkChannelsExist(patch.ChannelIDs); err != nil {
return nil, err
}
policyUpdateQuery := ""
policyUpdateArgs := []any{}
if patch.DisplayName != "" || patch.PostDurationDays != nil {
builder := s.getQueryBuilder().Update("RetentionPolicies")
if patch.DisplayName != "" {
builder = builder.Set("DisplayName", patch.DisplayName)
}
if patch.PostDurationDays != nil {
builder = builder.Set("PostDuration", *patch.PostDurationDays)
}
policyUpdateQuery, policyUpdateArgs, err = builder.
Where(sq.Eq{"Id": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
}
channelsDeleteQuery := ""
channelsDeleteArgs := []any{}
channelsInsertQuery := ""
channelsInsertArgs := []any{}
if patch.ChannelIDs != nil {
channelsDeleteQuery, channelsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err = s.buildInsertRetentionPoliciesChannelsQuery(patch.ID, patch.ChannelIDs)
if err != nil {
return nil, err
}
}
teamsDeleteQuery := ""
teamsDeleteArgs := []any{}
teamsInsertQuery := ""
teamsInsertArgs := []any{}
if patch.TeamIDs != nil {
teamsDeleteQuery, teamsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err = s.buildInsertRetentionPoliciesTeamsQuery(patch.ID, patch.TeamIDs)
if err != nil {
return nil, err
}
}
queryString, args, err := s.buildGetPolicyQuery(patch.ID)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Update the fields of the policy in RetentionPolicies
if _, err = executePossiblyEmptyQuery(txn, policyUpdateQuery, policyUpdateArgs...); err != nil {
return nil, err
}
// Remove all channels from the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsDeleteQuery, channelsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new channels for the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Remove all teams from the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsDeleteQuery, teamsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new teams for the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the policy which we just updated
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) buildGetPolicyQuery(id string) (string, []any, error) {
return s.buildGetPoliciesQuery(id, 0, 1)
}
// buildGetPoliciesQuery builds a query to select information for the policy with the specified
// ID, or, if `id` is the empty string, from all policies. The results returned will be sorted by
// policy display name and ID.
func (s *SqlRetentionPolicyStore) buildGetPoliciesQuery(id string, offset, limit int) (string, []any, error) {
rpcSubQuery := s.getQueryBuilder().
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesChannels.ChannelId) AS Count").
From("RetentionPolicies").
LeftJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
GroupBy("RetentionPolicies.Id").
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
if id != "" {
rpcSubQuery = rpcSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
}
rpcSubQueryString, args, err := rpcSubQuery.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
rptSubQuery := s.getQueryBuilder().
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesTeams.TeamId) AS Count").
From("RetentionPolicies").
LeftJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
GroupBy("RetentionPolicies.Id").
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
if id != "" {
rptSubQuery = rptSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
}
rptSubQueryString, _, err := rptSubQuery.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
query := s.getQueryBuilder().
Select(`
RetentionPolicies.Id as "Id",
RetentionPolicies.DisplayName,
RetentionPolicies.PostDuration as "PostDuration",
A.Count AS ChannelCount,
B.Count AS TeamCount
`).
From("RetentionPolicies").
InnerJoin(`(` + rpcSubQueryString + `) AS A ON RetentionPolicies.Id = A.Id`).
InnerJoin(`(` + rptSubQueryString + `) AS B ON RetentionPolicies.Id = B.Id`).
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id")
queryString, _, err := query.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
// MySQL does not support positional params, so we add one param for each WHERE clause.
if s.DriverName() == model.DatabaseDriverMysql {
args = append(args, args...)
}
return queryString, args, nil
}
func (s *SqlRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
queryString, args, err := s.buildGetPolicyQuery(id)
if err != nil {
return nil, err
}
var policy model.RetentionPolicyWithTeamAndChannelCounts
if err := s.GetReplicaX().Get(&policy, queryString, args...); err != nil {
return nil, err
}
return &policy, nil
}
func (s *SqlRetentionPolicyStore) GetAll(offset, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
policies := []*model.RetentionPolicyWithTeamAndChannelCounts{}
queryString, args, err := s.buildGetPoliciesQuery("", offset, limit)
if err != nil {
return policies, err
}
err = s.GetReplicaX().Select(&policies, queryString, args...)
return policies, err
}
func (s *SqlRetentionPolicyStore) GetCount() (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, "SELECT COUNT(*) FROM RetentionPolicies")
if err != nil {
return count, err
}
return count, nil
}
func (s *SqlRetentionPolicyStore) Delete(id string) error {
query := s.getQueryBuilder().
Delete("RetentionPolicies").
Where(sq.Eq{"Id": id})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_tosql")
}
sqlResult, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to permanent delete retention policy with id=%s", id)
}
numRowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return errors.Wrap(err, "unable to get rows affected")
} else if numRowsAffected == 0 {
return errors.New("policy not found")
}
return nil
}
func (s *SqlRetentionPolicyStore) GetChannels(policyId string, offset, limit int) (model.ChannelListWithTeamData, error) {
query := s.getQueryBuilder().Select(`Channels.*, Teams.DisplayName AS TeamDisplayName,
Teams.Name AS TeamName,Teams.UpdateAt AS TeamUpdateAt`).
From("RetentionPoliciesChannels").
InnerJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": policyId}).
OrderBy("Channels.DisplayName, Channels.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "retention_policies_channels_tosql")
}
channels := model.ChannelListWithTeamData{}
if err := s.GetReplicaX().Select(&channels, queryString, args...); err != nil {
return channels, errors.Wrap(err, "failed to find RetentionPoliciesChannels")
}
for _, channel := range channels {
channel.PolicyID = model.NewString(policyId)
}
return channels, nil
}
func (s *SqlRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("RetentionPolicies").
InnerJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
Where(sq.Eq{"RetentionPolicies.Id": policyId})
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "retention_policies_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
if err := s.checkChannelsExist(channelIds); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("policyId", "channelId")
for _, channelId := range channelIds {
query = query.Values(policyId, channelId)
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_channels_tosql")
}
_, err = s.GetMasterX().Exec(queryString, args...)
if err != nil {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
case *mysql.MySQLError:
if dbErr.Number == MySQLForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
query := s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"ChannelId": channelIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_channels_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to permanent delete retention policy channels with policyid=%s", policyId)
}
return nil
}
func (s *SqlRetentionPolicyStore) GetTeams(policyId string, offset, limit int) ([]*model.Team, error) {
query := s.getQueryBuilder().
Select("Teams.*").
From("RetentionPoliciesTeams").
InnerJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": policyId}).
OrderBy("Teams.DisplayName, Teams.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "retention_policies_teams_tosql")
}
teams := []*model.Team{}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return teams, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
func (s *SqlRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("RetentionPolicies").
InnerJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
Where(sq.Eq{"RetentionPolicies.Id": policyId})
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "retention_policies_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
if err := s.checkTeamsExist(teamIds); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamId := range teamIds {
query = query.Values(policyId, teamId)
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_teams_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to insert retention policies teams")
}
return nil
}
func (s *SqlRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
query := s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"TeamId": teamIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_teams_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "unable to permanent delete retention policies teams with policyid=%s", policyId)
}
return nil
}
func subQueryIN(property string, query sq.SelectBuilder) sq.Sqlizer {
queryString, args := query.MustSql()
subQuery := fmt.Sprintf("%s IN (SELECT * FROM (%s) AS A)", property, queryString)
return sq.Expr(subQuery, args...)
}
// DeleteOrphanedRows removes entries from RetentionPoliciesChannels and RetentionPoliciesTeams
// where a channel or team no longer exists.
func (s *SqlRetentionPolicyStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
rpcSubQuery := sq.Select("ChannelId").
From("RetentionPoliciesChannels").
LeftJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
Where("Channels.Id IS NULL").
Limit(uint64(limit))
rpcDeleteQuery, rpcArgs, err := s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(subQueryIN("ChannelId", rpcSubQuery)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "retention_policies_channels_tosql")
}
rptSubQuery := sq.Select("TeamId").
From("RetentionPoliciesTeams").
LeftJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
Where("Teams.Id IS NULL").
Limit(uint64(limit))
rptDeleteQuery, rptArgs, err := s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(subQueryIN("TeamId", rptSubQuery)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "retention_policies_teams_tosql")
}
result, err := s.GetMasterX().Exec(rpcDeleteQuery, rpcArgs...)
if err != nil {
return
}
rpcDeleted, err := result.RowsAffected()
if err != nil {
return
}
result, err = s.GetMasterX().Exec(rptDeleteQuery, rptArgs...)
if err != nil {
return
}
rptDeleted, err := result.RowsAffected()
if err != nil {
return
}
deleted = rpcDeleted + rptDeleted
return
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForTeam, error) {
query := s.getQueryBuilder().
Select(`Teams.Id AS "Id", RetentionPolicies.PostDuration AS "PostDuration"`).
From("Users").
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
},
).
OrderBy("Teams.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_policies_for_user_tosql")
}
policies := []*model.RetentionPolicyForTeam{}
if err := s.GetReplicaX().Select(&policies, queryString, args...); err != nil {
return policies, errors.Wrap(err, "failed to find Users")
}
return policies, nil
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("Users").
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
},
)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_policies_count_for_user_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count TeamPoliciesCountForUser")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error) {
query := s.getQueryBuilder().
Select(`Channels.Id as "Id", RetentionPolicies.PostDuration as "PostDuration"`).
From("Users").
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"Channels.DeleteAt": 0},
},
).
OrderBy("Channels.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_policies_for_user_tosql")
}
policies := []*model.RetentionPolicyForChannel{}
if err := s.GetReplicaX().Select(&policies, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return policies, nil
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("Users").
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"Channels.DeleteAt": 0},
},
)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_policies_count_users_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count ChannelPoliciesCountForUser")
}
return count, nil
}
// RetentionPolicyBatchDeletionInfo gives information on how to delete records
// under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`.
//
// `BaseBuilder` should already have selected the primary key(s) for the main table
// and should be joined to a table with a ChannelId column, which will be used to join
// on the Channels table.
// `Table` is the name of the table from which records are being deleted.
// `TimeColumn` is the name of the column which contains the timestamp of the record.
// `PrimaryKeys` contains the primary keys of `table`. It should be the same as the
// `From` clause in `baseBuilder`.
// `ChannelIDTable` is the table which contains the ChannelId column, it may be the
// same as `table`, or will be different if a join was used.
// `NowMillis` must be a Unix timestamp in milliseconds and is used by the granular
// policies; if `nowMillis - timestamp(record)` is greater than
// the post duration of a granular policy, than the record will be deleted.
// `GlobalPolicyEndTime` is used by the global policy; any record older than this time
// will be deleted by the global policy if it does not fall under a granular policy.
// To disable the granular policies, set `NowMillis` to 0.
// To disable the global policy, set `GlobalPolicyEndTime` to 0.
type RetentionPolicyBatchDeletionInfo struct {
BaseBuilder sq.SelectBuilder
Table string
TimeColumn string
PrimaryKeys []string
ChannelIDTable string
NowMillis int64
GlobalPolicyEndTime int64
Limit int64
}
// genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables
// which need to delete records for granular and global policies.
func genericPermanentDeleteBatchForRetentionPolicies(
r RetentionPolicyBatchDeletionInfo,
s *SqlStore,
cursor model.RetentionPolicyCursor,
) (int64, model.RetentionPolicyCursor, error) {
baseBuilder := r.BaseBuilder.InnerJoin("Channels ON " + r.ChannelIDTable + ".ChannelId = Channels.Id")
scopedTimeColumn := r.Table + "." + r.TimeColumn
nowStr := strconv.FormatInt(r.NowMillis, 10)
// A record falls under the scope of a granular retention policy if:
// 1. The policy's post duration is >= 0
// 2. The record's lifespan has not exceeded the policy's post duration
const millisecondsInADay = 24 * 60 * 60 * 1000
fallsUnderGranularPolicy := sq.And{
sq.GtOrEq{"RetentionPolicies.PostDuration": 0},
sq.Expr(nowStr + " - " + scopedTimeColumn + " > RetentionPolicies.PostDuration * " + strconv.FormatInt(millisecondsInADay, 10)),
}
// If the caller wants to disable the global policy from running
if r.GlobalPolicyEndTime <= 0 {
cursor.GlobalPoliciesDone = true
}
// If the caller wants to disable the granular policies from running
if r.NowMillis <= 0 {
cursor.ChannelPoliciesDone = true
cursor.TeamPoliciesDone = true
}
var totalRowsAffected int64
// First, delete all of the records which fall under the scope of a channel-specific policy
if !cursor.ChannelPoliciesDone {
channelPoliciesBuilder := baseBuilder.
InnerJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(fallsUnderGranularPolicy).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(channelPoliciesBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.ChannelPoliciesDone = true
}
totalRowsAffected += rowsAffected
r.Limit -= rowsAffected
}
// Next, delete all of the records which fall under the scope of a team-specific policy
if cursor.ChannelPoliciesDone && !cursor.TeamPoliciesDone {
// Channel-specific policies override team-specific policies.
teamPoliciesBuilder := baseBuilder.
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(sq.And{
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
sq.Expr("RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id"),
}).
Where(fallsUnderGranularPolicy).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(teamPoliciesBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.TeamPoliciesDone = true
}
totalRowsAffected += rowsAffected
r.Limit -= rowsAffected
}
// Finally, delete all of the records which fall under the scope of the global policy
if cursor.ChannelPoliciesDone && cursor.TeamPoliciesDone && !cursor.GlobalPoliciesDone {
// Granular policies override the global policy.
globalPolicyBuilder := baseBuilder.
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
LeftJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
LeftJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(sq.And{
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
sq.Eq{"RetentionPoliciesTeams.PolicyId": nil},
}).
Where(sq.Lt{scopedTimeColumn: r.GlobalPolicyEndTime}).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(globalPolicyBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.GlobalPoliciesDone = true
}
totalRowsAffected += rowsAffected
}
return totalRowsAffected, cursor, nil
}
// genericRetentionPoliciesDeletion actually executes the DELETE query using a sq.SelectBuilder
// which selects the rows to delete.
func genericRetentionPoliciesDeletion(
builder sq.SelectBuilder,
r RetentionPolicyBatchDeletionInfo,
s *SqlStore,
) (rowsAffected int64, err error) {
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, r.Table+"_tosql")
}
if s.DriverName() == model.DatabaseDriverPostgres {
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
query = `
DELETE FROM ` + r.Table + ` WHERE ` + primaryKeysStr + ` IN (
` + query + `
)`
} else {
// MySQL does not support the LIMIT clause in a subquery with IN
clauses := make([]string, len(r.PrimaryKeys))
for i, key := range r.PrimaryKeys {
clauses[i] = r.Table + "." + key + " = A." + key
}
joinClause := strings.Join(clauses, " AND ")
query = `
DELETE ` + r.Table + ` FROM ` + r.Table + ` INNER JOIN (
` + query + `
) AS A ON ` + joinClause
}
result, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlRoleStore struct {
*SqlStore
}
type Role struct {
Id string
Name string
DisplayName string
Description string
CreateAt int64
UpdateAt int64
DeleteAt int64
Permissions string
SchemeManaged bool
BuiltIn bool
}
type channelRolesPermissions struct {
GuestRoleName string
UserRoleName string
AdminRoleName string
HigherScopedGuestPermissions string
HigherScopedUserPermissions string
HigherScopedAdminPermissions string
}
func NewRoleFromModel(role *model.Role) *Role {
permissionsMap := make(map[string]bool)
permissions := ""
for _, permission := range role.Permissions {
if !permissionsMap[permission] {
permissions += fmt.Sprintf(" %v", permission)
permissionsMap[permission] = true
}
}
return &Role{
Id: role.Id,
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
CreateAt: role.CreateAt,
UpdateAt: role.UpdateAt,
DeleteAt: role.DeleteAt,
Permissions: permissions,
SchemeManaged: role.SchemeManaged,
BuiltIn: role.BuiltIn,
}
}
func (role Role) ToModel() *model.Role {
return &model.Role{
Id: role.Id,
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
CreateAt: role.CreateAt,
UpdateAt: role.UpdateAt,
DeleteAt: role.DeleteAt,
Permissions: strings.Fields(role.Permissions),
SchemeManaged: role.SchemeManaged,
BuiltIn: role.BuiltIn,
}
}
func newSqlRoleStore(sqlStore *SqlStore) store.RoleStore {
return &SqlRoleStore{sqlStore}
}
func (s *SqlRoleStore) Save(role *model.Role) (_ *model.Role, err error) {
// Check the role is valid before proceeding.
if !role.IsValidWithoutId() {
return nil, store.NewErrInvalidInput("Role", "<any>", fmt.Sprintf("%v", role))
}
if role.Id == "" {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
createdRole, terr := s.createRole(role, transaction)
if terr != nil {
return nil, errors.Wrap(terr, "unable to create Role")
} else if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "commit_transaction")
}
return createdRole, nil
}
dbRole := NewRoleFromModel(role)
dbRole.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Roles
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName,
Description=:Description, Permissions=:Permissions, SchemeManaged=:SchemeManaged, BuiltIn=:BuiltIn
WHERE Id=:Id`, &dbRole)
if err != nil {
return nil, errors.Wrap(err, "failed to update Role")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, fmt.Errorf("invalid number of updated rows, expected 1 but got %d", rowsChanged)
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) createRole(role *model.Role, transaction *sqlxTxWrapper) (*model.Role, error) {
// Check the role is valid before proceeding.
if !role.IsValidWithoutId() {
return nil, store.NewErrInvalidInput("Role", "<any>", fmt.Sprintf("%v", role))
}
dbRole := NewRoleFromModel(role)
dbRole.Id = model.NewId()
dbRole.CreateAt = model.GetMillis()
dbRole.UpdateAt = dbRole.CreateAt
if _, err := transaction.NamedExec(`INSERT INTO Roles
(Id, Name, DisplayName, Description, Permissions, CreateAt, UpdateAt, DeleteAt, SchemeManaged, BuiltIn)
VALUES
(:Id, :Name, :DisplayName, :Description, :Permissions, :CreateAt, :UpdateAt, :DeleteAt, :SchemeManaged, :BuiltIn)`, dbRole); err != nil {
return nil, errors.Wrap(err, "failed to save Role")
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) Get(roleId string) (*model.Role, error) {
dbRole := Role{}
if err := s.GetReplicaX().Get(&dbRole, "SELECT * from Roles WHERE Id = ?", roleId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", roleId)
}
return nil, errors.Wrap(err, "failed to get Role")
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) GetAll() ([]*model.Role, error) {
dbRoles := []Role{}
if err := s.GetReplicaX().Select(&dbRoles, "SELECT * from Roles"); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
func (s *SqlRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
dbRole := Role{}
if err := s.DBXFromContext(ctx).Get(&dbRole, "SELECT * from Roles WHERE Name = ?", name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to find Roles with name=%s", name)
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) GetByNames(names []string) ([]*model.Role, error) {
if len(names) == 0 {
return []*model.Role{}, nil
}
query := s.getQueryBuilder().
Select("Id, Name, DisplayName, Description, CreateAt, UpdateAt, DeleteAt, Permissions, SchemeManaged, BuiltIn").
From("Roles").
Where(sq.Eq{"Name": names})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
defer rows.Close()
for rows.Next() {
var role Role
err = rows.Scan(
&role.Id, &role.Name, &role.DisplayName, &role.Description,
&role.CreateAt, &role.UpdateAt, &role.DeleteAt, &role.Permissions,
&role.SchemeManaged, &role.BuiltIn)
if err != nil {
return nil, errors.Wrap(err, "failed to scan values")
}
roles = append(roles, role.ToModel())
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "unable to iterate over rows")
}
return roles, nil
}
func (s *SqlRoleStore) Delete(roleId string) (*model.Role, error) {
// Get the role.
var role Role
if err := s.GetReplicaX().Get(&role, "SELECT * from Roles WHERE Id = ?", roleId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", roleId)
}
return nil, errors.Wrapf(err, "failed to get Role with id=%s", roleId)
}
time := model.GetMillis()
role.DeleteAt = time
role.UpdateAt = time
res, err := s.GetMasterX().NamedExec(`UPDATE Roles
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName,
Description=:Description, Permissions=:Permissions, SchemeManaged=:SchemeManaged, BuiltIn=:BuiltIn
WHERE Id=:Id`, &role)
if err != nil {
return nil, errors.Wrap(err, "failed to update Role")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, fmt.Errorf("invalid number of updated rows, expected 1 but got %d", rowsChanged)
}
return role.ToModel(), nil
}
func (s *SqlRoleStore) PermanentDeleteAll() error {
if _, err := s.GetMasterX().Exec("DELETE FROM Roles"); err != nil {
return errors.Wrap(err, "failed to delete Roles")
}
return nil
}
func (s *SqlRoleStore) channelHigherScopedPermissionsQuery(roleNames []string) string {
sqlTmpl := `
SELECT
'' AS GuestRoleName,
RoleSchemes.DefaultChannelUserRole AS UserRoleName,
RoleSchemes.DefaultChannelAdminRole AS AdminRoleName,
'' AS HigherScopedGuestPermissions,
UserRoles.Permissions AS HigherScopedUserPermissions,
AdminRoles.Permissions AS HigherScopedAdminPermissions
FROM
Schemes AS RoleSchemes
JOIN Channels ON Channels.SchemeId = RoleSchemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Schemes ON Schemes.Id = Teams.SchemeId
RIGHT JOIN Roles AS UserRoles ON UserRoles.Name = Schemes.DefaultChannelUserRole
RIGHT JOIN Roles AS AdminRoles ON AdminRoles.Name = Schemes.DefaultChannelAdminRole
WHERE
RoleSchemes.DefaultChannelUserRole IN ('%[1]s')
OR RoleSchemes.DefaultChannelAdminRole IN ('%[1]s')
UNION
SELECT
RoleSchemes.DefaultChannelGuestRole AS GuestRoleName,
'' AS UserRoleName,
'' AS AdminRoleName,
GuestRoles.Permissions AS HigherScopedGuestPermissions,
'' AS HigherScopedUserPermissions,
'' AS HigherScopedAdminPermissions
FROM
Schemes AS RoleSchemes
JOIN Channels ON Channels.SchemeId = RoleSchemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Schemes ON Schemes.Id = Teams.SchemeId
RIGHT JOIN Roles AS GuestRoles ON GuestRoles.Name = Schemes.DefaultChannelGuestRole
WHERE
RoleSchemes.DefaultChannelGuestRole IN ('%[1]s')
UNION
SELECT
Schemes.DefaultChannelGuestRole AS GuestRoleName,
Schemes.DefaultChannelUserRole AS UserRoleName,
Schemes.DefaultChannelAdminRole AS AdminRoleName,
GuestRoles.Permissions AS HigherScopedGuestPermissions,
UserRoles.Permissions AS HigherScopedUserPermissions,
AdminRoles.Permissions AS HigherScopedAdminPermissions
FROM
Schemes
JOIN Channels ON Channels.SchemeId = Schemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Roles AS GuestRoles ON GuestRoles.Name = '%[2]s'
JOIN Roles AS UserRoles ON UserRoles.Name = '%[3]s'
JOIN Roles AS AdminRoles ON AdminRoles.Name = '%[4]s'
WHERE
(Schemes.DefaultChannelGuestRole IN ('%[1]s')
OR Schemes.DefaultChannelUserRole IN ('%[1]s')
OR Schemes.DefaultChannelAdminRole IN ('%[1]s'))
AND (Teams.SchemeId = ''
OR Teams.SchemeId IS NULL)
`
// The below three channel role names are referenced by their name value because there is no system scheme
// record that ships with Mattermost, otherwise the system scheme would be referenced by name and the channel
// roles would be referenced by their column names.
return fmt.Sprintf(
sqlTmpl,
strings.Join(roleNames, "', '"),
model.ChannelGuestRoleId,
model.ChannelUserRoleId,
model.ChannelAdminRoleId,
)
}
func (s *SqlRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
query := s.channelHigherScopedPermissionsQuery(roleNames)
rolesPermissions := []*channelRolesPermissions{}
if err := s.GetReplicaX().Select(&rolesPermissions, query); err != nil {
return nil, errors.Wrap(err, "failed to find RolePermissions")
}
roleNameHigherScopedPermissions := map[string]*model.RolePermissions{}
for _, rp := range rolesPermissions {
roleNameHigherScopedPermissions[rp.GuestRoleName] = &model.RolePermissions{RoleID: model.ChannelGuestRoleId, Permissions: strings.Split(rp.HigherScopedGuestPermissions, " ")}
roleNameHigherScopedPermissions[rp.UserRoleName] = &model.RolePermissions{RoleID: model.ChannelUserRoleId, Permissions: strings.Split(rp.HigherScopedUserPermissions, " ")}
roleNameHigherScopedPermissions[rp.AdminRoleName] = &model.RolePermissions{RoleID: model.ChannelAdminRoleId, Permissions: strings.Split(rp.HigherScopedAdminPermissions, " ")}
}
return roleNameHigherScopedPermissions, nil
}
func (s *SqlRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
query := s.getQueryBuilder().
Select("Roles.*").
From("Schemes").
Join("Roles ON Schemes.DefaultChannelGuestRole = Roles.Name OR Schemes.DefaultChannelUserRole = Roles.Name OR Schemes.DefaultChannelAdminRole = Roles.Name").
Where(sq.Eq{"Schemes.Scope": model.SchemeScopeChannel}).
Where(sq.Eq{"Roles.DeleteAt": 0}).
Where(sq.Eq{"Schemes.DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
dbRoles := []*Role{}
if err = s.GetReplicaX().Select(&dbRoles, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
// ChannelRolesUnderTeamRole finds all of the channel-scheme roles under the team of the given team-scheme role.
func (s *SqlRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
query := s.getQueryBuilder().
Select("ChannelSchemeRoles.*").
From("Roles AS HigherScopedRoles").
Join("Schemes AS HigherScopedSchemes ON (HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelGuestRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelUserRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelAdminRole)").
Join("Teams ON Teams.SchemeId = HigherScopedSchemes.Id").
Join("Channels ON Channels.TeamId = Teams.Id").
Join("Schemes AS ChannelSchemes ON Channels.SchemeId = ChannelSchemes.Id").
Join("Roles AS ChannelSchemeRoles ON (ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelGuestRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelUserRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelAdminRole)").
Where(sq.Eq{"HigherScopedSchemes.Scope": model.SchemeScopeTeam}).
Where(sq.Eq{"HigherScopedRoles.Name": roleName}).
Where(sq.Eq{"HigherScopedRoles.DeleteAt": 0}).
Where(sq.Eq{"HigherScopedSchemes.DeleteAt": 0}).
Where(sq.Eq{"Teams.DeleteAt": 0}).
Where(sq.Eq{"Channels.DeleteAt": 0}).
Where(sq.Eq{"ChannelSchemes.DeleteAt": 0}).
Where(sq.Eq{"ChannelSchemeRoles.DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
dbRoles := []*Role{}
if err = s.GetReplicaX().Select(&dbRoles, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
SchemeRoleDisplayNameTeamAdmin = "Team Admin Role for Scheme"
SchemeRoleDisplayNameTeamUser = "Team User Role for Scheme"
SchemeRoleDisplayNameTeamGuest = "Team Guest Role for Scheme"
SchemeRoleDisplayNameChannelAdmin = "Channel Admin Role for Scheme"
SchemeRoleDisplayNameChannelUser = "Channel User Role for Scheme"
SchemeRoleDisplayNameChannelGuest = "Channel Guest Role for Scheme"
SchemeRoleDisplayNamePlaybookAdmin = "Playbook Admin Role for Scheme"
SchemeRoleDisplayNamePlaybookMember = "Playbook Member Role for Scheme"
SchemeRoleDisplayNameRunAdmin = "Run Admin Role for Scheme"
SchemeRoleDisplayNameRunMember = "Run Member Role for Scheme"
)
type SqlSchemeStore struct {
*SqlStore
}
func newSqlSchemeStore(sqlStore *SqlStore) store.SchemeStore {
return &SqlSchemeStore{sqlStore}
}
func (s *SqlSchemeStore) Save(scheme *model.Scheme) (_ *model.Scheme, err error) {
if scheme.Id == "" {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
newScheme, terr := s.createScheme(scheme, transaction)
if terr != nil {
return nil, terr
}
if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "commit_transaction")
}
return newScheme, nil
}
if !scheme.IsValid() {
return nil, store.NewErrInvalidInput("Scheme", "<any>", fmt.Sprintf("%v", scheme))
}
scheme.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Schemes
SET UpdateAt=:UpdateAt, CreateAt=:CreateAt, DeleteAt=:DeleteAt, Name=:Name, DisplayName=:DisplayName, Description=:Description, Scope=:Scope,
DefaultTeamAdminRole=:DefaultTeamAdminRole, DefaultTeamUserRole=:DefaultTeamUserRole, DefaultTeamGuestRole=:DefaultTeamGuestRole,
DefaultChannelAdminRole=:DefaultChannelAdminRole, DefaultChannelUserRole=:DefaultChannelUserRole, DefaultChannelGuestRole=:DefaultChannelGuestRole,
DefaultPlaybookMemberRole=:DefaultPlaybookMemberRole, DefaultPlaybookAdminRole=:DefaultPlaybookAdminRole, DefaultRunMemberRole=:DefaultRunMemberRole, DefaultRunAdminRole=:DefaultRunAdminRole
WHERE Id=:Id`, scheme)
if err != nil {
return nil, errors.Wrap(err, "failed to update Scheme")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, errors.New("no record to update")
}
return scheme, nil
}
func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *sqlxTxWrapper) (*model.Scheme, error) {
// Fetch the default system scheme roles to populate default permissions.
defaultRoleNames := []string{
model.TeamAdminRoleId,
model.TeamUserRoleId,
model.TeamGuestRoleId,
model.ChannelAdminRoleId,
model.ChannelUserRoleId,
model.ChannelGuestRoleId,
model.PlaybookAdminRoleId,
model.PlaybookMemberRoleId,
model.RunAdminRoleId,
model.RunMemberRoleId,
}
defaultRoles := make(map[string]*model.Role)
roles, err := s.SqlStore.Role().GetByNames(defaultRoleNames)
if err != nil {
return nil, err
}
for _, role := range roles {
defaultRoles[role.Name] = role
}
if len(defaultRoles) != len(defaultRoleNames) {
return nil, errors.New("createScheme: unable to retrieve default scheme roles")
}
// Create the appropriate default roles for the scheme.
if scheme.Scope == model.SchemeScopeTeam {
// Team Admin Role
teamAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamAdmin, scheme.Name),
Permissions: defaultRoles[model.TeamAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err := s.SqlStore.Role().(*SqlRoleStore).createRole(teamAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamAdminRole = savedRole.Name
// Team User Role
teamUserRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamUser, scheme.Name),
Permissions: defaultRoles[model.TeamUserRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(teamUserRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamUserRole = savedRole.Name
// Team Guest Role
teamGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamGuest, scheme.Name),
Permissions: defaultRoles[model.TeamGuestRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(teamGuestRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamGuestRole = savedRole.Name
// playbook admin role
playbookAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNamePlaybookAdmin, scheme.Name),
Permissions: defaultRoles[model.PlaybookAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(playbookAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultPlaybookAdminRole = savedRole.Name
// playbook member role
playbookMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNamePlaybookMember, scheme.Name),
Permissions: defaultRoles[model.PlaybookMemberRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(playbookMemberRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultPlaybookMemberRole = savedRole.Name
// run admin role
runAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameRunAdmin, scheme.Name),
Permissions: defaultRoles[model.RunAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(runAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultRunAdminRole = savedRole.Name
// run member role
runMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameRunMember, scheme.Name),
Permissions: defaultRoles[model.RunMemberRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(runMemberRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultRunMemberRole = savedRole.Name
}
if scheme.Scope == model.SchemeScopeTeam || scheme.Scope == model.SchemeScopeChannel {
// Channel Admin Role
channelAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Admin Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelAdminRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelAdminRole.Permissions = []string{}
}
savedRole, err := s.SqlStore.Role().(*SqlRoleStore).createRole(channelAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelAdminRole = savedRole.Name
// Channel User Role
channelUserRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel User Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelUserRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelUserRole.Permissions = filterModerated(channelUserRole.Permissions)
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelUserRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelUserRole = savedRole.Name
// Channel Guest Role
channelGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Guest Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelGuestRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelGuestRole.Permissions = filterModerated(channelGuestRole.Permissions)
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelGuestRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelGuestRole = savedRole.Name
}
scheme.Id = model.NewId()
if scheme.Name == "" {
scheme.Name = model.NewId()
}
scheme.CreateAt = model.GetMillis()
scheme.UpdateAt = scheme.CreateAt
// Validate the scheme
if !scheme.IsValidForCreate() {
return nil, store.NewErrInvalidInput("Scheme", "<any>", fmt.Sprintf("%v", scheme))
}
if _, err := transaction.NamedExec(`INSERT INTO Schemes
(Id, Name, DisplayName, Description, Scope, DefaultTeamAdminRole, DefaultTeamUserRole, DefaultTeamGuestRole, DefaultChannelAdminRole, DefaultChannelUserRole, DefaultChannelGuestRole, CreateAt, UpdateAt, DeleteAt, DefaultPlaybookAdminRole, DefaultPlaybookMemberRole, DefaultRunAdminRole, DefaultRunMemberRole)
VALUES
(:Id, :Name, :DisplayName, :Description, :Scope, :DefaultTeamAdminRole, :DefaultTeamUserRole, :DefaultTeamGuestRole, :DefaultChannelAdminRole, :DefaultChannelUserRole, :DefaultChannelGuestRole, :CreateAt, :UpdateAt, :DeleteAt, :DefaultPlaybookAdminRole, :DefaultPlaybookMemberRole, :DefaultRunAdminRole, :DefaultRunMemberRole)`, scheme); err != nil {
return nil, errors.Wrap(err, "failed to save Scheme")
}
return scheme, nil
}
func filterModerated(permissions []string) []string {
filteredPermissions := []string{}
for _, perm := range permissions {
if _, ok := model.ChannelModeratedPermissionsMap[perm]; ok {
filteredPermissions = append(filteredPermissions, perm)
}
}
return filteredPermissions
}
func (s *SqlSchemeStore) Get(schemeId string) (*model.Scheme, error) {
var scheme model.Scheme
if err := s.GetReplicaX().Get(&scheme, "SELECT * from Schemes WHERE Id = ?", schemeId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeId=%s", schemeId))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeId=%s", schemeId)
}
return &scheme, nil
}
func (s *SqlSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
var scheme model.Scheme
if err := s.GetReplicaX().Get(&scheme, "SELECT * from Schemes WHERE Name = ?", schemeName); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeName=%s", schemeName))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeName=%s", schemeName)
}
return &scheme, nil
}
func (s *SqlSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
// Get the scheme
scheme := model.Scheme{}
if err := s.GetMasterX().Get(&scheme, `SELECT * from Schemes WHERE Id = ?`, schemeId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeId=%s", schemeId))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeId=%s", schemeId)
}
// Update any teams or channels using this scheme to the default scheme.
if scheme.Scope == model.SchemeScopeTeam {
if _, err := s.GetMasterX().Exec(`UPDATE Teams SET SchemeId = '' WHERE SchemeId = ?`, schemeId); err != nil {
return nil, errors.Wrapf(err, "failed to update Teams with schemeId=%s", schemeId)
}
s.Team().ClearCaches()
} else if scheme.Scope == model.SchemeScopeChannel {
if _, err := s.GetMasterX().Exec(`UPDATE Channels SET SchemeId = '' WHERE SchemeId = ?`, schemeId); err != nil {
return nil, errors.Wrapf(err, "failed to update Channels with schemeId=%s", schemeId)
}
}
// Blow away the channel caches.
s.Channel().ClearCaches()
// Delete the roles belonging to the scheme.
roleNames := []string{scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole}
if scheme.Scope == model.SchemeScopeTeam {
roleNames = append(roleNames, scheme.DefaultTeamGuestRole, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole)
}
if scheme.Scope == model.SchemeScopePlaybook {
roleNames = append(roleNames, scheme.DefaultPlaybookAdminRole, scheme.DefaultPlaybookMemberRole)
}
if scheme.Scope == model.SchemeScopeRun {
roleNames = append(roleNames, scheme.DefaultRunAdminRole, scheme.DefaultRunMemberRole)
}
time := model.GetMillis()
updateQuery, args, err := s.getQueryBuilder().
Update("Roles").
Where(sq.Eq{"Name": roleNames}).
Set("UpdateAt", time).
Set("DeleteAt", time).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if _, err = s.GetMasterX().Exec(updateQuery, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update Roles with name in (%s)", roleNames)
}
// Delete the scheme itself.
scheme.UpdateAt = time
scheme.DeleteAt = time
res, err := s.GetMasterX().NamedExec(`UPDATE Schemes
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName, Description=:Description, Scope=:Scope,
DefaultTeamAdminRole=:DefaultTeamAdminRole, DefaultTeamUserRole=:DefaultTeamUserRole, DefaultTeamGuestRole=:DefaultTeamGuestRole,
DefaultChannelAdminRole=:DefaultChannelAdminRole, DefaultChannelUserRole=:DefaultChannelUserRole, DefaultChannelGuestRole=:DefaultChannelGuestRole
WHERE Id=:Id`, &scheme)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Scheme with schemeId=%s", schemeId)
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrapf(err, "failed to get RowsAffected while updating scheme with schemeId=%s", schemeId)
}
if rowsChanged != 1 {
return nil, errors.New("no record to update")
}
return &scheme, nil
}
func (s *SqlSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
schemes := []*model.Scheme{}
query := s.getQueryBuilder().
Select("*").
From("Schemes").
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset))
if scope != "" {
query = query.Where(sq.Eq{"Scope": scope})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if err := s.GetReplicaX().Select(&schemes, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Schemes")
}
return schemes, nil
}
func (s *SqlSchemeStore) PermanentDeleteAll() error {
if _, err := s.GetMasterX().Exec("DELETE from Schemes"); err != nil {
return errors.Wrap(err, "failed to delete Schemes")
}
return nil
}
func (s *SqlSchemeStore) CountByScope(scope string) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `SELECT count(*) FROM Schemes WHERE Scope = ? AND DeleteAt = 0`, scope)
if err != nil {
return 0, errors.Wrap(err, "failed to count Schemes by scope")
}
return count, nil
}
func (s *SqlSchemeStore) CountWithoutPermission(schemeScope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
joinCol := fmt.Sprintf("Default%s%sRole", roleScope, roleType)
query := fmt.Sprintf(`
SELECT
count(*)
FROM Schemes
JOIN Roles ON Roles.Name = Schemes.%s
WHERE
Schemes.DeleteAt = 0 AND
Schemes.Scope = '%s' AND
Roles.Permissions NOT LIKE '%%%s%%'
`, joinCol, schemeScope, permissionID)
var count int64
err := s.GetReplicaX().Get(&count, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count Schemes without permission")
}
return count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"encoding/json"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
sessionsCleanupDelay = 100 * time.Millisecond
)
type SqlSessionStore struct {
*SqlStore
}
func newSqlSessionStore(sqlStore *SqlStore) store.SessionStore {
return &SqlSessionStore{sqlStore}
}
func (me SqlSessionStore) Save(session *model.Session) (*model.Session, error) {
if session.Id != "" {
return nil, store.NewErrInvalidInput("Session", "id", session.Id)
}
session.PreSave()
if err := session.IsValid(); err != nil {
return nil, err
}
jsonProps, err := json.Marshal(session.Props)
if err != nil {
return nil, errors.Wrap(err, "failed marshalling session props")
}
if me.IsBinaryParamEnabled() {
jsonProps = AppendBinaryFlag(jsonProps)
}
query, args, err := me.getQueryBuilder().
Insert("Sessions").
Columns("Id", "Token", "CreateAt", "ExpiresAt", "LastActivityAt", "UserId", "DeviceId", "Roles", "IsOAuth", "ExpiredNotify", "Props").
Values(session.Id, session.Token, session.CreateAt, session.ExpiresAt, session.LastActivityAt, session.UserId, session.DeviceId, session.Roles, session.IsOAuth, session.ExpiredNotify, jsonProps).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "sessions_tosql")
}
if _, err = me.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to save Session with id=%s", session.Id)
}
teamMembers, err := me.Team().GetTeamsForUser(context.Background(), session.UserId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", session.UserId)
}
session.TeamMembers = make([]*model.TeamMember, 0, len(teamMembers))
for _, tm := range teamMembers {
if tm.DeleteAt == 0 {
session.TeamMembers = append(session.TeamMembers, tm)
}
}
return session, nil
}
func (me SqlSessionStore) Get(ctx context.Context, sessionIdOrToken string) (*model.Session, error) {
sessions := []*model.Session{}
if err := me.DBXFromContext(ctx).Select(&sessions, "SELECT * FROM Sessions WHERE Token = ? OR Id = ? LIMIT 1", sessionIdOrToken, sessionIdOrToken); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with sessionIdOrToken=%s", sessionIdOrToken)
}
if len(sessions) == 0 {
return nil, store.NewErrNotFound("Session", fmt.Sprintf("sessionIdOrToken=%s", sessionIdOrToken))
}
session := sessions[0]
tempMembers, err := me.Team().GetTeamsForUser(
WithMaster(context.Background()),
session.UserId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", session.UserId)
}
sessions[0].TeamMembers = make([]*model.TeamMember, 0, len(tempMembers))
for _, tm := range tempMembers {
if tm.DeleteAt == 0 {
sessions[0].TeamMembers = append(sessions[0].TeamMembers, tm)
}
}
return session, nil
}
func (me SqlSessionStore) GetSessions(userId string) ([]*model.Session, error) {
sessions := []*model.Session{}
if err := me.GetReplicaX().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = ? ORDER BY LastActivityAt DESC", userId); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with userId=%s", userId)
}
teamMembers, err := me.Team().GetTeamsForUser(context.Background(), userId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", userId)
}
for _, session := range sessions {
session.TeamMembers = make([]*model.TeamMember, 0, len(teamMembers))
for _, tm := range teamMembers {
if tm.DeleteAt == 0 {
session.TeamMembers = append(session.TeamMembers, tm)
}
}
}
return sessions, nil
}
func (me SqlSessionStore) GetSessionsWithActiveDeviceIds(userId string) ([]*model.Session, error) {
query :=
`SELECT *
FROM
Sessions
WHERE
UserId = ? AND
ExpiresAt != 0 AND
? <= ExpiresAt AND
DeviceId != ''`
sessions := []*model.Session{}
if err := me.GetReplicaX().Select(&sessions, query, userId, model.GetMillis()); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with userId=%s", userId)
}
return sessions, nil
}
func (me SqlSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
now := model.GetMillis()
builder := me.getQueryBuilder().
Select("*").
From("Sessions").
Where(sq.NotEq{"ExpiresAt": 0}).
Where(sq.Lt{"ExpiresAt": now}).
Where(sq.Gt{"ExpiresAt": now - thresholdMillis})
if mobileOnly {
builder = builder.Where(sq.NotEq{"DeviceId": ""})
}
if unnotifiedOnly {
builder = builder.Where(sq.NotEq{"ExpiredNotify": true})
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "sessions_tosql")
}
sessions := []*model.Session{}
err = me.GetReplicaX().Select(&sessions, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Sessions")
}
return sessions, nil
}
func (me SqlSessionStore) UpdateExpiredNotify(sessionId string, notified bool) error {
query, args, err := me.getQueryBuilder().
Update("Sessions").
Set("ExpiredNotify", notified).
Where(sq.Eq{"Id": sessionId}).
ToSql()
if err != nil {
return errors.Wrap(err, "sessions_tosql")
}
_, err = me.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to update Session with id=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) Remove(sessionIdOrToken string) error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions WHERE Id = ? Or Token = ?", sessionIdOrToken, sessionIdOrToken)
if err != nil {
return errors.Wrapf(err, "failed to delete Session with sessionIdOrToken=%s", sessionIdOrToken)
}
return nil
}
func (me SqlSessionStore) RemoveAllSessions() error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions")
if err != nil {
return errors.Wrap(err, "failed to delete all Sessions")
}
return nil
}
func (me SqlSessionStore) PermanentDeleteSessionsByUser(userId string) error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete Session with userId=%s", userId)
}
return nil
}
func (me SqlSessionStore) UpdateExpiresAt(sessionId string, time int64) error {
_, err := me.GetMasterX().Exec("UPDATE Sessions SET ExpiresAt = ?, ExpiredNotify = false WHERE Id = ?", time, sessionId)
if err != nil {
return errors.Wrapf(err, "failed to update Session with sessionId=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) UpdateLastActivityAt(sessionId string, time int64) error {
_, err := me.GetMasterX().Exec("UPDATE Sessions SET LastActivityAt = ? WHERE Id = ?", time, sessionId)
if err != nil {
return errors.Wrapf(err, "failed to update Session with id=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) UpdateRoles(userId, roles string) (string, error) {
if len(roles) > model.UserRolesMaxLength {
return "", fmt.Errorf("given session roles length (%d) exceeds max storage limit (%d)", len(roles), model.UserRolesMaxLength)
}
_, err := me.GetMasterX().Exec("UPDATE Sessions SET Roles = ? WHERE UserId = ?", roles, userId)
if err != nil {
return "", errors.Wrapf(err, "failed to update Session with userId=%s and roles=%s", userId, roles)
}
return userId, nil
}
func (me SqlSessionStore) UpdateDeviceId(id string, deviceId string, expiresAt int64) (string, error) {
query := "UPDATE Sessions SET DeviceId = ?, ExpiresAt = ?, ExpiredNotify = false WHERE Id = ?"
_, err := me.GetMasterX().Exec(query, deviceId, expiresAt, id)
if err != nil {
return "", errors.Wrapf(err, "failed to update Session with id=%s", id)
}
return deviceId, nil
}
func (me SqlSessionStore) UpdateProps(session *model.Session) error {
jsonProps, err := json.Marshal(session.Props)
if err != nil {
return errors.Wrap(err, "failed marshalling session props")
}
if me.IsBinaryParamEnabled() {
jsonProps = AppendBinaryFlag(jsonProps)
}
query, args, err := me.getQueryBuilder().
Update("Sessions").
Set("Props", jsonProps).
Where(sq.Eq{"Id": session.Id}).
ToSql()
if err != nil {
errors.Wrap(err, "sessions_tosql")
}
_, err = me.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to update Session")
}
return nil
}
func (me SqlSessionStore) AnalyticsSessionCount() (int64, error) {
var count int64
query :=
`SELECT
COUNT(*)
FROM
Sessions
WHERE ExpiresAt > ?`
if err := me.GetReplicaX().Get(&count, query, model.GetMillis()); err != nil {
return int64(0), errors.Wrap(err, "failed to count Sessions")
}
return count, nil
}
func (me SqlSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
var query string
if me.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions WHERE Id IN (SELECT Id FROM Sessions WHERE ExpiresAt != 0 AND ? > ExpiresAt LIMIT ?)"
} else {
query = "DELETE FROM Sessions WHERE ExpiresAt != 0 AND ? > ExpiresAt LIMIT ?"
}
var rowsAffected int64 = 1
for rowsAffected > 0 {
sqlResult, err := me.GetMasterX().Exec(query, expiryTime, batchSize)
if err != nil {
return errors.Wrap(err, "unable to delete sessions")
}
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete sessions")
}
time.Sleep(sessionsCleanupDelay)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
const (
DefaultGetUsersForSyncLimit = 100
)
type SqlSharedChannelStore struct {
*SqlStore
}
func newSqlSharedChannelStore(sqlStore *SqlStore) store.SharedChannelStore {
return &SqlSharedChannelStore{
SqlStore: sqlStore,
}
}
// Save inserts a new shared channel record.
func (s SqlSharedChannelStore) Save(sc *model.SharedChannel) (sh *model.SharedChannel, err error) {
sc.PreSave()
if err := sc.IsValid(); err != nil {
return nil, err
}
// make sure the shared channel is associated with a real channel.
channel, err := s.stores.channel.Get(sc.ChannelId, true)
if err != nil {
return nil, fmt.Errorf("invalid channel: %w", err)
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query, args, err := s.getQueryBuilder().Insert("SharedChannels").
Columns("ChannelId", "TeamId", "Home", "ReadOnly", "ShareName", "ShareDisplayName", "SharePurpose", "ShareHeader", "CreatorId", "CreateAt", "UpdateAt", "RemoteId").
Values(sc.ChannelId, sc.TeamId, sc.Home, sc.ReadOnly, sc.ShareName, sc.ShareDisplayName, sc.SharePurpose, sc.ShareHeader, sc.CreatorId, sc.CreateAt, sc.UpdateAt, sc.RemoteId).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchannel_tosql")
}
if _, err := transaction.Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel: ChannelId=%s", sc.ChannelId)
}
// set `Shared` flag in Channels table if needed
if channel.Shared == nil || !*channel.Shared {
if err := s.stores.channel.SetShared(channel.Id, true); err != nil {
return nil, err
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return sc, nil
}
// Get fetches a shared channel by channel_id.
func (s SqlSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
var sc model.SharedChannel
query := s.getQueryBuilder().
Select("*").
From("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannel_tosql")
}
if err := s.GetReplicaX().Get(&sc, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannel", channelId)
}
return nil, errors.Wrapf(err, "failed to find shared channel with ChannelId=%s", channelId)
}
return &sc, nil
}
// HasChannel returns whether a given channelID is a shared channel or not.
func (s SqlSharedChannelStore) HasChannel(channelID string) (bool, error) {
builder := s.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelID}).
Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "get_shared_channel_exists_tosql")
}
var exists bool
if err := s.GetReplicaX().Get(&exists, query, args...); err != nil {
return exists, errors.Wrapf(err, "failed to get shared channel for channel_id=%s", channelID)
}
return exists, nil
}
// GetAll fetches a paginated list of shared channels filtered by SharedChannelSearchOpts.
func (s SqlSharedChannelStore) GetAll(offset, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
if opts.ExcludeHome && opts.ExcludeRemote {
return nil, errors.New("cannot exclude home and remote shared channels")
}
safeConv := func(offset, limit int) (uint64, uint64, error) {
if offset < 0 {
return 0, 0, errors.New("offset must be positive integer")
}
if limit < 0 {
return 0, 0, errors.New("limit must be positive integer")
}
return uint64(offset), uint64(limit), nil
}
safeOffset, safeLimit, err := safeConv(offset, limit)
if err != nil {
return nil, err
}
query := s.getSharedChannelsQuery(opts, false)
query = query.OrderBy("sc.ShareDisplayName, sc.ShareName").Limit(safeLimit).Offset(safeOffset)
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to create query")
}
channels := []*model.SharedChannel{}
err = s.GetReplicaX().Select(&channels, squery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get shared channels")
}
return channels, nil
}
// GetAllCount returns the number of shared channels that would be fetched using SharedChannelSearchOpts.
func (s SqlSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
if opts.ExcludeHome && opts.ExcludeRemote {
return 0, errors.New("cannot exclude home and remote shared channels")
}
query := s.getSharedChannelsQuery(opts, true)
squery, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "failed to create query")
}
var count int64
err = s.GetReplicaX().Get(&count, squery, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count channels")
}
return count, nil
}
func (s SqlSharedChannelStore) getSharedChannelsQuery(opts model.SharedChannelFilterOpts, forCount bool) sq.SelectBuilder {
var selectStr string
if forCount {
selectStr = "count(sc.ChannelId)"
} else {
selectStr = "sc.*"
}
query := s.getQueryBuilder().
Select(selectStr).
From("SharedChannels AS sc")
if opts.MemberId != "" {
query = query.Join("ChannelMembers AS cm ON cm.ChannelId = sc.ChannelId").
Where(sq.Eq{"cm.UserId": opts.MemberId})
}
if opts.TeamId != "" {
query = query.Where(sq.Eq{"sc.TeamId": opts.TeamId})
}
if opts.CreatorId != "" {
query = query.Where(sq.Eq{"sc.CreatorId": opts.CreatorId})
}
if opts.ExcludeHome {
query = query.Where(sq.NotEq{"sc.Home": true})
}
if opts.ExcludeRemote {
query = query.Where(sq.Eq{"sc.Home": true})
}
return query
}
// Update updates the shared channel.
func (s SqlSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
if err := sc.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Update("SharedChannels").Set("ChannelId", sc.ChannelId).
Set("TeamId", sc.TeamId).
Set("Home", sc.Home).
Set("ReadOnly", sc.ReadOnly).
Set("ShareName", sc.ShareName).
Set("ShareDisplayName", sc.ShareDisplayName).
Set("SharePurpose", sc.SharePurpose).
Set("ShareHeader", sc.ShareHeader).
Set("CreatorId", sc.CreatorId).
Set("CreateAt", sc.CreateAt).
Set("UpdateAt", sc.UpdateAt).
Set("RemoteId", sc.RemoteId).
Where(sq.Eq{"ChannelId": sc.ChannelId}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "updatesharedchannel_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update shared channel with channelId=%s", sc.ChannelId)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count != 1 {
return nil, fmt.Errorf("expected number of shared channels to be updated is 1 but was %d", count)
}
return sc, nil
}
// Delete deletes a single shared channel plus associated SharedChannelRemotes.
// Returns true if shared channel found and deleted, false if not found.
func (s SqlSharedChannelStore) Delete(channelId string) (ok bool, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return false, errors.Wrap(err, "DeleteSharedChannel: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
squery, args, err := s.getQueryBuilder().
Delete("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_tosql")
}
result, err := transaction.Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannel")
}
// Also remove remotes from SharedChannelRemotes (if any).
squery, args, err = s.getQueryBuilder().
Delete("SharedChannelRemotes").
Where(sq.Eq{"ChannelId": channelId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_remotes_tosql")
}
_, err = transaction.Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannelRemotes")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
if count > 0 {
// unset the channel's Shared flag
if err = s.Channel().SetShared(channelId, false); err != nil {
return false, errors.Wrap(err, "error unsetting channel share flag")
}
}
if err = transaction.Commit(); err != nil {
return false, errors.Wrap(err, "commit_transaction")
}
return count > 0, nil
}
// SaveRemote inserts a new shared channel remote record.
func (s SqlSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
remote.PreSave()
if err := remote.IsValid(); err != nil {
return nil, err
}
// make sure the shared channel remote is associated with a real channel.
if _, err := s.stores.channel.Get(remote.ChannelId, true); err != nil {
return nil, fmt.Errorf("invalid channel: %w", err)
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelRemotes").
Columns("Id", "ChannelId", "CreatorId", "CreateAt", "UpdateAt", "IsInviteAccepted", "IsInviteConfirmed", "RemoteId", "LastPostUpdateAt", "LastPostId").
Values(remote.Id, remote.ChannelId, remote.CreatorId, remote.CreateAt, remote.UpdateAt, remote.IsInviteAccepted, remote.IsInviteConfirmed, remote.RemoteId, remote.LastPostUpdateAt, remote.LastPostId).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchannelremote_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_remote: channel_id=%s, id=%s", remote.ChannelId, remote.Id)
}
return remote, nil
}
// Update updates the shared channel remote.
func (s SqlSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
if err := remote.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Update("SharedChannelRemotes").
Set("CreatorId", remote.CreatorId).
Set("CreateAt", remote.CreateAt).
Set("UpdateAt", remote.UpdateAt).
Set("IsInviteAccepted", remote.IsInviteAccepted).
Set("IsInviteConfirmed", remote.IsInviteConfirmed).
Set("RemoteId", remote.RemoteId).
Set("LastPostUpdateAt", remote.LastPostUpdateAt).
Set("LastPostId", remote.LastPostId).
Where(sq.And{
sq.Eq{"Id": remote.Id},
sq.Eq{"ChannelId": remote.ChannelId},
}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "updatesharedchannelremote_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update shared channel remote with remoteId=%s", remote.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count != 1 {
return nil, fmt.Errorf("expected number of shared channel remotes to be updated is 1 but was %d", count)
}
return remote, nil
}
// GetRemote fetches a shared channel remote by id.
func (s SqlSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.Id": id})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remote_tosql")
}
if err := s.GetReplicaX().Get(&remote, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemote", id)
}
return nil, errors.Wrapf(err, "failed to find shared channel remote with id=%s", id)
}
return &remote, nil
}
// GetRemoteByIds fetches a shared channel remote by channel id and remote cluster id.
func (s SqlSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.ChannelId": channelId}).
Where(sq.Eq{"SharedChannelRemotes.RemoteId": remoteId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remote_by_ids_tosql")
}
if err := s.GetReplicaX().Get(&remote, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemote", fmt.Sprintf("channelId=%s, remoteId=%s", channelId, remoteId))
}
return nil, errors.Wrapf(err, "failed to find shared channel remote with channelId=%s, remoteId=%s", channelId, remoteId)
}
return &remote, nil
}
// GetRemotes fetches all shared channel remotes associated with channel_id.
func (s SqlSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
remotes := []*model.SharedChannelRemote{}
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes")
if opts.ChannelId != "" {
query = query.Where(sq.Eq{"ChannelId": opts.ChannelId})
}
if opts.RemoteId != "" {
query = query.Where(sq.Eq{"RemoteId": opts.RemoteId})
}
if !opts.InclUnconfirmed {
query = query.Where(sq.Eq{"IsInviteConfirmed": true})
}
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remotes_tosql")
}
if err := s.GetReplicaX().Select(&remotes, squery, args...); err != nil {
if err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get shared channel remotes for channel_id=%s; remote_id=%s",
opts.ChannelId, opts.RemoteId)
}
}
return remotes, nil
}
// HasRemote returns whether a given remoteId and channelId are present in the shared channel remotes or not.
func (s SqlSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
builder := s.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("SharedChannelRemotes").
Where(sq.Eq{"RemoteId": remoteId}).
Where(sq.Eq{"ChannelId": channelID}).
Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "get_shared_channel_hasremote_tosql")
}
var hasRemote bool
if err := s.GetReplicaX().Get(&hasRemote, query, args...); err != nil {
return hasRemote, errors.Wrapf(err, "failed to get channel remotes for channel_id=%s", channelID)
}
return hasRemote, nil
}
// GetRemoteForUser returns a remote cluster for the given userId only if the user belongs to at least one channel
// shared with the remote.
func (s SqlSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
builder := s.getQueryBuilder().
Select("rc.*").
From("RemoteClusters AS rc").
Join("SharedChannelRemotes AS scr ON rc.RemoteId = scr.RemoteId").
Join("ChannelMembers AS cm ON scr.ChannelId = cm.ChannelId").
Where(sq.Eq{"rc.RemoteId": remoteId}).
Where(sq.Eq{"cm.UserId": userId})
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_remote_for_user_tosql")
}
var rc model.RemoteCluster
if err := s.GetReplicaX().Get(&rc, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("RemoteCluster", remoteId)
}
return nil, errors.Wrapf(err, "failed to get remote for user_id=%s", userId)
}
return &rc, nil
}
// UpdateRemoteCursor updates the LastPostUpdateAt timestamp and LastPostId for the specified SharedChannelRemote.
func (s SqlSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
squery, args, err := s.getQueryBuilder().
Update("SharedChannelRemotes").
Set("LastPostUpdateAt", cursor.LastPostUpdateAt).
Set("LastPostId", cursor.LastPostId).
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "update_shared_channel_remote_cursor_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update cursor for SharedChannelRemote")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", id)
}
return nil
}
// DeleteRemote deletes a single shared channel remote.
// Returns true if remote found and deleted, false if not found.
func (s SqlSharedChannelStore) DeleteRemote(id string) (bool, error) {
squery, args, err := s.getQueryBuilder().
Delete("SharedChannelRemotes").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_remote_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannelRemote")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
return count > 0, nil
}
// GetRemotesStatus returns the status for each remote invited to the
// specified shared channel.
func (s SqlSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
status := []*model.SharedChannelRemoteStatus{}
query := s.getQueryBuilder().
Select("scr.ChannelId, rc.DisplayName, rc.SiteURL, rc.LastPingAt, sc.ReadOnly, scr.IsInviteAccepted").
From("SharedChannelRemotes scr, RemoteClusters rc, SharedChannels sc").
Where("scr.RemoteId = rc.RemoteId").
Where("scr.ChannelId = sc.ChannelId").
Where(sq.Eq{"scr.ChannelId": channelId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remotes_status_tosql")
}
if err := s.GetReplicaX().Select(&status, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemoteStatus", channelId)
}
return nil, errors.Wrapf(err, "failed to get shared channel remote status for channel_id=%s", channelId)
}
return status, nil
}
// SaveUser inserts a new shared channel user record to the SharedChannelUsers table.
func (s SqlSharedChannelStore) SaveUser(scUser *model.SharedChannelUser) (*model.SharedChannelUser, error) {
scUser.PreSave()
if err := scUser.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelUsers").
Columns("Id", "UserId", "ChannelId", "RemoteId", "CreateAt", "LastSyncAt").
Values(scUser.Id, scUser.UserId, scUser.ChannelId, scUser.RemoteId, scUser.CreateAt, scUser.LastSyncAt).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchanneluser_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_user: user_id=%s, remote_id=%s", scUser.UserId, scUser.RemoteId)
}
return scUser, nil
}
// GetSingleUser fetches a shared channel user based on userID, channelID and remoteID.
func (s SqlSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
var scu model.SharedChannelUser
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
Where(sq.Eq{"SharedChannelUsers.RemoteId": remoteID}).
Where(sq.Eq{"SharedChannelUsers.ChannelId": channelID}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelsingleuser_tosql")
}
if err := s.GetReplicaX().Get(&scu, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelUser", userID)
}
return nil, errors.Wrapf(err, "failed to find shared channel user with UserId=%s, ChannelId=%s, RemoteId=%s", userID, channelID, remoteID)
}
return &scu, nil
}
// GetUsersForUser fetches all shared channel user records based on userID.
func (s SqlSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchanneluser_tosql")
}
users := []*model.SharedChannelUser{}
if err := s.GetReplicaX().Select(&users, squery, args...); err != nil {
if err == sql.ErrNoRows {
return make([]*model.SharedChannelUser, 0), nil
}
return nil, errors.Wrapf(err, "failed to find shared channel user with UserId=%s", userID)
}
return users, nil
}
// GetUsersForSync fetches all shared channel users that need to be synchronized, meaning their
// `SharedChannelUsers.LastSyncAt` is less than or equal to `User.UpdateAt`.
func (s SqlSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
if filter.Limit <= 0 {
filter.Limit = DefaultGetUsersForSyncLimit
}
query := s.getQueryBuilder().
Select("u.*").
Distinct().
From("Users AS u").
Join("SharedChannelUsers AS scu ON u.Id = scu.UserId").
OrderBy("u.Id").
Limit(filter.Limit)
if filter.CheckProfileImage {
query = query.Where("scu.LastSyncAt < u.LastPictureUpdate")
} else {
query = query.Where("scu.LastSyncAt < u.UpdateAt")
}
if filter.ChannelID != "" {
query = query.Where(sq.Eq{"scu.ChannelId": filter.ChannelID})
}
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelusersforsync_tosql")
}
users := []*model.User{}
if err := s.GetReplicaX().Select(&users, sqlQuery, args...); err != nil {
if err == sql.ErrNoRows {
return make([]*model.User, 0), nil
}
return nil, errors.Wrapf(err, "failed to fetch shared channel users with ChannelId=%s",
filter.ChannelID)
}
return users, nil
}
// UpdateUserLastSyncAt updates the LastSyncAt timestamp for the specified SharedChannelUser.
func (s SqlSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
var query string
if s.DriverName() == model.DatabaseDriverPostgres {
query = `
UPDATE
SharedChannelUsers AS scu
SET
LastSyncAt = GREATEST(Users.UpdateAt, Users.LastPictureUpdate)
FROM
Users
WHERE
Users.Id = scu.UserId AND scu.UserId = ? AND scu.ChannelId = ? AND scu.RemoteId = ?
`
} else if s.DriverName() == model.DatabaseDriverMysql {
query = `
UPDATE
SharedChannelUsers AS scu
INNER JOIN
Users ON scu.UserId = Users.Id
SET
LastSyncAt = GREATEST(Users.UpdateAt, Users.LastPictureUpdate)
WHERE
scu.UserId = ? AND scu.ChannelId = ? AND scu.RemoteId = ?
`
} else {
return errors.New("unsupported DB driver " + s.DriverName())
}
result, err := s.GetMasterX().Exec(query, userID, channelID, remoteID)
if err != nil {
return fmt.Errorf("failed to update LastSyncAt for SharedChannelUser with userId=%s, channelId=%s, remoteId=%s: %w",
userID, channelID, remoteID, err)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("SharedChannelUser not found: userId=%s, channelId=%s, remoteId=%s", userID, channelID, remoteID)
}
return nil
}
// SaveAttachment inserts a new shared channel file attachment record to the SharedChannelFiles table.
func (s SqlSharedChannelStore) SaveAttachment(attachment *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
attachment.PreSave()
if err := attachment.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesahredchannelattachment_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_attachment: file_id=%s, remote_id=%s", attachment.FileId, attachment.RemoteId)
}
return attachment, nil
}
// UpsertAttachment inserts a new shared channel file attachment record to the SharedChannelFiles table or updates its
// LastSyncAt.
func (s SqlSharedChannelStore) UpsertAttachment(attachment *model.SharedChannelAttachment) (string, error) {
attachment.PreSave()
if err := attachment.IsValid(); err != nil {
return "", err
}
query := s.getQueryBuilder().
Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE LastSyncAt = ?", attachment.LastSyncAt))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (id) DO UPDATE SET LastSyncAt = ?", attachment.LastSyncAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return "", errors.Wrap(err, "upsertsharedchannelattachment_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return "", errors.Wrap(err, "failed to upsert SharedChannelAttachments")
}
return attachment.Id, nil
}
// GetAttachment fetches a shared channel file attachment record based on file_id and remoteId.
func (s SqlSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
var attachment model.SharedChannelAttachment
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelAttachments").
Where(sq.Eq{"SharedChannelAttachments.FileId": fileId}).
Where(sq.Eq{"SharedChannelAttachments.RemoteId": remoteId}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelattachment_tosql")
}
if err := s.GetReplicaX().Get(&attachment, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelAttachment", fileId)
}
return nil, errors.Wrapf(err, "failed to find shared channel attachment with FileId=%s, RemoteId=%s", fileId, remoteId)
}
return &attachment, nil
}
// UpdateAttachmentLastSyncAt updates the LastSyncAt timestamp for the specified SharedChannelAttachment.
func (s SqlSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
squery, args, err := s.getQueryBuilder().
Update("SharedChannelAttachments").
Set("LastSyncAt", syncTime).
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "update_shared_channel_attachment_last_sync_at_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update LastSyncAt for SharedChannelAttachment")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", id)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/jmoiron/sqlx"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type StoreTestWrapper struct {
orig *SqlStore
}
func NewStoreTestWrapper(orig *SqlStore) *StoreTestWrapper {
return &StoreTestWrapper{orig}
}
func (w *StoreTestWrapper) GetMasterX() storetest.SqlXExecutor {
return w.orig.GetMasterX()
}
func (w *StoreTestWrapper) DriverName() string {
return w.orig.DriverName()
}
type Builder interface {
ToSql() (string, []any, error)
}
// sqlxExecutor exposes sqlx operations. It is used to enable some internal store methods to
// accept both transactions (*sqlxTxWrapper) and common db handlers (*sqlxDbWrapper).
type sqlxExecutor interface {
Get(dest any, query string, args ...any) error
GetBuilder(dest any, builder Builder) error
NamedExec(query string, arg any) (sql.Result, error)
Exec(query string, args ...any) (sql.Result, error)
ExecBuilder(builder Builder) (sql.Result, error)
ExecRaw(query string, args ...any) (sql.Result, error)
NamedQuery(query string, arg any) (*sqlx.Rows, error)
QueryRowX(query string, args ...any) *sqlx.Row
QueryX(query string, args ...any) (*sqlx.Rows, error)
Select(dest any, query string, args ...any) error
SelectBuilder(dest any, builder Builder) error
}
// namedParamRegex is used to capture all named parameters and convert them
// to lowercase. This is necessary to be able to use a single query for both
// Postgres and MySQL.
// This will also lowercase any constant strings containing a :, but sqlx
// will fail the query, so it won't be checked in inadvertently.
var namedParamRegex = regexp.MustCompile(`:\w+`)
type sqlxDBWrapper struct {
*sqlx.DB
queryTimeout time.Duration
trace bool
}
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
return &sqlxDBWrapper{
DB: db,
queryTimeout: timeout,
trace: trace,
}
}
func (w *sqlxDBWrapper) Stats() sql.DBStats {
return w.DB.Stats()
}
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
tx, err := w.DB.Beginx()
if err != nil {
return nil, err
}
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
}
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
tx, err := w.DB.BeginTxx(context.Background(), opts)
if err != nil {
return nil, err
}
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
}
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.GetContext(ctx, dest, query, args...)
}
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Get(dest, query, args...)
}
func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) {
if w.DB.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.DB.NamedExecContext(ctx, query, arg)
}
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
query = w.DB.Rebind(query)
return w.ExecRaw(query, args...)
}
func (w *sqlxDBWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
query, args, err := builder.ToSql()
if err != nil {
return nil, err
}
return w.Exec(query, args...)
}
func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
query = w.DB.Rebind(query)
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.ExecContext(context.Background(), query, args...)
}
// ExecRaw is like Exec but without any rebinding of params. You need to pass
// the exact param types of your target database.
func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.ExecContext(ctx, query, args...)
}
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
if w.DB.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.DB.NamedQueryContext(ctx, query, arg)
}
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.QueryRowxContext(ctx, query, args...)
}
func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.QueryxContext(ctx, query, args)
}
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
return w.SelectCtx(context.Background(), dest, query, args...)
}
func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, args ...any) error {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(ctx, w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.SelectContext(ctx, dest, query, args...)
}
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Select(dest, query, args...)
}
type sqlxTxWrapper struct {
*sqlx.Tx
queryTimeout time.Duration
trace bool
}
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper {
return &sqlxTxWrapper{
Tx: tx,
queryTimeout: timeout,
trace: trace,
}
}
func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.GetContext(ctx, dest, query, args...)
}
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Get(dest, query, args...)
}
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
query = w.Tx.Rebind(query)
return w.ExecRaw(query, args...)
}
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
query = w.Tx.Rebind(query)
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.ExecContext(context.Background(), query, args...)
}
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
query, args, err := builder.ToSql()
if err != nil {
return nil, err
}
return w.Exec(query, args...)
}
// ExecRaw is like Exec but without any rebinding of params. You need to pass
// the exact param types of your target database.
func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.ExecContext(ctx, query, args...)
}
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
if w.Tx.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.Tx.NamedExecContext(ctx, query, arg)
}
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
if w.Tx.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
// There is no tx.NamedQueryContext support in the sqlx API. (https://github.com/jmoiron/sqlx/issues/447)
// So we need to implement this ourselves.
type result struct {
rows *sqlx.Rows
err error
}
// Need to add a buffer of 1 to prevent goroutine leak.
resChan := make(chan *result, 1)
go func() {
rows, err := w.Tx.NamedQuery(query, arg)
resChan <- &result{
rows: rows,
err: err,
}
}()
// staticcheck fails to check that res gets re-assigned later.
res := &result{} //nolint:staticcheck
select {
case res = <-resChan:
case <-ctx.Done():
res = &result{
rows: nil,
err: ctx.Err(),
}
}
return res.rows, res.err
}
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.QueryRowxContext(ctx, query, args...)
}
func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.QueryxContext(ctx, query, args)
}
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.SelectContext(ctx, dest, query, args...)
}
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Select(dest, query, args...)
}
func removeSpace(r rune) rune {
// Strip everything except ' '
// This also strips out more than one space,
// but we ignore it for now until someone complains.
if unicode.IsSpace(r) && r != ' ' {
return -1
}
return r
}
func printArgs(query string, dur time.Duration, args ...any) {
query = strings.Map(removeSpace, query)
fields := make([]mlog.Field, 0, len(args)+1)
fields = append(fields, mlog.Duration("duration", dur))
for i, arg := range args {
fields = append(fields, mlog.Any("arg"+strconv.Itoa(i), arg))
}
mlog.Debug(query, fields...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlStatusStore struct {
*SqlStore
}
func newSqlStatusStore(sqlStore *SqlStore) store.StatusStore {
return &SqlStatusStore{sqlStore}
}
func (s SqlStatusStore) SaveOrUpdate(st *model.Status) error {
query := s.getQueryBuilder().
Insert("Status").
Columns("UserId", "Status", "Manual", "LastActivityAt", "DNDEndTime", "PrevStatus").
Values(st.UserId, st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Status = ?, Manual = ?, LastActivityAt = ?, DNDEndTime = ?, PrevStatus = ?",
st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid) DO UPDATE SET Status = ?, Manual = ?, LastActivityAt = ?, DNDEndTime = ?, PrevStatus = ?",
st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "status_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to upsert Status")
}
return nil
}
func (s SqlStatusStore) Get(userId string) (*model.Status, error) {
var status model.Status
if err := s.GetReplicaX().Get(&status, "SELECT * FROM Status WHERE UserId = ?", userId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Status", fmt.Sprintf("userId=%s", userId))
}
return nil, errors.Wrapf(err, "failed to get Status with userId=%s", userId)
}
return &status, nil
}
func (s SqlStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
query := s.getQueryBuilder().
Select("UserId, Status, Manual, LastActivityAt").
From("Status").
Where(sq.Eq{"UserId": userIds})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Statuses")
}
statuses := []*model.Status{}
defer rows.Close()
for rows.Next() {
var status model.Status
if err = rows.Scan(&status.UserId, &status.Status, &status.Manual, &status.LastActivityAt); err != nil {
return nil, errors.Wrap(err, "unable to scan from rows")
}
statuses = append(statuses, &status)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "failed while iterating over rows")
}
return statuses, nil
}
// MySQL doesn't have support for RETURNING clause, so we use a transaction to get the updated rows.
func (s SqlStatusStore) updateExpiredStatuses(t *sqlxTxWrapper) ([]*model.Status, error) {
statuses := []*model.Status{}
currUnixTime := time.Now().UTC().Unix()
selectQuery, selectParams, err := s.getQueryBuilder().
Select("*").
From("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": currUnixTime},
},
).ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
err = t.Select(&statuses, selectQuery, selectParams...)
if err != nil {
return nil, errors.Wrap(err, "updateExpiredStatusesT: failed to get expired dnd statuses")
}
updateQuery, args, err := s.getQueryBuilder().
Update("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": currUnixTime},
},
).
Set("Status", sq.Expr("PrevStatus")).
Set("PrevStatus", model.StatusDnd).
Set("DNDEndTime", 0).
Set("Manual", false).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if _, err := t.Exec(updateQuery, args...); err != nil {
return nil, errors.Wrapf(err, "updateExpiredStatusesT: failed to update statuses")
}
return statuses, nil
}
func (s SqlStatusStore) UpdateExpiredDNDStatuses() (_ []*model.Status, err error) {
if s.DriverName() == model.DatabaseDriverMysql {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
statuses, terr := s.updateExpiredStatuses(transaction)
if terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: updateExpiredDNDStatusesT")
}
if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: commit_transaction")
}
for _, status := range statuses {
status.Status = status.PrevStatus
status.PrevStatus = model.StatusDnd
status.DNDEndTime = 0
status.Manual = false
}
return statuses, nil
}
queryString, args, err := s.getQueryBuilder().
Update("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": time.Now().UTC().Unix()},
},
).
Set("Status", sq.Expr("PrevStatus")).
Set("PrevStatus", model.StatusDnd).
Set("DNDEndTime", 0).
Set("Manual", false).
Suffix("RETURNING *").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
rows, err := s.GetMasterX().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Statuses")
}
defer rows.Close()
statuses := []*model.Status{}
for rows.Next() {
var status model.Status
if err = rows.Scan(&status.UserId, &status.Status, &status.Manual, &status.LastActivityAt,
&status.DNDEndTime, &status.PrevStatus); err != nil {
return nil, errors.Wrap(err, "unable to scan from rows")
}
statuses = append(statuses, &status)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "failed while iterating over rows")
}
return statuses, nil
}
func (s SqlStatusStore) ResetAll() error {
if _, err := s.GetMasterX().Exec("UPDATE Status SET Status = ? WHERE Manual = false", model.StatusOffline); err != nil {
return errors.Wrap(err, "failed to update Statuses")
}
return nil
}
func (s SqlStatusStore) GetTotalActiveUsersCount() (int64, error) {
time := model.GetMillis() - (1000 * 60 * 60 * 24)
var count int64
err := s.GetReplicaX().Get(&count, "SELECT COUNT(UserId) FROM Status WHERE LastActivityAt > ?", time)
if err != nil {
return count, errors.Wrap(err, "failed to count active users")
}
return count, nil
}
func (s SqlStatusStore) UpdateLastActivityAt(userId string, lastActivityAt int64) error {
if _, err := s.GetMasterX().Exec("UPDATE Status SET LastActivityAt = ? WHERE UserId = ?", lastActivityAt, userId); err != nil {
return errors.Wrapf(err, "failed to update last activity for userId=%s", userId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
dbsql "database/sql"
"fmt"
"log"
"path"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/morph"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/morph/drivers"
ms "github.com/mattermost/morph/drivers/mysql"
ps "github.com/mattermost/morph/drivers/postgres"
"github.com/go-sql-driver/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
mbindata "github.com/mattermost/morph/sources/embedded"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/db"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type migrationDirection string
const (
IndexTypeFullText = "full_text"
IndexTypeFullTextFunc = "full_text_func"
IndexTypeDefault = "default"
PGDupTableErrorCode = "42P07" // see https://github.com/lib/pq/blob/master/error.go#L268
MySQLDupTableErrorCode = uint16(1050) // see https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html#error_er_table_exists_error
PGForeignKeyViolationErrorCode = "23503"
MySQLForeignKeyViolationErrorCode = 1452
PGDuplicateObjectErrorCode = "42710"
MySQLDuplicateObjectErrorCode = 1022
DBPingAttempts = 18
DBPingTimeoutSecs = 10
// This is a numerical version string by postgres. The format is
// 2 characters for major, minor, and patch version prior to 10.
// After 10, it's major and minor only.
// 10.1 would be 100001.
// 9.6.3 would be 90603.
minimumRequiredPostgresVersion = 100000
// major*1000 + minor*100 + patch
minimumRequiredMySQLVersion = 5712
migrationsDirectionUp migrationDirection = "up"
migrationsDirectionDown migrationDirection = "down"
replicaLagPrefix = "replica-lag"
RemoteClusterSiteURLUniqueIndex = "remote_clusters_site_url_unique"
)
var tablesToCheckForCollation = []string{"incomingwebhooks", "preferences", "users", "uploadsessions", "channels", "publicchannels"}
type SqlStoreStores struct {
team store.TeamStore
channel store.ChannelStore
post store.PostStore
retentionPolicy store.RetentionPolicyStore
thread store.ThreadStore
user store.UserStore
bot store.BotStore
audit store.AuditStore
cluster store.ClusterDiscoveryStore
remoteCluster store.RemoteClusterStore
compliance store.ComplianceStore
session store.SessionStore
oauth store.OAuthStore
system store.SystemStore
webhook store.WebhookStore
command store.CommandStore
commandWebhook store.CommandWebhookStore
preference store.PreferenceStore
license store.LicenseStore
token store.TokenStore
emoji store.EmojiStore
status store.StatusStore
fileInfo store.FileInfoStore
uploadSession store.UploadSessionStore
reaction store.ReactionStore
job store.JobStore
userAccessToken store.UserAccessTokenStore
plugin store.PluginStore
channelMemberHistory store.ChannelMemberHistoryStore
role store.RoleStore
scheme store.SchemeStore
TermsOfService store.TermsOfServiceStore
productNotices store.ProductNoticesStore
group store.GroupStore
UserTermsOfService store.UserTermsOfServiceStore
linkMetadata store.LinkMetadataStore
sharedchannel store.SharedChannelStore
draft store.DraftStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
trueUpReview store.TrueUpReviewStore
}
type SqlStore struct {
// rrCounter and srCounter should be kept first.
// See https://github.com/mattermost/mattermost-server/v6/server/channels/pull/7281
rrCounter int64
srCounter int64
masterX *sqlxDBWrapper
ReplicaXs []*sqlxDBWrapper
searchReplicaXs []*sqlxDBWrapper
replicaLagHandles []*dbsql.DB
stores SqlStoreStores
settings *model.SqlSettings
lockedToMaster bool
context context.Context
license *model.License
licenseMutex sync.RWMutex
metrics einterfaces.MetricsInterface
isBinaryParam bool
pgDefaultTextSearchConfig string
}
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
store := &SqlStore{
rrCounter: 0,
srCounter: 0,
settings: &settings,
metrics: metrics,
}
store.initConnection()
ver, err := store.GetDbVersion(true)
if err != nil {
mlog.Fatal("Error while getting DB version.", mlog.Err(err))
}
ok, err := store.ensureMinimumDBVersion(ver)
if !ok {
mlog.Fatal("Error while checking DB version.", mlog.Err(err))
}
err = store.ensureDatabaseCollation()
if err != nil {
mlog.Fatal("Error while checking DB collation.", mlog.Err(err))
}
err = store.migrate(migrationsDirectionUp)
if err != nil {
mlog.Fatal("Failed to apply database migrations.", mlog.Err(err))
}
store.isBinaryParam, err = store.computeBinaryParam()
if err != nil {
mlog.Fatal("Failed to compute binary param", mlog.Err(err))
}
store.pgDefaultTextSearchConfig, err = store.computeDefaultTextSearchConfig()
if err != nil {
mlog.Fatal("Failed to compute default text search config", mlog.Err(err))
}
store.stores.team = newSqlTeamStore(store)
store.stores.channel = newSqlChannelStore(store, metrics)
store.stores.post = newSqlPostStore(store, metrics)
store.stores.retentionPolicy = newSqlRetentionPolicyStore(store, metrics)
store.stores.user = newSqlUserStore(store, metrics)
store.stores.bot = newSqlBotStore(store, metrics)
store.stores.audit = newSqlAuditStore(store)
store.stores.cluster = newSqlClusterDiscoveryStore(store)
store.stores.remoteCluster = newSqlRemoteClusterStore(store)
store.stores.compliance = newSqlComplianceStore(store)
store.stores.session = newSqlSessionStore(store)
store.stores.oauth = newSqlOAuthStore(store)
store.stores.system = newSqlSystemStore(store)
store.stores.webhook = newSqlWebhookStore(store, metrics)
store.stores.command = newSqlCommandStore(store)
store.stores.commandWebhook = newSqlCommandWebhookStore(store)
store.stores.preference = newSqlPreferenceStore(store)
store.stores.license = newSqlLicenseStore(store)
store.stores.token = newSqlTokenStore(store)
store.stores.emoji = newSqlEmojiStore(store, metrics)
store.stores.status = newSqlStatusStore(store)
store.stores.fileInfo = newSqlFileInfoStore(store, metrics)
store.stores.uploadSession = newSqlUploadSessionStore(store)
store.stores.thread = newSqlThreadStore(store)
store.stores.job = newSqlJobStore(store)
store.stores.userAccessToken = newSqlUserAccessTokenStore(store)
store.stores.channelMemberHistory = newSqlChannelMemberHistoryStore(store)
store.stores.plugin = newSqlPluginStore(store)
store.stores.TermsOfService = newSqlTermsOfServiceStore(store, metrics)
store.stores.UserTermsOfService = newSqlUserTermsOfServiceStore(store)
store.stores.linkMetadata = newSqlLinkMetadataStore(store)
store.stores.sharedchannel = newSqlSharedChannelStore(store)
store.stores.reaction = newSqlReactionStore(store)
store.stores.role = newSqlRoleStore(store)
store.stores.scheme = newSqlSchemeStore(store)
store.stores.group = newSqlGroupStore(store)
store.stores.productNotices = newSqlProductNoticesStore(store)
store.stores.draft = newSqlDraftStore(store, metrics)
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
return store
}
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
// It also applies any database configuration settings that are required.
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB {
db, err := dbsql.Open(*settings.DriverName, dataSource)
if err != nil {
mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err))
}
for i := 0; i < DBPingAttempts; i++ {
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", dataSource))
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err == nil {
break
} else {
if i == DBPingAttempts-1 {
mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err))
} else {
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
time.Sleep(DBPingTimeoutSecs * time.Second)
}
}
}
if strings.HasPrefix(connType, replicaLagPrefix) {
// If this is a replica lag connection, we just open one connection.
//
// Arguably, if the query doesn't require a special credential, it does take up
// one extra connection from the replica DB. But falling back to the replica
// data source when the replica lag data source is null implies an ordering constraint
// which makes things brittle and is not a good design.
// If connections are an overhead, it is advised to use a connection pool.
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
} else {
db.SetMaxIdleConns(*settings.MaxIdleConns)
db.SetMaxOpenConns(*settings.MaxOpenConns)
}
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
return db
}
func (ss *SqlStore) SetContext(context context.Context) {
ss.context = context
}
func (ss *SqlStore) Context() context.Context {
return ss.context
}
func noOpMapper(s string) string { return s }
func (ss *SqlStore) initConnection() {
dataSource := *ss.settings.DataSource
if ss.DriverName() == model.DatabaseDriverMysql {
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
// covers that already. Ideally we'd like to do this only for the upgrade
// step. To be reviewed in MM-35789.
var err error
dataSource, err = ResetReadTimeout(dataSource)
if err != nil {
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource))
}
}
handle := SetupConnection("master", dataSource, ss.settings)
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.masterX.MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.masterX.DB.DB, "master")
}
if len(ss.settings.DataSourceReplicas) > 0 {
ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas))
for i, replica := range ss.settings.DataSourceReplicas {
handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings)
ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.ReplicaXs[i].MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i))
}
}
}
if len(ss.settings.DataSourceSearchReplicas) > 0 {
ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas))
for i, replica := range ss.settings.DataSourceSearchReplicas {
handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings)
ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.searchReplicaXs[i].MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i))
}
}
}
if len(ss.settings.ReplicaLagSettings) > 0 {
ss.replicaLagHandles = make([]*dbsql.DB, len(ss.settings.ReplicaLagSettings))
for i, src := range ss.settings.ReplicaLagSettings {
if src.DataSource == nil {
continue
}
ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings)
}
}
}
func (ss *SqlStore) DriverName() string {
return *ss.settings.DriverName
}
// specialSearchChars have special meaning and can be treated as spaces
func (ss *SqlStore) specialSearchChars() []string {
chars := []string{
"<",
">",
"+",
"-",
"(",
")",
"~",
":",
}
// Postgres can handle "@" without any errors
// Also helps postgres in enabling search for EmailAddresses
if ss.DriverName() != model.DatabaseDriverPostgres {
chars = append(chars, "@")
}
return chars
}
// computeBinaryParam returns whether the data source uses binary_parameters
// when using Postgres
func (ss *SqlStore) computeBinaryParam() (bool, error) {
if ss.DriverName() != model.DatabaseDriverPostgres {
return false, nil
}
return DSNHasBinaryParam(*ss.settings.DataSource)
}
func (ss *SqlStore) computeDefaultTextSearchConfig() (string, error) {
if ss.DriverName() != model.DatabaseDriverPostgres {
return "", nil
}
var defaultTextSearchConfig string
err := ss.GetMasterX().Get(&defaultTextSearchConfig, `SHOW default_text_search_config`)
return defaultTextSearchConfig, err
}
func (ss *SqlStore) IsBinaryParamEnabled() bool {
return ss.isBinaryParam
}
// GetDbVersion returns the version of the database being used.
// If numerical is set to true, it attempts to return a numerical version string
// that can be parsed by callers.
func (ss *SqlStore) GetDbVersion(numerical bool) (string, error) {
var sqlVersion string
if ss.DriverName() == model.DatabaseDriverPostgres {
if numerical {
sqlVersion = `SHOW server_version_num`
} else {
sqlVersion = `SHOW server_version`
}
} else if ss.DriverName() == model.DatabaseDriverMysql {
sqlVersion = `SELECT version()`
} else {
return "", errors.New("Not supported driver")
}
var version string
err := ss.GetReplicaX().Get(&version, sqlVersion)
if err != nil {
return "", err
}
return version, nil
}
func (ss *SqlStore) GetMasterX() *sqlxDBWrapper {
return ss.masterX
}
func (ss *SqlStore) SetMasterX(db *sql.DB) {
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(db, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.masterX.MapperFunc(noOpMapper)
}
}
func (ss *SqlStore) GetInternalMasterDB() *sql.DB {
return ss.GetMasterX().DB.DB
}
func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper {
if !ss.hasLicense() {
return ss.GetMasterX()
}
if len(ss.settings.DataSourceSearchReplicas) == 0 {
return ss.GetReplicaX()
}
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
return ss.searchReplicaXs[rrNum]
}
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return ss.GetMasterX()
}
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
return ss.ReplicaXs[rrNum]
}
func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return []*sql.DB{
ss.GetMasterX().DB.DB,
}
}
dbs := make([]*sql.DB, len(ss.ReplicaXs))
for i, rx := range ss.ReplicaXs {
dbs[i] = rx.DB.DB
}
return dbs
}
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return ss.GetMasterX().DB.DB
}
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
return ss.ReplicaXs[rrNum].DB.DB
}
func (ss *SqlStore) TotalMasterDbConnections() int {
return ss.GetMasterX().Stats().OpenConnections
}
// ReplicaLagAbs queries all the replica databases to get the absolute replica lag value
// and updates the Prometheus metric with it.
func (ss *SqlStore) ReplicaLagAbs() error {
for i, item := range ss.settings.ReplicaLagSettings {
if item.QueryAbsoluteLag == nil || *item.QueryAbsoluteLag == "" {
continue
}
var binDiff float64
var node string
err := ss.replicaLagHandles[i].QueryRow(*item.QueryAbsoluteLag).Scan(&node, &binDiff)
if err != nil {
return err
}
// There is no nil check needed here because it's called from the metrics store.
ss.metrics.SetReplicaLagAbsolute(node, binDiff)
}
return nil
}
// ReplicaLagAbs queries all the replica databases to get the time-based replica lag value
// and updates the Prometheus metric with it.
func (ss *SqlStore) ReplicaLagTime() error {
for i, item := range ss.settings.ReplicaLagSettings {
if item.QueryTimeLag == nil || *item.QueryTimeLag == "" {
continue
}
var timeDiff float64
var node string
err := ss.replicaLagHandles[i].QueryRow(*item.QueryTimeLag).Scan(&node, &timeDiff)
if err != nil {
return err
}
// There is no nil check needed here because it's called from the metrics store.
ss.metrics.SetReplicaLagTime(node, timeDiff)
}
return nil
}
func (ss *SqlStore) TotalReadDbConnections() int {
if len(ss.settings.DataSourceReplicas) == 0 {
return 0
}
count := 0
for _, db := range ss.ReplicaXs {
count = count + db.Stats().OpenConnections
}
return count
}
func (ss *SqlStore) TotalSearchDbConnections() int {
if len(ss.settings.DataSourceSearchReplicas) == 0 {
return 0
}
count := 0
for _, db := range ss.searchReplicaXs {
count = count + db.Stats().OpenConnections
}
return count
}
func (ss *SqlStore) MarkSystemRanUnitTests() {
props, err := ss.System().Get()
if err != nil {
return
}
unitTests := props[model.SystemRanUnitTests]
if unitTests == "" {
systemTests := &model.System{Name: model.SystemRanUnitTests, Value: "1"}
ss.System().Save(systemTests)
}
}
func (ss *SqlStore) DoesTableExist(tableName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT count(relname) FROM pg_class WHERE relname=$1`,
strings.ToLower(tableName),
)
if err != nil {
mlog.Fatal("Failed to check if table exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT
COUNT(0) AS table_exists
FROM
information_schema.TABLES
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
`,
tableName,
)
if err != nil {
mlog.Fatal("Failed to check if table exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) DoesColumnExist(tableName string, columnName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT COUNT(0)
FROM pg_attribute
WHERE attrelid = $1::regclass
AND attname = $2
AND NOT attisdropped`,
strings.ToLower(tableName),
strings.ToLower(columnName),
)
if err != nil {
if err.Error() == "pq: relation \""+strings.ToLower(tableName)+"\" does not exist" {
return false
}
mlog.Fatal("Failed to check if column exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT
COUNT(0) AS column_exists
FROM
information_schema.COLUMNS
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?`,
tableName,
columnName,
)
if err != nil {
mlog.Fatal("Failed to check if column exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) DoesTriggerExist(triggerName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count, `
SELECT
COUNT(0)
FROM
pg_trigger
WHERE
tgname = $1
`, triggerName)
if err != nil {
mlog.Fatal("Failed to check if trigger exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count, `
SELECT
COUNT(0)
FROM
information_schema.triggers
WHERE
trigger_schema = DATABASE()
AND trigger_name = ?
`, triggerName)
if err != nil {
mlog.Fatal("Failed to check if trigger exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) CreateColumnIfNotExists(tableName string, columnName string, mySqlColType string, postgresColType string, defaultValue string) bool {
if ss.DoesColumnExist(tableName, columnName) {
return false
}
if ss.DriverName() == model.DatabaseDriverPostgres {
_, err := ss.GetMasterX().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
mlog.Fatal("Failed to create column", mlog.Err(err))
}
return true
} else if ss.DriverName() == model.DatabaseDriverMysql {
_, err := ss.GetMasterX().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
mlog.Fatal("Failed to create column", mlog.Err(err))
}
return true
} else {
mlog.Fatal("Failed to create column because of missing driver")
return false
}
}
func (ss *SqlStore) RemoveTableIfExists(tableName string) bool {
if !ss.DoesTableExist(tableName) {
return false
}
_, err := ss.GetMasterX().ExecNoTimeout("DROP TABLE " + tableName)
if err != nil {
mlog.Fatal("Failed to drop table", mlog.Err(err))
}
return true
}
func IsConstraintAlreadyExistsError(err error) bool {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGDuplicateObjectErrorCode {
return true
}
case *mysql.MySQLError:
if dbErr.Number == MySQLDuplicateObjectErrorCode {
return true
}
}
return false
}
func IsUniqueConstraintError(err error, indexName []string) bool {
unique := false
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" {
unique = true
}
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1062 {
unique = true
}
field := false
for _, contain := range indexName {
if strings.Contains(err.Error(), contain) {
field = true
break
}
}
return unique && field
}
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1)
copy(all, ss.ReplicaXs)
all[len(ss.ReplicaXs)] = ss.masterX
return all
}
// RecycleDBConnections closes active connections by setting the max conn lifetime
// to d, and then resets them back to their original duration.
func (ss *SqlStore) RecycleDBConnections(d time.Duration) {
// Get old time.
originalDuration := time.Duration(*ss.settings.ConnMaxLifetimeMilliseconds) * time.Millisecond
// Set the max lifetimes for all connections.
for _, conn := range ss.GetAllConns() {
conn.SetConnMaxLifetime(d)
}
// Wait for that period with an additional 2 seconds of scheduling delay.
time.Sleep(d + 2*time.Second)
// Reset max lifetime back to original value.
for _, conn := range ss.GetAllConns() {
conn.SetConnMaxLifetime(originalDuration)
}
}
func (ss *SqlStore) Close() {
ss.masterX.Close()
for _, replica := range ss.ReplicaXs {
replica.Close()
}
for _, replica := range ss.searchReplicaXs {
replica.Close()
}
}
func (ss *SqlStore) LockToMaster() {
ss.lockedToMaster = true
}
func (ss *SqlStore) UnlockFromMaster() {
ss.lockedToMaster = false
}
func (ss *SqlStore) Team() store.TeamStore {
return ss.stores.team
}
func (ss *SqlStore) Channel() store.ChannelStore {
return ss.stores.channel
}
func (ss *SqlStore) Post() store.PostStore {
return ss.stores.post
}
func (ss *SqlStore) RetentionPolicy() store.RetentionPolicyStore {
return ss.stores.retentionPolicy
}
func (ss *SqlStore) User() store.UserStore {
return ss.stores.user
}
func (ss *SqlStore) Bot() store.BotStore {
return ss.stores.bot
}
func (ss *SqlStore) Session() store.SessionStore {
return ss.stores.session
}
func (ss *SqlStore) Audit() store.AuditStore {
return ss.stores.audit
}
func (ss *SqlStore) ClusterDiscovery() store.ClusterDiscoveryStore {
return ss.stores.cluster
}
func (ss *SqlStore) RemoteCluster() store.RemoteClusterStore {
return ss.stores.remoteCluster
}
func (ss *SqlStore) Compliance() store.ComplianceStore {
return ss.stores.compliance
}
func (ss *SqlStore) OAuth() store.OAuthStore {
return ss.stores.oauth
}
func (ss *SqlStore) System() store.SystemStore {
return ss.stores.system
}
func (ss *SqlStore) Webhook() store.WebhookStore {
return ss.stores.webhook
}
func (ss *SqlStore) Command() store.CommandStore {
return ss.stores.command
}
func (ss *SqlStore) CommandWebhook() store.CommandWebhookStore {
return ss.stores.commandWebhook
}
func (ss *SqlStore) Preference() store.PreferenceStore {
return ss.stores.preference
}
func (ss *SqlStore) License() store.LicenseStore {
return ss.stores.license
}
func (ss *SqlStore) Token() store.TokenStore {
return ss.stores.token
}
func (ss *SqlStore) Emoji() store.EmojiStore {
return ss.stores.emoji
}
func (ss *SqlStore) Status() store.StatusStore {
return ss.stores.status
}
func (ss *SqlStore) FileInfo() store.FileInfoStore {
return ss.stores.fileInfo
}
func (ss *SqlStore) UploadSession() store.UploadSessionStore {
return ss.stores.uploadSession
}
func (ss *SqlStore) Reaction() store.ReactionStore {
return ss.stores.reaction
}
func (ss *SqlStore) Job() store.JobStore {
return ss.stores.job
}
func (ss *SqlStore) UserAccessToken() store.UserAccessTokenStore {
return ss.stores.userAccessToken
}
func (ss *SqlStore) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return ss.stores.channelMemberHistory
}
func (ss *SqlStore) Plugin() store.PluginStore {
return ss.stores.plugin
}
func (ss *SqlStore) Thread() store.ThreadStore {
return ss.stores.thread
}
func (ss *SqlStore) Role() store.RoleStore {
return ss.stores.role
}
func (ss *SqlStore) TermsOfService() store.TermsOfServiceStore {
return ss.stores.TermsOfService
}
func (ss *SqlStore) ProductNotices() store.ProductNoticesStore {
return ss.stores.productNotices
}
func (ss *SqlStore) UserTermsOfService() store.UserTermsOfServiceStore {
return ss.stores.UserTermsOfService
}
func (ss *SqlStore) Scheme() store.SchemeStore {
return ss.stores.scheme
}
func (ss *SqlStore) Group() store.GroupStore {
return ss.stores.group
}
func (ss *SqlStore) LinkMetadata() store.LinkMetadataStore {
return ss.stores.linkMetadata
}
func (ss *SqlStore) NotifyAdmin() store.NotifyAdminStore {
return ss.stores.notifyAdmin
}
func (ss *SqlStore) SharedChannel() store.SharedChannelStore {
return ss.stores.sharedchannel
}
func (ss *SqlStore) PostPriority() store.PostPriorityStore {
return ss.stores.postPriority
}
func (ss *SqlStore) Draft() store.DraftStore {
return ss.stores.draft
}
func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}
func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore {
return ss.stores.trueUpReview
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO
$func$
BEGIN
EXECUTE
(SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE'
FROM pg_class
WHERE relkind = 'r' -- only tables
AND relnamespace = 'public'::regnamespace
AND NOT relname = 'db_migrations'
);
END
$func$;`)
} else {
tables := []string{}
ss.masterX.Select(&tables, `show tables`)
for _, t := range tables {
if t != "db_migrations" {
ss.masterX.Exec(`TRUNCATE TABLE ` + t)
}
}
}
}
func (ss *SqlStore) getQueryBuilder() sq.StatementBuilderType {
return sq.StatementBuilder.PlaceholderFormat(ss.getQueryPlaceholder())
}
func (ss *SqlStore) getQueryPlaceholder() sq.PlaceholderFormat {
if ss.DriverName() == model.DatabaseDriverPostgres {
return sq.Dollar
}
return sq.Question
}
// getSubQueryBuilder is necessary to generate the SQL query and args to pass to sub-queries because squirrel does not support WHERE clause in sub-queries.
func (ss *SqlStore) getSubQueryBuilder() sq.StatementBuilderType {
return sq.StatementBuilder.PlaceholderFormat(sq.Question)
}
func (ss *SqlStore) CheckIntegrity() <-chan model.IntegrityCheckResult {
results := make(chan model.IntegrityCheckResult)
go CheckRelationalIntegrity(ss, results)
return results
}
func (ss *SqlStore) UpdateLicense(license *model.License) {
ss.licenseMutex.Lock()
defer ss.licenseMutex.Unlock()
ss.license = license
}
func (ss *SqlStore) GetLicense() *model.License {
return ss.license
}
func (ss *SqlStore) hasLicense() bool {
ss.licenseMutex.Lock()
hasLicense := ss.license != nil
ss.licenseMutex.Unlock()
return hasLicense
}
func (ss *SqlStore) migrate(direction migrationDirection) error {
assets := db.Assets()
assetsList, err := assets.ReadDir(path.Join("migrations", ss.DriverName()))
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNamesForDriver[i] = entry.Name()
}
src, err := mbindata.WithInstance(&mbindata.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(path.Join("migrations", ss.DriverName(), name))
},
})
if err != nil {
return err
}
var driver drivers.Driver
switch ss.DriverName() {
case model.DatabaseDriverMysql:
dataSource, rErr := ResetReadTimeout(*ss.settings.DataSource)
if rErr != nil {
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(rErr), mlog.String("src", *ss.settings.DataSource))
return rErr
}
dataSource, err = AppendMultipleStatementsFlag(dataSource)
if err != nil {
return err
}
db := SetupConnection("master", dataSource, ss.settings)
driver, err = ms.WithInstance(db)
defer db.Close()
case model.DatabaseDriverPostgres:
driver, err = ps.WithInstance(ss.GetMasterX().DB.DB)
default:
err = fmt.Errorf("unsupported database type %s for migration", ss.DriverName())
}
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLogger(log.New(&morphWriter{}, "", log.Lshortfile)),
morph.WithLock("mm-lock-key"),
morph.SetStatementTimeoutInSeconds(*ss.settings.MigrationsStatementTimeoutSeconds),
}
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer engine.Close()
switch direction {
case migrationsDirectionDown:
_, err = engine.ApplyDown(-1)
return err
default:
return engine.ApplyAll()
}
}
func convertMySQLFullTextColumnsToPostgres(columnNames string) string {
columns := strings.Split(columnNames, ", ")
concatenatedColumnNames := ""
for i, c := range columns {
concatenatedColumnNames += c
if i < len(columns)-1 {
concatenatedColumnNames += " || ' ' || "
}
}
return concatenatedColumnNames
}
// IsDuplicate checks whether an error is a duplicate key error, which comes when processes are competing on creating the same
// tables in the database.
func IsDuplicate(err error) bool {
var pqErr *pq.Error
var mysqlErr *mysql.MySQLError
switch {
case errors.As(errors.Cause(err), &pqErr):
if pqErr.Code == PGDupTableErrorCode {
return true
}
case errors.As(errors.Cause(err), &mysqlErr):
if mysqlErr.Number == MySQLDupTableErrorCode {
return true
}
}
return false
}
// ensureMinimumDBVersion gets the DB version and ensures it is
// above the required minimum version requirements.
func (ss *SqlStore) ensureMinimumDBVersion(ver string) (bool, error) {
switch *ss.settings.DriverName {
case model.DatabaseDriverPostgres:
intVer, err2 := strconv.Atoi(ver)
if err2 != nil {
return false, fmt.Errorf("cannot parse DB version: %v", err2)
}
if intVer < minimumRequiredPostgresVersion {
return false, fmt.Errorf("minimum Postgres version requirements not met. Found: %s, Wanted: %s", versionString(intVer, *ss.settings.DriverName), versionString(minimumRequiredPostgresVersion, *ss.settings.DriverName))
}
case model.DatabaseDriverMysql:
// Usually a version string is of the form 5.6.49-log, 10.4.5-MariaDB etc.
if strings.Contains(strings.ToLower(ver), "maria") {
mlog.Warn("MariaDB detected. You are using an unsupported database. Please consider using MySQL or Postgres.")
return true, nil
}
parts := strings.Split(ver, "-")
if len(parts) < 1 {
return false, fmt.Errorf("cannot parse MySQL DB version: %s", ver)
}
// Get the major and minor versions.
versions := strings.Split(parts[0], ".")
if len(versions) < 3 {
return false, fmt.Errorf("cannot parse MySQL DB version: %s", ver)
}
majorVer, err2 := strconv.Atoi(versions[0])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
minorVer, err2 := strconv.Atoi(versions[1])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
patchVer, err2 := strconv.Atoi(versions[2])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
intVer := majorVer*1000 + minorVer*100 + patchVer
if intVer < minimumRequiredMySQLVersion {
return false, fmt.Errorf("minimum MySQL version requirements not met. Found: %s, Wanted: %s", versionString(intVer, *ss.settings.DriverName), versionString(minimumRequiredMySQLVersion, *ss.settings.DriverName))
}
}
return true, nil
}
func (ss *SqlStore) ensureDatabaseCollation() error {
if *ss.settings.DriverName != model.DatabaseDriverMysql {
return nil
}
var connCollation struct {
Variable_name string
Value string
}
if err := ss.GetMasterX().Get(&connCollation, "SHOW VARIABLES LIKE 'collation_connection'"); err != nil {
return errors.Wrap(err, "unable to select variables")
}
// we compare table collation with the connection collation value so that we can
// catch collation mismatches for tables we have a migration for.
for _, tableName := range tablesToCheckForCollation {
// we check if table exists because this code runs before the migrations applied
// which means if there is a fresh db, we may fail on selecting the table_collation
var exists int
if err := ss.GetMasterX().Get(&exists, "SELECT count(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND LOWER(table_name) = ?", tableName); err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to check if table exists for collation check: %q", tableName))
} else if exists == 0 {
continue
}
var tableCollation string
if err := ss.GetMasterX().Get(&tableCollation, "SELECT table_collation FROM information_schema.tables WHERE table_schema = DATABASE() AND LOWER(table_name) = ?", tableName); err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to get table collation: %q", tableName))
}
if tableCollation != connCollation.Value {
mlog.Warn("Table collation mismatch", mlog.String("table_name", tableName), mlog.String("connection_collation", connCollation.Value), mlog.String("table_collation", tableCollation))
}
}
return nil
}
// versionString converts an integer representation of a DB version
// to a pretty-printed string.
// Postgres doesn't follow three-part version numbers from 10.0 onwards:
// https://www.postgresql.org/docs/13/libpq-status.html#LIBPQ-PQSERVERVERSION.
// For MySQL, we consider a major*1000 + minor*100 + patch format.
func versionString(v int, driver string) string {
switch driver {
case model.DatabaseDriverPostgres:
minor := v % 10000
major := v / 10000
return strconv.Itoa(major) + "." + strconv.Itoa(minor)
case model.DatabaseDriverMysql:
minor := v % 1000
major := v / 1000
patch := minor % 100
minor = minor / 100
return strconv.Itoa(major) + "." + strconv.Itoa(minor) + "." + strconv.Itoa(patch)
}
return ""
}
func (ss *SqlStore) toReserveCase(str string) string {
if ss.DriverName() == model.DatabaseDriverPostgres {
return fmt.Sprintf("%q", str)
}
return fmt.Sprintf("`%s`", strings.Title(str))
}
func (ss *SqlStore) GetDBSchemaVersion() (int, error) {
var version int
if err := ss.GetMasterX().Get(&version, "SELECT Version FROM db_migrations ORDER BY Version DESC LIMIT 1"); err != nil {
return 0, errors.Wrap(err, "unable to select from db_migrations")
}
return version, nil
}
func (ss *SqlStore) GetAppliedMigrations() ([]model.AppliedMigration, error) {
migrations := []model.AppliedMigration{}
if err := ss.GetMasterX().Select(&migrations, "SELECT Version, Name FROM db_migrations ORDER BY Version DESC"); err != nil {
return nil, errors.Wrap(err, "unable to select from db_migrations")
}
return migrations, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strconv"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type SqlSystemStore struct {
*SqlStore
}
func newSqlSystemStore(sqlStore *SqlStore) store.SystemStore {
return &SqlSystemStore{sqlStore}
}
func (s SqlSystemStore) Save(system *model.System) error {
query := "INSERT INTO Systems (Name, Value) VALUES (:Name, :Value)"
if _, err := s.GetMasterX().NamedExec(query, system); err != nil {
return errors.Wrapf(err, "failed to save system property with name=%s", system.Name)
}
return nil
}
func (s SqlSystemStore) SaveOrUpdate(system *model.System) error {
query := s.getQueryBuilder().
Insert("Systems").
Columns("Name", "Value").
Values(system.Name, system.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", system.Value))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (name) DO UPDATE SET Value = ?", system.Value))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "system_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to upsert system property")
}
return nil
}
func (s SqlSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
if err := s.SaveOrUpdate(system); err != nil {
return err
}
if strings.HasPrefix(system.Name, model.WarnMetricStatusStorePrefix) &&
(system.Value == model.WarnMetricStatusRunonce || system.Value == model.WarnMetricStatusLimitReached) {
if err := s.SaveOrUpdate(&model.System{
Name: model.SystemWarnMetricLastRunTimestampKey,
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
}); err != nil {
return errors.Wrapf(err, "failed to save system property with name=%s", model.SystemWarnMetricLastRunTimestampKey)
}
}
return nil
}
func (s SqlSystemStore) Update(system *model.System) error {
query := "UPDATE Systems SET Value=:Value WHERE Name=:Name"
if _, err := s.GetMasterX().NamedExec(query, system); err != nil {
return errors.Wrapf(err, "failed to update system property with name=%s", system.Name)
}
return nil
}
func (s SqlSystemStore) Get() (model.StringMap, error) {
systems := []model.System{}
props := make(model.StringMap)
if err := s.GetReplicaX().Select(&systems, "SELECT * FROM Systems"); err != nil {
return nil, errors.Wrap(err, "failed to get System list")
}
for _, prop := range systems {
props[prop.Name] = prop.Value
}
return props, nil
}
func (s SqlSystemStore) GetByName(name string) (*model.System, error) {
var system model.System
if err := s.GetMasterX().Get(&system, "SELECT * FROM Systems WHERE Name = ?", name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("System", fmt.Sprintf("name=%s", system.Name))
}
return nil, errors.Wrapf(err, "failed to get system property with name=%s", system.Name)
}
return &system, nil
}
func (s SqlSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
var system model.System
if _, err := s.GetMasterX().Exec("DELETE FROM Systems WHERE Name = ?", name); err != nil {
return nil, errors.Wrapf(err, "failed to permanent delete system property with name=%s", system.Name)
}
return &system, nil
}
// InsertIfExists inserts a given system value if it does not already exist. If a value
// already exists, it returns the old one, else returns the new one.
func (s SqlSystemStore) InsertIfExists(system *model.System) (_ *model.System, err error) {
tx, err := s.GetMasterX().BeginXWithIsolation(&sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
var origSystem model.System
if err := tx.Get(&origSystem, `SELECT * FROM Systems
WHERE Name = ?`, system.Name); err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get system property with name=%s", system.Name)
}
if origSystem.Value != "" {
// Already a value exists, return that.
return &origSystem, nil
}
// Key does not exist, need to insert.
if _, err := tx.NamedExec("INSERT INTO Systems (Name, Value) VALUES (:Name, :Value)", system); err != nil {
return nil, errors.Wrapf(err, "failed to save system property with name=%s", system.Name)
}
if err := tx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return system, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
TeamMemberExistsError = "store.sql_team.save_member.exists.app_error"
)
type SqlTeamStore struct {
*SqlStore
teamsQuery sq.SelectBuilder
}
type teamMember struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
CreateAt int64
}
func NewTeamMemberFromModel(tm *model.TeamMember) *teamMember {
return &teamMember{
TeamId: tm.TeamId,
UserId: tm.UserId,
Roles: tm.ExplicitRoles,
DeleteAt: tm.DeleteAt,
SchemeGuest: sql.NullBool{Valid: true, Bool: tm.SchemeGuest},
SchemeUser: sql.NullBool{Valid: true, Bool: tm.SchemeUser},
SchemeAdmin: sql.NullBool{Valid: true, Bool: tm.SchemeAdmin},
CreateAt: tm.CreateAt,
}
}
type teamMemberWithSchemeRoles struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
CreateAt int64
}
type teamMemberWithSchemeRolesList []teamMemberWithSchemeRoles
func teamMemberSliceColumns() []string {
return []string{"TeamId", "UserId", "Roles", "DeleteAt", "SchemeUser", "SchemeAdmin", "SchemeGuest", "CreateAt"}
}
func teamMemberToSlice(member *model.TeamMember) []any {
resultSlice := []any{}
resultSlice = append(resultSlice, member.TeamId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.DeleteAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
resultSlice = append(resultSlice, member.CreateAt)
return resultSlice
}
func wildcardSearchTerm(term string) string {
return strings.ToLower("%" + term + "%")
}
type rolesInfo struct {
roles []string
explicitRoles []string
schemeGuest bool
schemeUser bool
schemeAdmin bool
}
func getTeamRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole string, roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.TeamGuestRoleId:
result.schemeGuest = true
case model.TeamUserRoleId:
result.schemeUser = true
case model.TeamAdminRoleId:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamGuestRoleId)
}
}
if result.schemeUser {
if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamUserRoleId)
}
}
if result.schemeAdmin {
if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db teamMemberWithSchemeRoles) ToModel() *model.TeamMember {
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
rolesResult := getTeamRoles(schemeGuest, schemeUser, schemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(db.Roles))
tm := &model.TeamMember{
TeamId: db.TeamId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
DeleteAt: db.DeleteAt,
SchemeGuest: rolesResult.schemeGuest,
SchemeUser: rolesResult.schemeUser,
SchemeAdmin: rolesResult.schemeAdmin,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
CreateAt: db.CreateAt,
}
return tm
}
func (db teamMemberWithSchemeRolesList) ToModel() []*model.TeamMember {
tms := make([]*model.TeamMember, 0)
for _, tm := range db {
tms = append(tms, tm.ToModel())
}
return tms
}
func newSqlTeamStore(sqlStore *SqlStore) store.TeamStore {
s := &SqlTeamStore{
SqlStore: sqlStore,
}
s.teamsQuery = s.getQueryBuilder().
Select("Teams.*").
From("Teams")
return s
}
// Save adds the team to the database if a team with the same name does not already
// exist in the database. It returns the team added if the operation is successful.
func (s SqlTeamStore) Save(team *model.Team) (*model.Team, error) {
if team.Id != "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.PreSave()
if err := team.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Teams
(Id, CreateAt, UpdateAt, DeleteAt, DisplayName, Name, Description, Email, Type, CompanyName, AllowedDomains,
InviteId, AllowOpenInvite, LastTeamIconUpdate, SchemeId, GroupConstrained, CloudLimitsArchived)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :DisplayName, :Name, :Description, :Email, :Type, :CompanyName, :AllowedDomains,
:InviteId, :AllowOpenInvite, :LastTeamIconUpdate, :SchemeId, :GroupConstrained, :CloudLimitsArchived)`, team); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "teams_name_key"}) {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
return nil, errors.Wrapf(err, "failed to save Team with id=%s", team.Id)
}
return team, nil
}
// Update updates the details of the team passed as the parameter using the team Id
// if the team exists in the database.
// It returns the updated team if the operation is successful.
func (s SqlTeamStore) Update(team *model.Team) (*model.Team, error) {
team.PreUpdate()
if err := team.IsValid(); err != nil {
return nil, err
}
oldTeam := model.Team{}
err := s.GetMasterX().Get(&oldTeam, `SELECT * FROM Teams WHERE Id=?`, team.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get Team with id=%s", team.Id)
}
if oldTeam.Id == "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.CreateAt = oldTeam.CreateAt
team.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Teams
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, DisplayName=:DisplayName, Name=:Name,
Description=:Description, Email=:Email, Type=:Type, CompanyName=:CompanyName, AllowedDomains=:AllowedDomains,
InviteId=:InviteId, AllowOpenInvite=:AllowOpenInvite, LastTeamIconUpdate=:LastTeamIconUpdate,
SchemeId=:SchemeId, GroupConstrained=:GroupConstrained, CloudLimitsArchived=:CloudLimitsArchived
WHERE Id=:Id`, team)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Team with id=%s", team.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to get rows_affected")
}
if count > 1 {
return nil, errors.Wrapf(err, "multiple Teams updated with id=%s", team.Id)
}
return team, nil
}
// Get returns from the database the team that matches the id provided as parameter.
// If the team doesn't exist it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) Get(id string) (*model.Team, error) {
team := model.Team{}
if err := s.GetReplicaX().Get(&team, `SELECT * FROM Teams WHERE Id=?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", id)
}
return nil, errors.Wrapf(err, "failed to get Team with id=%s", id)
}
if team.Id == "" {
return nil, store.NewErrNotFound("Team", id)
}
return &team, nil
}
func (s SqlTeamStore) GetMany(ids []string) ([]*model.Team, error) {
query := s.getQueryBuilder().
Select("*").
From("Teams").
Where(sq.Eq{"Id": ids})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getmany_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get teams with ids %v", ids)
}
if len(teams) == 0 {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("ids=%v", ids))
}
return teams, nil
}
// GetByInviteId returns from the database the team that matches the inviteId provided as parameter.
// If the parameter provided is empty or if there is no match in the database, it returns a model.AppError
// with a http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByInviteId(inviteId string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"InviteId": inviteId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
if inviteId == "" || team.InviteId != inviteId {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
return &team, nil
}
func (s SqlTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
teams := []*model.Team{}
err := s.GetReplicaX().Select(&teams, "SELECT * FROM Teams WHERE InviteId = ''")
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams with empty InviteID")
}
return teams, nil
}
// GetByName returns from the database the team that matches the name provided as parameter.
// If there is no match in the database, it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByName(name string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": name}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to find Team with name=%s", name)
}
return &team, nil
}
func (s SqlTeamStore) GetByNames(names []string) ([]*model.Team, error) {
uniqueNames := utils.RemoveDuplicatesFromStringArray(names)
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": uniqueNames}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return nil, errors.Wrap(err, "failed to find Teams")
}
if len(teams) != len(uniqueNames) {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return teams, nil
}
func (s SqlTeamStore) teamSearchQuery(opts *model.TeamSearch, countQuery bool) sq.SelectBuilder {
var selectStr string
if countQuery {
selectStr = "count(*)"
} else {
selectStr = "t.*"
if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectStr += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Teams as t")
// Don't order or limit if getting count
if !countQuery {
query = query.OrderBy("t.DisplayName")
if opts.IsPaginated() {
query = query.Limit(uint64(*opts.PerPage)).Offset(uint64(*opts.Page * *opts.PerPage))
}
}
term := opts.Term
if term != "" {
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(Name %[1]s ? OR DisplayName %[1]s ?)", operatorKeyword), term, term)
}
if opts.PolicyID != nil && *opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": *opts.PolicyID})
} else if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where("RetentionPoliciesTeams.TeamId IS NULL")
} else if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId")
}
var teamFilters sq.Sqlizer
var openInviteFilter sq.Sqlizer
if opts.AllowOpenInvite != nil {
if *opts.AllowOpenInvite {
openInviteFilter = sq.Eq{"AllowOpenInvite": true}
} else {
openInviteFilter = sq.And{
sq.Or{
sq.NotEq{"AllowOpenInvite": true},
sq.Eq{"AllowOpenInvite": nil},
},
sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
},
}
}
teamFilters = openInviteFilter
}
var groupConstrainedFilter sq.Sqlizer
if opts.GroupConstrained != nil {
if *opts.GroupConstrained {
groupConstrainedFilter = sq.Eq{"GroupConstrained": true}
} else {
groupConstrainedFilter = sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
}
}
if teamFilters == nil {
teamFilters = groupConstrainedFilter
} else {
teamFilters = sq.Or{teamFilters, groupConstrainedFilter}
}
}
if opts.TeamType != nil {
teamTypeFilter := sq.Eq{"Type": *opts.TeamType}
teamFilters = sq.And{teamFilters, teamTypeFilter}
}
query = query.Where(teamFilters)
return query
}
// SearchAll returns from the database a list of teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
return teams, nil
}
// SearchAllPaged returns a teams list and the total count of teams that matched the search.
func (s SqlTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
teams := []*model.Team{}
var totalCount int64
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return nil, 0, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
queryString, args, err = s.teamSearchQuery(opts, true).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&totalCount, queryString, args...)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count Teams with term=%s", opts.Term)
}
return teams, totalCount, nil
}
// SearchOpen returns from the database a list of public teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(true)
return s.SearchAll(opts)
}
// SearchPrivate returns from the database a list of private teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(false)
return s.SearchAll(opts)
}
// GetAll returns all teams
func (s SqlTeamStore) GetAll() ([]*model.Team, error) {
teams := []*model.Team{}
query, args, err := s.teamsQuery.OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPage returns teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
selectString := "Teams.*"
if opts != nil && opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectString += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
builder := s.getQueryBuilder().
Select(selectString).
From("Teams").
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
if opts != nil {
if (opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained) ||
(opts.IncludePolicyID != nil && *opts.IncludePolicyID) {
builder = builder.LeftJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId")
}
if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
builder = builder.Where("RetentionPoliciesTeams.TeamId IS NULL")
}
if opts.AllowOpenInvite != nil {
builder = builder.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetTeamsByUserId returns from the database all teams that userId belongs to.
func (s SqlTeamStore) GetTeamsByUserId(userId string) ([]*model.Team, error) {
teams := []*model.Team{}
query, args, err := s.teamsQuery.
Join("TeamMembers ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPrivateTeamListing returns all private teams.
func (s SqlTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": false}).
OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.Team{}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetAllTeamListing returns all public teams.
func (s SqlTeamStore) GetAllTeamListing() ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.Team{}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// PermanentDelete permanently deletes from the database the team entry that matches the teamId passed as parameter.
// To soft-delete the team you can Update it with the DeleteAt field set to the current millisecond using model.GetMillis()
func (s SqlTeamStore) PermanentDelete(teamId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Teams").
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Team with id=%s", teamId)
}
return nil
}
// AnalyticsTeamCount returns the total number of teams.
func (s SqlTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
query := s.getQueryBuilder().Select("COUNT(*) FROM Teams")
if opts == nil || (opts.IncludeDeleted != nil && !*opts.IncludeDeleted) {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts != nil && opts.AllowOpenInvite != nil {
query = query.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Teams")
}
return c, nil
}
func (s SqlTeamStore) getTeamMembersWithSchemeSelectQuery() sq.SelectBuilder {
return s.getQueryBuilder().
Select(
"TeamMembers.*",
"TeamScheme.DefaultTeamGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultTeamUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultTeamAdminRole TeamSchemeDefaultAdminRole",
).
From("TeamMembers").
LeftJoin("Teams ON TeamMembers.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id")
}
func (s SqlTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
newTeamMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
newTeamMembers[member.TeamId] = 0
}
for _, member := range members {
newTeamMembers[member.TeamId]++
users[member.UserId] = true
if err := member.IsValid(); err != nil {
return nil, err
}
}
teams := []string{}
for team := range newTeamMembers {
teams = append(teams, team)
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
queryRoles := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlRolesQuery, argsRoles, err := queryRoles.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, sqlRolesQuery, argsRoles...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
if maxUsersPerTeam >= 0 {
queryCount := s.getQueryBuilder().
Select(
"COUNT(0) as Count, TeamMembers.TeamId as TeamId",
).
From("TeamMembers").
Join("Users ON TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teams}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Where(sq.Eq{"Users.DeleteAt": 0}).
GroupBy("TeamMembers.TeamId")
sqlCountQuery, argsCount, errCount := queryCount.ToSql()
if errCount != nil {
return nil, errors.Wrap(err, "member_count_tosql")
}
counters := []struct {
Count int
TeamId string
}{}
err = s.GetMasterX().Select(&counters, sqlCountQuery, argsCount...)
if err != nil {
return nil, errors.Wrap(err, "failed to count users in the teams of the memberships")
}
for teamId, newMembers := range newTeamMembers {
existingMembers := 0
for _, counter := range counters {
if counter.TeamId == teamId {
existingMembers = counter.Count
}
}
if existingMembers+newMembers > maxUsersPerTeam {
return nil, store.NewErrLimitExceeded("TeamMember", existingMembers+newMembers, "team members limit exceeded")
}
}
}
query := s.getQueryBuilder().Insert("TeamMembers").Columns(teamMemberSliceColumns()...)
for _, member := range members {
query = query.Values(teamMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "insert_members_to_sql")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"TeamId", "teammembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("TeamMember", err, "")
}
return nil, errors.Wrap(err, "unable_to_save_team_member")
}
newMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
members, err := s.SaveMultipleMembers([]*model.TeamMember{member}, maxUsersPerTeam)
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
teams := []string{}
for _, member := range members {
member.PreUpdate()
newTeamMember := NewTeamMemberFromModel(member)
if err := member.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`UPDATE TeamMembers
SET Roles=:Roles, DeleteAt=:DeleteAt, CreateAt=:CreateAt, SchemeGuest=:SchemeGuest,
SchemeUser=:SchemeUser, SchemeAdmin=:SchemeAdmin
WHERE TeamId=:TeamId AND UserId=:UserId`, newTeamMember); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
teams = append(teams, member.TeamId)
}
query := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, sqlQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
updatedMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
updatedMember := *member
updatedMember.SchemeGuest = rolesResult.schemeGuest
updatedMember.SchemeUser = rolesResult.schemeUser
updatedMember.SchemeAdmin = rolesResult.schemeAdmin
updatedMember.Roles = strings.Join(rolesResult.roles, " ")
updatedMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
updatedMembers = append(updatedMembers, &updatedMember)
}
return updatedMembers, nil
}
func (s SqlTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
members, err := s.UpdateMultipleMembers([]*model.TeamMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
// GetMember returns a single member of the team that matches the teamId and userId provided as parameters.
func (s SqlTeamStore) GetMember(ctx context.Context, teamId string, userId string) (*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userId})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var dbMember teamMemberWithSchemeRoles
err = s.DBXFromContext(ctx).Get(&dbMember, queryString, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TeamMember", fmt.Sprintf("teamId=%s, userId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s and userId=%s", teamId, userId)
}
return dbMember.ToModel(), nil
}
// GetMembers returns a list of members from the database that matches the teamId passed as parameter and,
// also expects teamMembersGetOptions to be passed as a parameter which allows to further filter what to show in the result.
// TeamMembersGetOptions Model has following options->
// 1. Sort through USERNAME [ if provided, which otherwise defaults to ID ]
// 2. Sort through USERNAME [ if provided, which otherwise defaults to ID ] and exclude deleted members.
// 3. Return all the members but, exclude deleted ones.
// 4. Apply ViewUsersRestrictions to restrict what is visible to the user.
func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Limit(uint64(limit)).
Offset(uint64(offset))
if teamMembersGetOptions == nil || teamMembersGetOptions.Sort == "" {
query = query.OrderBy("UserId")
}
if teamMembersGetOptions != nil {
if teamMembersGetOptions.Sort == model.USERNAME || teamMembersGetOptions.ExcludeDeletedUsers {
query = query.LeftJoin("Users ON TeamMembers.UserId = Users.Id")
}
if teamMembersGetOptions.ExcludeDeletedUsers {
query = query.Where(sq.Eq{"Users.DeleteAt": 0})
}
if teamMembersGetOptions.Sort == model.USERNAME {
query = query.OrderBy(model.USERNAME)
}
query = applyTeamMemberViewRestrictionsFilter(query, teamMembersGetOptions.ViewRestrictions)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s", teamId)
}
return dbMembers.ToModel(), nil
}
// GetTotalMemberCount returns the number of all members in a team for the teamId passed as a parameter.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetTotalMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetActiveMemberCount returns the number of active members in a team for the teamId passed as a parameter i.e. members with 'DeleteAt = 0'
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetActiveMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where("Users.DeleteAt = 0").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetMembersByIds returns a list of members from the database that matches the teamId and the list of userIds passed as parameters.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
if len(userIds) == 0 {
return nil, errors.New("invalid list of user ids")
}
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userIds}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0})
query = applyTeamMemberViewRestrictionsFilter(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
if err = s.GetReplicaX().Select(&dbMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUser returns a list of teams that the user is a member of. Expects userId to be passed as a parameter. It can also negative the teamID passed.
func (s SqlTeamStore) GetTeamsForUser(ctx context.Context, userId, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId})
if excludeTeamID != "" {
query = query.Where(sq.NotEq{"TeamMembers.TeamId": excludeTeamID})
}
if !includeDeleted {
query = query.Where(sq.Eq{"TeamMembers.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.SqlStore.DBXFromContext(ctx).Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUserWithPagination returns limited TeamMembers according to the perPage parameter specified.
// It also offsets the records as per the page parameter supplied.
func (s SqlTeamStore) GetTeamsForUserWithPagination(userId string, page, perPage int) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId}).
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetChannelUnreadsForAllTeams returns unreads msg count, mention counts, and notifyProps
// for all the channels in all the teams except the excluded ones.
func (s SqlTeamStore) GetChannelUnreadsForAllTeams(excludeTeamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "DeleteAt": 0}).
Where(sq.NotEq{"TeamId": excludeTeamId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.ChannelUnread{}
err = s.GetReplicaX().Select(&data, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with userId=%s and teamId!=%s", userId, excludeTeamId)
}
return data, nil
}
// GetChannelUnreadsForTeam returns unreads msg count, mention counts and notifyProps for all the channels in a single team.
func (s SqlTeamStore) GetChannelUnreadsForTeam(teamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "TeamId": teamId, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
channels := []*model.ChannelUnread{}
err = s.GetReplicaX().Select(&channels, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlTeamStore) RemoveMembers(teamId string, userIds []string) error {
builder := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).
Where(sq.Eq{"UserId": userIds})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s and userId in %v", teamId, userIds)
}
return nil
}
// RemoveMember remove from the database the team members that match the userId and teamId passed as parameter.
func (s SqlTeamStore) RemoveMember(teamId string, userId string) error {
return s.RemoveMembers(teamId, []string{userId})
}
// RemoveAllMembersByTeam removes from the database the team members that belong to the teamId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByTeam(teamId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s", teamId)
}
return nil
}
// RemoveAllMembersByUser removes from the database the team members that match the userId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByUser(userId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with userId=%s", userId)
}
return nil
}
// UpdateLastTeamIconUpdate sets the last updated time for the icon based on the parameter passed in teamId. The
// LastTeamIconUpdate and UpdateAt fields are set to the parameter passed in curTime. Returns nil on success and an error
// otherwise.
func (s SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) error {
query, args, err := s.getQueryBuilder().
Update("Teams").
SetMap(sq.Eq{"LastTeamIconUpdate": curTime, "UpdateAt": curTime}).
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to update Team")
}
return nil
}
// GetTeamsByScheme returns from the database all teams that match the schemeId provided as parameter, up to
// a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"SchemeId": schemeId}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with schemeId=%s", schemeId)
}
return teams, nil
}
// MigrateTeamMembers performs the Advanced Permissions Phase 2 migration for TeamMember objects. Migration is done
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) (_ map[string]string, err error) {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamMembers := []teamMember{}
if err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (?, ?) ORDER BY TeamId, UserId LIMIT 100", fromTeamId, fromUserId); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
// No more team members in query result means that the migration has finished.
return nil, nil
}
for i := range teamMembers {
member := teamMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.TeamAdminRoleId {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamUserRoleId {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamGuestRoleId {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.NamedExec(`UPDATE TeamMembers
SET TeamId=:TeamId,
UserId=:UserId,
Roles=:Roles,
DeleteAt=:DeleteAt,
SchemeUser=:SchemeUser,
SchemeAdmin=:SchemeAdmin,
SchemeGuest=:SchemeGuest
WHERE TeamId=:TeamId AND UserId=:UserId`, &member); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
data := make(map[string]string)
data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId
data["UserId"] = teamMembers[len(teamMembers)-1].UserId
return data, nil
}
// ResetAllTeamSchemes Set all Team's SchemeId values to an empty string.
func (s SqlTeamStore) ResetAllTeamSchemes() error {
if _, err := s.GetMasterX().Exec("UPDATE Teams SET SchemeId=''"); err != nil {
return errors.Wrap(err, "failed to update Teams")
}
return nil
}
// ClearCaches method not implemented.
func (s SqlTeamStore) ClearCaches() {}
// InvalidateAllTeamIdsForUser does not execute anything because the store does not handle the cache.
//
//nolint:unparam
func (s SqlTeamStore) InvalidateAllTeamIdsForUser(userId string) {}
// ClearAllCustomRoleAssignments removes all custom role assignments from TeamMembers.
func (s SqlTeamStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastTeamId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
var err error
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamMembers := []*teamMember{}
if err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (?, ?) ORDER BY TeamId, UserId LIMIT 1000", lastTeamId, lastUserId); err != nil {
return errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
break
}
for _, member := range teamMembers {
lastUserId = member.UserId
lastTeamId = member.TeamId
var newRoles []string
for _, role := range strings.Fields(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err := transaction.Exec("UPDATE TeamMembers SET Roles = ? WHERE UserId = ? AND TeamId = ?", newRolesString, member.UserId, member.TeamId); err != nil {
return errors.Wrap(err, "failed to update TeamMembers")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
// AnalyticsGetTeamCountForScheme returns the number of active teams that match the schemeId passed as parameter.
func (s SqlTeamStore) AnalyticsGetTeamCountForScheme(schemeId string) (int64, error) {
query, args, err := s.getQueryBuilder().
Select("count(*)").
From("Teams").
Where(sq.Eq{"SchemeId": schemeId, "DeleteAt": 0}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Teams with schemeId=%s", schemeId)
}
return count, nil
}
// GetAllForExportAfter returns teams for export, up to a total limit passed as parameter where Teams.Id is greater than the afterId passed as parameter.
func (s SqlTeamStore) GetAllForExportAfter(limit int, afterId string) ([]*model.TeamForExport, error) {
data := []*model.TeamForExport{}
query, args, err := s.getQueryBuilder().
Select("Teams.*", "Schemes.Name as SchemeName").
From("Teams").
LeftJoin("Schemes ON Teams.SchemeId = Schemes.Id").
Where(sq.Gt{"Teams.Id": afterId}).
OrderBy("Id").
Limit(uint64(limit)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetUserTeamIds get the team ids to which the user belongs to. allowFromCache parameter does not have any effect in this Store
//
//nolint:unparam
func (s SqlTeamStore) GetUserTeamIds(userId string, allowFromCache bool) ([]string, error) {
teamIds := []string{}
query, args, err := s.getQueryBuilder().
Select("TeamId").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return []string{}, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teamIds, query, args...)
if err != nil {
return []string{}, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return teamIds, nil
}
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
func (s SqlTeamStore) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error) {
var teamIDs []string
query, args, err := s.getQueryBuilder().
Select("TM1.TeamId").
From("TeamMembers AS TM1").
InnerJoin("TeamMembers AS TM2 ON TM1.TeamId = TM2.TeamId").
InnerJoin("Teams ON TM1.TeamId = Teams.Id").
Where(sq.And{
sq.Eq{"TM1.UserId": userID},
sq.Eq{"TM1.DeleteAt": 0},
sq.Eq{"TM2.UserId": otherUserID},
sq.Eq{"TM2.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teamIDs, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with user IDs %s and %s", userID, otherUserID)
}
return teamIDs, nil
}
// GetTeamMembersForExport gets the various teams for which a user, denoted by userId, is a part of.
func (s SqlTeamStore) GetTeamMembersForExport(userId string) ([]*model.TeamMemberForExport, error) {
members := []*model.TeamMemberForExport{}
query, args, err := s.getQueryBuilder().
Select("TeamMembers.TeamId", "TeamMembers.UserId", "TeamMembers.Roles", "TeamMembers.DeleteAt",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) as SchemeGuest",
"TeamMembers.SchemeUser", "TeamMembers.SchemeAdmin", "Teams.Name as TeamName").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&members, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return members, nil
}
// UserBelongsToTeams returns true if the user denoted by userId is a member of the teams in the teamIds string array.
func (s SqlTeamStore) UserBelongsToTeams(userId string, teamIds []string) (bool, error) {
idQuery := sq.Eq{
"UserId": userId,
"TeamId": teamIds,
"DeleteAt": 0,
}
query, params, err := s.getQueryBuilder().Select("Count(*)").From("TeamMembers").Where(idQuery).ToSql()
if err != nil {
return false, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, query, params...)
if err != nil {
return false, errors.Wrap(err, "failed to count TeamMembers")
}
return c > 0, nil
}
// UpdateMembersRole updates all the members of teamID in the userIds string array to be admins and sets all other
// users as not being admin.
func (s SqlTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
query, args, err := s.getQueryBuilder().
Update("TeamMembers").
Set("SchemeAdmin", sq.Case().When(sq.Eq{"UserId": userIDs}, "true").Else("false")).
Where(sq.Eq{"TeamId": teamID, "DeleteAt": 0}).
Where(sq.Or{sq.Eq{"SchemeGuest": false}, sq.Expr("SchemeGuest IS NULL")}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to update TeamMembers")
}
return nil
}
func applyTeamMemberViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query.Join("Users ru ON (TeamMembers.UserId = ru.Id)")
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = ru.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = ru.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery.Distinct()
}
func applyTeamMemberViewRestrictionsFilterForStats(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = Users.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = Users.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery
}
// GroupSyncedTeamCount returns the number of teams that are group constrained.
func (s SqlTeamStore) GroupSyncedTeamCount() (int64, error) {
builder := s.getQueryBuilder().Select("COUNT(*)").From("Teams").Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Teams")
}
return count, nil
}
func (s SqlTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
builderF := func(selectClause string) sq.SelectBuilder {
return s.getQueryBuilder().
Select(selectClause).
From("TeamMembers").
Join("Users ON Users.id = TeamMembers.userid").
LeftJoin("Bots ON Bots.userid = Users.id").
Where(sq.GtOrEq{"TeamMembers.createat": since}).
Where(sq.Eq{"TeamMembers.deleteat": 0, "teamid": teamID, "Users.deleteat": 0, "Bots.userid": nil})
}
countBuilder := builderF("count(*)")
query, args, err := countBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var totalCount int64
err = s.GetReplicaX().Get(&totalCount, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to count team members since")
}
newTeamMembersBuilder := builderF("Users.Id, Users.Username, Users.FirstName, Users.LastName, Users.Position, Users.LastPictureUpdate, TeamMembers.CreateAt, Users.Nickname").
Limit(uint64(limit + 1)).
Offset(uint64(offset))
query, args, err = newTeamMembersBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var ntms []*model.NewTeamMember
err = s.GetReplicaX().Select(&ntms, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get team members since")
}
return model.GetNewTeamMembersListWithPagination(ntms, limit), totalCount, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlTermsOfServiceStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlTermsOfServiceStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.TermsOfServiceStore {
return SqlTermsOfServiceStore{sqlStore, metrics}
}
func (s SqlTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
if termsOfService.Id != "" {
return nil, store.NewErrInvalidInput("TermsOfService", "Id", termsOfService.Id)
}
termsOfService.PreSave()
if err := termsOfService.IsValid(); err != nil {
return nil, err
}
query := `INSERT INTO TermsOfService
(Id, CreateAt, UserId, Text)
VALUES
(:Id, :CreateAt, :UserId, :Text)
`
if _, err := s.GetMasterX().NamedExec(query, termsOfService); err != nil {
return nil, errors.Wrapf(err, "could not save a new TermsOfService")
}
return termsOfService, nil
}
func (s SqlTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
var termsOfService model.TermsOfService
query := s.getQueryBuilder().
Select("*").
From("TermsOfService").
OrderBy("CreateAt DESC").
Limit(uint64(1))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get latest TOS")
}
if err := s.GetReplicaX().Get(&termsOfService, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TermsOfService", "CreateAt=latest")
}
return nil, errors.Wrap(err, "could not find latest TermsOfService")
}
return &termsOfService, nil
}
func (s SqlTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
var termsOfService model.TermsOfService
queryString, _, err := s.getQueryBuilder().
Select("*").
From("TermsOfService").
Where("id = ?").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "terms_of_service_to_sql")
}
err = s.GetReplicaX().Get(&termsOfService, queryString, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TermsOfService", "id")
}
return nil, errors.Wrapf(err, "could not find TermsOfService with id=%s", id)
}
return &termsOfService, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"strconv"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// JoinedThread allows querying the Threads + Posts table in a single query, before looking up
// users and unpacking into a model.ThreadResponse.
type JoinedThread struct {
PostId string
ReplyCount int64
LastReplyAt int64
LastViewedAt int64
UnreadReplies int64
UnreadMentions int64
Participants model.StringArray
ThreadDeleteAt int64
TeamId string
IsUrgent bool
model.Post
}
func (thread *JoinedThread) toThreadResponse(users map[string]*model.User) *model.ThreadResponse {
threadParticipants := make([]*model.User, 0, len(thread.Participants))
for _, participantUserId := range thread.Participants {
if participant, ok := users[participantUserId]; ok {
threadParticipants = append(threadParticipants, participant)
}
}
return &model.ThreadResponse{
PostId: thread.PostId,
ReplyCount: thread.ReplyCount,
LastReplyAt: thread.LastReplyAt,
LastViewedAt: thread.LastViewedAt,
UnreadReplies: thread.UnreadReplies,
UnreadMentions: thread.UnreadMentions,
Participants: threadParticipants,
Post: thread.Post.ToNilIfInvalid(),
DeleteAt: thread.ThreadDeleteAt,
IsUrgent: thread.IsUrgent,
}
}
type SqlThreadStore struct {
*SqlStore
// threadsSelectQuery is for querying directly into model.Thread
threadsSelectQuery sq.SelectBuilder
// threadsAndPostsSelectQuery is for querying into a struct embedding fields from
// model.Thread and model.Post.
threadsAndPostsSelectQuery sq.SelectBuilder
}
func (s *SqlThreadStore) ClearCaches() {
}
func newSqlThreadStore(sqlStore *SqlStore) store.ThreadStore {
s := SqlThreadStore{
SqlStore: sqlStore,
}
s.initializeQueries()
return &s
}
func (s *SqlThreadStore) initializeQueries() {
s.threadsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS DeleteAt",
"COALESCE(Threads.ThreadTeamId, '') AS TeamId",
).
From("Threads")
s.threadsAndPostsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS ThreadDeleteAt",
"COALESCE(Threads.ThreadTeamId, '') AS TeamId",
).
From("Threads")
}
func (s *SqlThreadStore) Get(id string) (*model.Thread, error) {
var thread model.Thread
query := s.threadsSelectQuery.
Where(sq.Eq{"PostId": id})
err := s.GetReplicaX().GetBuilder(&thread, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to get thread with id=%s", id)
}
return &thread, nil
}
func (s *SqlThreadStore) getTotalThreadsQuery(userId, teamId string, opts model.GetUserThreadsOpts) sq.SelectBuilder {
query := s.getQueryBuilder().
Select("COUNT(ThreadMemberships.PostId)").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
})
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
return query
}
// GetTotalUnreadThreads counts the number of unread threads for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
query := s.getTotalThreadsQuery(userId, teamId, opts).
Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
var totalUnreadThreads int64
err := s.GetReplicaX().GetBuilder(&totalUnreadThreads, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread threads for user id=%s", userId)
}
return totalUnreadThreads, nil
}
// GetTotalUnreadThreads counts the number of threads for the given user, optionally constrained
// to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
if opts.Unread {
return 0, errors.New("GetTotalThreads does not support the Unread flag; use GetTotalUnreadThreads instead")
}
query := s.getTotalThreadsQuery(userId, teamId, opts)
var totalThreads int64
err := s.GetReplicaX().GetBuilder(&totalThreads, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count threads for user id=%s", userId)
}
return totalThreads, nil
}
// GetTotalUnreadMentions counts the number of unread mentions for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalUnreadMentions int64
query := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
})
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
err := s.GetReplicaX().GetBuilder(&totalUnreadMentions, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread mentions for user id=%s", userId)
}
return totalUnreadMentions, nil
}
// GetTotalUnreadUrgentMentions counts the number of unread mentions for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadUrgentMentions(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalUnreadUrgentMentions int64
query := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
From("ThreadMemberships").
Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
"PostsPriority.Priority": model.PostPriorityUrgent,
})
if teamId != "" || !opts.Deleted {
query = query.Join("Threads ON Threads.PostId = ThreadMemberships.PostId")
}
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.
Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
err := s.GetReplicaX().GetBuilder(&totalUnreadUrgentMentions, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread urgent mentions for user id=%s", userId)
}
return totalUnreadUrgentMentions, nil
}
func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
pageSize := uint64(30)
if opts.PageSize != 0 {
pageSize = opts.PageSize
}
unreadRepliesQuery := sq.
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.Expr("Posts.RootId = ThreadMemberships.PostId")).
Where(sq.Expr("Posts.CreateAt > ThreadMemberships.LastViewed"))
if !opts.Deleted {
unreadRepliesQuery = unreadRepliesQuery.Where(sq.Eq{"Posts.DeleteAt": 0})
}
query := s.threadsAndPostsSelectQuery.
Column(postSliceCoalesceQuery()).
Columns(
"ThreadMemberships.LastViewed as LastViewedAt",
"ThreadMemberships.UnreadMentions as UnreadMentions",
).
Column(sq.Alias(unreadRepliesQuery, "UnreadReplies")).
Join("Posts ON Posts.Id = Threads.PostId").
Join("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId")
query = query.
Where(sq.Eq{"ThreadMemberships.UserId": userId}).
Where(sq.Eq{"ThreadMemberships.Following": true})
if opts.IncludeIsUrgent {
urgencyCase := sq.
Case().
When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true").
Else("false")
query = query.
Column(sq.Alias(urgencyCase, "IsUrgent")).
LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId")
}
// If a team is specified, constrain to channels in that team or DMs/GMs without
// a team at all.
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Or{
sq.Eq{"Threads.ThreadDeleteAt": nil},
sq.Eq{"Threads.ThreadDeleteAt": 0},
})
}
if opts.Since > 0 {
query = query.
Where(sq.Or{
sq.GtOrEq{"ThreadMemberships.LastUpdated": opts.Since},
sq.GtOrEq{"Threads.LastReplyAt": opts.Since},
})
}
if opts.Unread {
query = query.Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
}
order := "DESC"
if opts.Before != "" {
query = query.Where(sq.Expr(`Threads.LastReplyAt < (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.Before))
}
if opts.After != "" {
order = "ASC"
query = query.Where(sq.Expr(`Threads.LastReplyAt > (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.After))
}
query = query.
OrderBy("Threads.LastReplyAt " + order).
Limit(pageSize)
var threads []*JoinedThread
err := s.GetReplicaX().SelectBuilder(&threads, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch threads for user id=%s", userId)
}
// Build the de-duplicated set of user ids representing participants across all threads.
var participantUserIds []string
for _, thread := range threads {
for _, participantUserId := range thread.Participants {
participantUserIds = append(participantUserIds, participantUserId)
}
}
participantUserIds = model.RemoveDuplicateStrings(participantUserIds)
// Resolve the user objects for all participants, with extended metadata if requested.
allParticipants := make(map[string]*model.User, len(participantUserIds))
if opts.Extended {
users, err := s.User().GetProfileByIds(context.Background(), participantUserIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get %d thread profiles for user id=%s", len(participantUserIds), userId)
}
for _, user := range users {
allParticipants[user.Id] = user
}
} else {
for _, participantUserId := range participantUserIds {
allParticipants[participantUserId] = &model.User{Id: participantUserId}
}
}
result := make([]*model.ThreadResponse, 0, len(threads))
for _, thread := range threads {
result = append(result, thread.toThreadResponse(allParticipants))
}
return result, nil
}
// GetTeamsUnreadForUser returns the total unread threads and unread mentions
// for a user from all teams.
func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
fetchConditions := sq.And{
sq.Eq{"ThreadMemberships.UserId": userID},
sq.Eq{"ThreadMemberships.Following": true},
sq.Eq{"Threads.ThreadTeamId": teamIDs},
sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0},
}
var eg errgroup.Group
unreadThreads := []struct {
Count int64
TeamId string
}{}
unreadMentions := []struct {
Count int64
TeamId string
}{}
unreadUrgentMentions := []struct {
Count int64
TeamId string
}{}
// Running these concurrently hasn't shown any major downside
// than running them serially. So using a bit of perf boost.
// In any case, they will be replaced by computed columns later.
eg.Go(func() error {
repliesQuery := s.getQueryBuilder().
Select("COUNT(Threads.PostId) AS Count, ThreadTeamId AS TeamId").
From("Threads").
LeftJoin("ThreadMemberships ON Threads.PostId = ThreadMemberships.PostId").
Where(fetchConditions).
Where("Threads.LastReplyAt > ThreadMemberships.LastViewed").
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadThreads, repliesQuery), "failed to get total unread threads")
})
eg.Go(func() error {
mentionsQuery := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(fetchConditions).
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadMentions, mentionsQuery), "failed to get total unread mentions")
})
if includeUrgentMentionCount {
eg.Go(func() error {
urgentMentionsQuery := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId").
Where(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}).
Where(fetchConditions).
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadUrgentMentions, urgentMentionsQuery), "failed to get total unread urgent mentions")
})
}
// Wait for them to be over
if err := eg.Wait(); err != nil {
return nil, err
}
res := make(map[string]*model.TeamUnread)
// A bit of linear complexity here to create and return the map.
// This makes it easy to consume the output in the app layer.
for _, item := range unreadThreads {
res[item.TeamId] = &model.TeamUnread{
ThreadCount: item.Count,
}
}
for _, item := range unreadMentions {
if _, ok := res[item.TeamId]; ok {
res[item.TeamId].ThreadMentionCount = item.Count
} else {
res[item.TeamId] = &model.TeamUnread{
ThreadMentionCount: item.Count,
}
}
}
for _, item := range unreadUrgentMentions {
if _, ok := res[item.TeamId]; ok {
res[item.TeamId].ThreadUrgentMentionCount = item.Count
} else {
res[item.TeamId] = &model.TeamUnread{
ThreadUrgentMentionCount: item.Count,
}
}
}
return res, nil
}
func (s *SqlThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
users := []string{}
fetchConditions := sq.And{
sq.Eq{"PostId": threadID},
}
if fetchOnlyActive {
fetchConditions = sq.And{
sq.Eq{"Following": true},
fetchConditions,
}
}
query := s.getQueryBuilder().
Select("ThreadMemberships.UserId").
From("ThreadMemberships").
Where(fetchConditions)
err := s.GetReplicaX().SelectBuilder(&users, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread followers for thread id=%s", threadID)
}
return users, nil
}
func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityEnabled bool) (*model.ThreadResponse, error) {
if !threadMembership.Following {
return nil, store.NewErrNotFound("ThreadMembership", "<following>")
}
unreadRepliesQuery := sq.
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": threadMembership.PostId},
sq.Gt{"Posts.CreateAt": threadMembership.LastViewed},
sq.Eq{"Posts.DeleteAt": 0},
})
query := s.threadsAndPostsSelectQuery
for _, c := range postSliceColumns() {
query = query.Column("Posts." + c)
}
var thread JoinedThread
query = query.
Column(sq.Alias(unreadRepliesQuery, "UnreadReplies")).
LeftJoin("Posts ON Posts.Id = Threads.PostId").
Where(sq.Eq{"Threads.PostId": threadMembership.PostId})
if postPriorityEnabled {
urgencyCase := sq.
Case().
When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true").
Else("false")
query = query.
Column(sq.Alias(urgencyCase, "IsUrgent")).
LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId")
}
err := s.GetReplicaX().GetBuilder(&thread, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Thread", threadMembership.PostId)
}
return nil, errors.Wrapf(err, "failed to get thread for user id=%s, post id=%s", threadMembership.UserId, threadMembership.PostId)
}
thread.LastViewedAt = threadMembership.LastViewed
thread.UnreadMentions = threadMembership.UnreadMentions
users := []*model.User{}
if extended {
var err error
users, err = s.User().GetProfileByIds(context.Background(), thread.Participants, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread for user id=%s", threadMembership.UserId)
}
} else {
for _, userId := range thread.Participants {
users = append(users, &model.User{Id: userId})
}
}
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
return thread.toThreadResponse(usersMap), nil
}
// MarkAllAsReadByChannels marks thread membership for the given users in the given channels
// as read. This is used by the application layer to keep threads up-to-date when CRT is disabled
// for the enduser, avoiding an influx of unread threads when first turning the feature on.
func (s *SqlThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
if len(channelIDs) == 0 {
return nil
}
now := model.GetMillis()
var query sq.UpdateBuilder
if s.DriverName() == model.DatabaseDriverPostgres {
query = s.getQueryBuilder().Update("ThreadMemberships").From("Threads")
} else {
query = s.getQueryBuilder().Update("ThreadMemberships", "Threads")
}
query = query.Set("LastViewed", now).
Set("UnreadMentions", 0).
Set("LastUpdated", now).
Where(sq.Eq{"ThreadMemberships.UserId": userID}).
Where(sq.Expr("Threads.PostId = ThreadMemberships.PostId")).
Where(sq.Eq{"Threads.ChannelId": channelIDs}).
Where(sq.Expr("Threads.LastReplyAt > ThreadMemberships.LastViewed"))
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return errors.Wrapf(err, "failed to mark all threads as read by channels for user id=%s", userID)
}
return nil
}
func (s *SqlThreadStore) MarkAllAsRead(userId string, threadIds []string) error {
timestamp := model.GetMillis()
query := s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"PostId": threadIds}).
Set("LastViewed", timestamp).
Set("UnreadMentions", 0).
Set("LastUpdated", model.GetMillis())
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to mark %d threads as read for user id=%s", len(threadIds), userId)
}
return nil
}
// MarkAllAsReadByTeam marks all threads for the given user in the given team as read from the
// current time.
func (s *SqlThreadStore) MarkAllAsReadByTeam(userId, teamId string) error {
timestamp := model.GetMillis()
var query sq.UpdateBuilder
if s.DriverName() == model.DatabaseDriverPostgres {
query = s.getQueryBuilder().Update("ThreadMemberships").From("Threads")
} else {
query = s.getQueryBuilder().Update("ThreadMemberships", "Threads")
}
query = query.
Where("Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{"ThreadMemberships.UserId": userId}).
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
Set("LastViewed", timestamp).
Set("UnreadMentions", 0).
Set("LastUpdated", timestamp)
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to update thread read state for user id=%s", userId)
}
return nil
}
// MarkAsRead marks the given thread for the given user as unread from the given timestamp.
func (s *SqlThreadStore) MarkAsRead(userId, threadId string, timestamp int64) error {
query := s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"PostId": threadId}).
Set("LastViewed", timestamp).
Set("LastUpdated", model.GetMillis())
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to update thread read state for user id=%s thread_id=%v", userId, threadId)
}
return nil
}
func (s *SqlThreadStore) saveMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
query := s.getQueryBuilder().
Insert("ThreadMemberships").
Columns("PostId", "UserId", "Following", "LastViewed", "LastUpdated", "UnreadMentions").
Values(membership.PostId, membership.UserId, membership.Following, membership.LastViewed, membership.LastUpdated, membership.UnreadMentions)
_, err := ex.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to save thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
}
return membership, nil
}
func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
return s.updateMembership(s.GetMasterX(), membership)
}
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
query := s.getQueryBuilder().
Update("ThreadMemberships").
Set("Following", membership.Following).
Set("LastViewed", membership.LastViewed).
Set("LastUpdated", membership.LastUpdated).
Set("UnreadMentions", membership.UnreadMentions).
Where(sq.And{
sq.Eq{"PostId": membership.PostId},
sq.Eq{"UserId": membership.UserId},
})
_, err := ex.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to update thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
}
return membership, nil
}
func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.ThreadMembership, error) {
memberships := []*model.ThreadMembership{}
query := s.getQueryBuilder().
Select("ThreadMemberships.*").
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
From("ThreadMemberships").
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
Where(sq.Eq{"ThreadMemberships.UserId": userId})
err := s.GetReplicaX().SelectBuilder(&memberships, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s", userId)
}
return memberships, nil
}
func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error) {
return s.getMembershipForUser(s.GetReplicaX(), userId, postId)
}
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
var membership model.ThreadMembership
query := s.getQueryBuilder().
Select("*").
From("ThreadMemberships").
Where(sq.And{
sq.Eq{"PostId": postId},
sq.Eq{"UserId": userId},
})
err := ex.GetBuilder(&membership, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Thread", postId)
}
return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s postid=%s", userId, postId)
}
return &membership, nil
}
func (s *SqlThreadStore) DeleteMembershipForUser(userId string, postId string) error {
query := s.getQueryBuilder().
Delete("ThreadMemberships").
Where(sq.And{
sq.Eq{"PostId": postId},
sq.Eq{"UserId": userId},
})
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrap(err, "failed to delete thread membership")
}
return nil
}
// MaintainMembership creates or updates a thread membership for the given user
// and post. This method is used to update the state of a membership in response
// to some events like:
// - post creation (mentions handling)
// - channel marked unread
// - user explicitly following a thread
func (s *SqlThreadStore) MaintainMembership(userId, postId string, opts store.ThreadMembershipOpts) (_ *model.ThreadMembership, err error) {
trx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(trx, &err)
membership, err := s.getMembershipForUser(trx, userId, postId)
now := utils.MillisFromTime(time.Now())
// if membership exists, update it if:
// a. user started/stopped following a thread
// b. mention count changed
// c. user viewed a thread
if err == nil {
followingNeedsUpdate := (opts.UpdateFollowing && (membership.Following != opts.Following))
if followingNeedsUpdate || opts.IncrementMentions || opts.UpdateViewedTimestamp {
if followingNeedsUpdate {
membership.Following = opts.Following
}
if opts.UpdateViewedTimestamp {
membership.LastViewed = now
membership.UnreadMentions = 0
} else if opts.IncrementMentions {
membership.UnreadMentions += 1
}
membership.LastUpdated = now
if _, err = s.updateMembership(trx, membership); err != nil {
return nil, err
}
}
if err = trx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return membership, err
}
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return nil, errors.Wrap(err, "failed to get thread membership")
}
membership = &model.ThreadMembership{
PostId: postId,
UserId: userId,
Following: opts.Following,
LastUpdated: now,
}
if opts.IncrementMentions {
membership.UnreadMentions = 1
}
if opts.UpdateViewedTimestamp {
membership.LastViewed = now
}
membership, err = s.saveMembership(trx, membership)
if err != nil {
return nil, err
}
if opts.UpdateParticipants {
if s.DriverName() == model.DatabaseDriverPostgres {
userIdParam, err2 := jsonArray([]string{userId}).Value()
if err2 != nil {
return nil, err2
}
if s.IsBinaryParamEnabled() {
userIdParam = AppendBinaryFlag(userIdParam.([]byte))
}
if _, err2 := trx.ExecRaw(`UPDATE Threads
SET participants = participants || $1::jsonb
WHERE postid=$2
AND NOT participants ? $3`, userIdParam, postId, userId); err2 != nil {
return nil, err2
}
} else {
// CONCAT('$[', JSON_LENGTH(Participants), ']') just generates $[n]
// which is the positional syntax required for appending.
if _, err2 := trx.Exec(`UPDATE Threads
SET Participants = JSON_ARRAY_INSERT(Participants, CONCAT('$[', JSON_LENGTH(Participants), ']'), ?)
WHERE PostId=?
AND NOT JSON_CONTAINS(Participants, ?)`, userId, postId, strconv.Quote(userId)); err2 != nil {
return nil, err2
}
}
}
if err = trx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return membership, err
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("Threads.PostId").
From("Threads")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "Threads",
TimeColumn: "LastReplyAt",
PrimaryKeys: []string{"PostId"},
ChannelIDTable: "Threads",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// PermanentDeleteBatchThreadMembershipsForRetentionPolicies deletes a batch of records
// which are affected by the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("ThreadMemberships.PostId").
From("ThreadMemberships").
InnerJoin("Threads ON ThreadMemberships.PostId = Threads.PostId")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "ThreadMemberships",
TimeColumn: "LastUpdated",
PrimaryKeys: []string{"PostId"},
ChannelIDTable: "Threads",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes orphaned rows from Threads and ThreadMemberships
func (s *SqlThreadStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var threadsQuery string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Threads table. More details on the PR with
// this commit.
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
WHERE Threads.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX(PRIMARY))
LIMIT ?
) AS A
)`
} else {
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
LEFT JOIN Channels ON Threads.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
// We only delete a thread membership if the entire thread no longer exists,
// not if the root post has been deleted
const threadMembershipsQuery = `
DELETE FROM ThreadMemberships WHERE PostId IN (
SELECT * FROM (
SELECT ThreadMemberships.PostId FROM ThreadMemberships
LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId
WHERE Threads.PostId IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(threadsQuery, limit)
if err != nil {
return
}
rpcDeleted, err := result.RowsAffected()
if err != nil {
return
}
result, err = s.GetMasterX().Exec(threadMembershipsQuery, limit)
if err != nil {
return
}
rptDeleted, err := result.RowsAffected()
if err != nil {
return
}
deleted = rpcDeleted + rptDeleted
return
}
// return number of unread replies for a single thread
func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": threadMembership.PostId},
sq.Gt{"Posts.CreateAt": threadMembership.LastViewed},
sq.Eq{"Posts.DeleteAt": 0},
})
var unreadReplies int64
err := s.GetReplicaX().GetBuilder(&unreadReplies, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread reply count for post id=%s", threadMembership.PostId)
}
return unreadReplies, nil
}
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.LastReplyAt > ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
// gets all threads within the team which user follows.
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func userContains(userIDs []string, searchedUserID string) bool {
for _, userID := range userIDs {
if userID == searchedUserID {
return true
}
}
return false
}
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
// create list of userIDs
var userIDs []string
for _, topThread := range topThreads {
userID := topThread.UserId
if !userContains(userIDs, userID) {
userIDs = append(userIDs, userID)
}
}
usersMap := map[string]*model.User{}
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
}
for _, user := range users {
usersMap[user.Id] = user
}
// resolve user, root post for each top thread
for _, topThread := range topThreads {
postCreator := usersMap[topThread.UserId]
topThread.UserInformation = &model.InsightUserInformation{
Id: postCreator.Id,
LastPictureUpdate: postCreator.LastPictureUpdate,
FirstName: postCreator.FirstName,
LastName: postCreator.LastName,
Username: postCreator.Username,
NickName: postCreator.Nickname,
}
post, err := s.Post().GetSingle(topThread.PostId, false)
if err != nil {
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
}
topThread.Post = post
}
return topThreads, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlTokenStore struct {
*SqlStore
}
func newSqlTokenStore(sqlStore *SqlStore) store.TokenStore {
return &SqlTokenStore{sqlStore}
}
func (s SqlTokenStore) Save(token *model.Token) error {
if err := token.IsValid(); err != nil {
return err
}
query, args, err := s.getQueryBuilder().
Insert("Tokens").
Columns("Token", "CreateAt", "Type", "Extra").
Values(token.Token, token.CreateAt, token.Type, token.Extra).
ToSql()
if err != nil {
return errors.Wrap(err, "token_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to save Token")
}
return nil
}
func (s SqlTokenStore) Delete(token string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE Token = ?", token); err != nil {
return errors.Wrapf(err, "failed to delete Token with value %s", token)
}
return nil
}
func (s SqlTokenStore) GetByToken(tokenString string) (*model.Token, error) {
var token model.Token
if err := s.GetReplicaX().Get(&token, "SELECT * FROM Tokens WHERE Token = ?", tokenString); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Token", fmt.Sprintf("Token=%s", tokenString))
}
return nil, errors.Wrapf(err, "failed to get Token with value %s", tokenString)
}
return &token, nil
}
func (s SqlTokenStore) Cleanup(expiryTime int64) {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE CreateAt < ?", expiryTime); err != nil {
mlog.Error("Unable to cleanup token store.")
}
}
func (s SqlTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
tokens := []*model.Token{}
query, args, err := s.getQueryBuilder().
Select("*").
From("Tokens").
Where(sq.Eq{"Type": tokenType}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all tokens by type")
}
if err := s.GetReplicaX().Select(&tokens, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get all tokens of Type=%s", tokenType)
}
return tokens, nil
}
func (s SqlTokenStore) RemoveAllTokensByType(tokenType string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE Type = ?", tokenType); err != nil {
return errors.Wrapf(err, "failed to remove all Tokens with Type=%s", tokenType)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"strconv"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// SqlLicenseStore encapsulates the database writes and reads for
// model.LicenseRecord objects.
type SqlTrueUpReviewStore struct {
*SqlStore
}
func newSqlTrueUpReviewStore(sqlStore *SqlStore) store.TrueUpReviewStore {
return &SqlTrueUpReviewStore{sqlStore}
}
func trueUpReviewStatusColumns() []string {
return []string{
"DueDate",
"Completed",
}
}
func (s *SqlTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Select("*").
From("TrueUpReviewHistory").
Where(sq.Eq{"DueDate": dueDate})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_trueUpReviewStatusRecord_tosql")
}
var trueUpReviewStatus model.TrueUpReviewStatus
if err := s.GetReplicaX().Get(&trueUpReviewStatus, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TrueUpReviewStatus", strconv.FormatInt(dueDate, 10))
}
return nil, err
}
return &trueUpReviewStatus, nil
}
func (s *SqlTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
builder := s.getQueryBuilder().Insert("TrueUpReviewHistory").Columns(trueUpReviewStatusColumns()...).Values(reviewStatus.ToSlice()...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "create_trueUpReviewStatusRecord_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "fail to create true up review status record")
}
return reviewStatus, nil
}
func (s *SqlTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Update("TrueUpReviewHistory").
Set("Completed", reviewStatus.Completed).
Where(sq.Eq{"DueDate": reviewStatus.DueDate})
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update true up review status with DueDate=%d", reviewStatus.DueDate)
}
return reviewStatus, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUploadSessionStore struct {
*SqlStore
}
func newSqlUploadSessionStore(sqlStore *SqlStore) store.UploadSessionStore {
return &SqlUploadSessionStore{
SqlStore: sqlStore,
}
}
func (us SqlUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
if session == nil {
return nil, errors.New("SqlUploadSessionStore.Save: session should not be nil")
}
session.PreSave()
if err := session.IsValid(); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: validation failed")
}
query, args, err := us.getQueryBuilder().
Insert("UploadSessions").
Columns("Id", "Type", "CreateAt", "UserId", "ChannelId", "Filename", "Path", "FileSize", "FileOffset", "RemoteId", "ReqFileId").
Values(session.Id, session.Type, session.CreateAt, session.UserId, session.ChannelId, session.Filename, session.Path, session.FileSize, session.FileOffset, session.RemoteId, session.ReqFileId).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: failed to insert")
}
return session, nil
}
func (us SqlUploadSessionStore) Update(session *model.UploadSession) error {
if session == nil {
return errors.New("SqlUploadSessionStore.Update: session should not be nil")
}
if err := session.IsValid(); err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Update: validation failed")
}
query, args, err := us.getQueryBuilder().
Update("UploadSessions").
Set("Type", session.Type).
Set("CreateAt", session.CreateAt).
Set("UserId", session.UserId).
Set("ChannelId", session.ChannelId).
Set("Filename", session.Filename).
Set("Path", session.Path).
Set("FileSize", session.FileSize).
Set("FileOffset", session.FileOffset).
Set("RemoteId", session.RemoteId).
Set("ReqFileId", session.ReqFileId).
Where(sq.Eq{"Id": session.Id}).
ToSql()
if err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Update: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
if err == sql.ErrNoRows {
return store.NewErrNotFound("UploadSession", session.Id)
}
return errors.Wrapf(err, "SqlUploadSessionStore.Update: failed to update session with id=%s", session.Id)
}
return nil
}
func (us SqlUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
if !model.IsValidId(id) {
return nil, errors.New("SqlUploadSessionStore.Get: id is not valid")
}
query, args, err := us.getQueryBuilder().
Select("*").
From("UploadSessions").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Get: failed to build query")
}
var session model.UploadSession
if err := us.DBXFromContext(ctx).Get(&session, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UploadSession", id)
}
return nil, errors.Wrapf(err, "SqlUploadSessionStore.Get: failed to select session with id=%s", id)
}
return &session, nil
}
func (us SqlUploadSessionStore) GetForUser(userId string) ([]*model.UploadSession, error) {
query, args, err := us.getQueryBuilder().
Select("*").
From("UploadSessions").
Where(sq.Eq{"UserId": userId}).
OrderBy("CreateAt ASC").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.GetForUser: failed to build query")
}
sessions := []*model.UploadSession{}
if err := us.GetReplicaX().Select(&sessions, query, args...); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.GetForUser: failed to select")
}
return sessions, nil
}
func (us SqlUploadSessionStore) Delete(id string) error {
if !model.IsValidId(id) {
return errors.New("SqlUploadSessionStore.Delete: id is not valid")
}
query, args, err := us.getQueryBuilder().
Delete("UploadSessions").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Delete: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Delete: failed to delete")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUserAccessTokenStore struct {
*SqlStore
}
func newSqlUserAccessTokenStore(sqlStore *SqlStore) store.UserAccessTokenStore {
return &SqlUserAccessTokenStore{sqlStore}
}
func (s SqlUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
token.PreSave()
if err := token.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("UserAccessTokens").
Columns("Id", "Token", "UserId", "Description", "IsActive").
Values(token.Id, token.Token, token.UserId, token.Description, token.IsActive).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "UserAccessToken_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save UserAccessToken")
}
return token, nil
}
func (s SqlUserAccessTokenStore) Delete(tokenId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsAndTokensById(transaction, tokenId); err == nil {
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsAndTokensById(transaction *sqlxTxWrapper, tokenId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = ?"
}
if _, err := transaction.Exec(query, tokenId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken id=%s", tokenId)
}
return s.deleteTokensById(transaction, tokenId)
}
func (s SqlUserAccessTokenStore) deleteTokensById(transaction *sqlxTxWrapper, tokenId string) error {
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to delete UserAccessToken id=%s", tokenId)
}
return nil
}
func (s SqlUserAccessTokenStore) DeleteAllForUser(userId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsandTokensByUser(transaction, userId); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsandTokensByUser(transaction *sqlxTxWrapper, userId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.UserId = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.UserId = ?"
}
if _, err := transaction.Exec(query, userId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken userId=%s", userId)
}
return s.deleteTokensByUser(transaction, userId)
}
func (s SqlUserAccessTokenStore) deleteTokensByUser(transaction *sqlxTxWrapper, userId string) error {
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete UserAccessToken userId=%s", userId)
}
return nil
}
func (s SqlUserAccessTokenStore) Get(tokenId string) (*model.UserAccessToken, error) {
var token model.UserAccessToken
if err := s.GetReplicaX().Get(&token, "SELECT * FROM UserAccessTokens WHERE Id = ?", tokenId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserAccessToken", tokenId)
}
return nil, errors.Wrapf(err, "failed to get UserAccessToken with id=%s", tokenId)
}
return &token, nil
}
func (s SqlUserAccessTokenStore) GetAll(offset, limit int) ([]*model.UserAccessToken, error) {
tokens := []*model.UserAccessToken{}
if err := s.GetReplicaX().Select(&tokens, "SELECT * FROM UserAccessTokens LIMIT ? OFFSET ?", limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find UserAccessTokens")
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
var token model.UserAccessToken
if err := s.GetReplicaX().Get(&token, "SELECT * FROM UserAccessTokens WHERE Token = ?", tokenString); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserAccessToken", fmt.Sprintf("token=%s", tokenString))
}
return nil, errors.Wrapf(err, "failed to get UserAccessToken with token=%s", tokenString)
}
return &token, nil
}
func (s SqlUserAccessTokenStore) GetByUser(userId string, offset, limit int) ([]*model.UserAccessToken, error) {
tokens := []*model.UserAccessToken{}
if err := s.GetReplicaX().Select(&tokens, "SELECT * FROM UserAccessTokens WHERE UserId = ? LIMIT ? OFFSET ?", userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find UserAccessTokens with userId=%s", userId)
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
term = sanitizeSearchTerm(term, "\\")
tokens := []*model.UserAccessToken{}
params := []any{term, term, term}
query := `
SELECT
uat.*
FROM UserAccessTokens uat
INNER JOIN Users u
ON uat.UserId = u.Id
WHERE uat.Id LIKE ? OR uat.UserId LIKE ? OR u.Username LIKE ?`
if err := s.GetReplicaX().Select(&tokens, query, params...); err != nil {
return nil, errors.Wrapf(err, "failed to find UserAccessTokens by term with value '%s'", term)
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) UpdateTokenEnable(tokenId string) error {
if _, err := s.GetMasterX().Exec("UPDATE UserAccessTokens SET IsActive = TRUE WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to update UserAccessTokens with id=%s", tokenId)
}
return nil
}
func (s SqlUserAccessTokenStore) UpdateTokenDisable(tokenId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsAndDisableToken(transaction, tokenId); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsAndDisableToken(transaction *sqlxTxWrapper, tokenId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = ?"
}
if _, err := transaction.Exec(query, tokenId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken id=%s", tokenId)
}
return s.updateTokenDisable(transaction, tokenId)
}
func (s SqlUserAccessTokenStore) updateTokenDisable(transaction *sqlxTxWrapper, tokenId string) error {
if _, err := transaction.Exec("UPDATE UserAccessTokens SET IsActive = FALSE WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to update UserAccessToken with id=%s", tokenId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sort"
"strings"
"unicode/utf8"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxGroupChannelsForProfiles = 50
)
var (
UserSearchTypeNamesNoFullName = []string{"Username", "Nickname"}
UserSearchTypeNames = []string{"Username", "FirstName", "LastName", "Nickname"}
UserSearchTypeAllNoFullName = []string{"Username", "Nickname", "Email"}
UserSearchTypeAll = []string{"Username", "FirstName", "LastName", "Nickname", "Email"}
)
type SqlUserStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
// usersQuery is a starting point for all queries that return one or more Users.
usersQuery sq.SelectBuilder
}
func (us *SqlUserStore) ClearCaches() {}
func (us SqlUserStore) InvalidateProfileCacheForUser(userId string) {}
func newSqlUserStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.UserStore {
us := &SqlUserStore{
SqlStore: sqlStore,
metrics: metrics,
}
// note: we are providing field names explicitly here to maintain order of columns (needed when using raw queries)
us.usersQuery = us.getQueryBuilder().
Select("u.Id", "u.CreateAt", "u.UpdateAt", "u.DeleteAt", "u.Username", "u.Password", "u.AuthData", "u.AuthService", "u.Email", "u.EmailVerified", "u.Nickname", "u.FirstName", "u.LastName", "u.Position", "u.Roles", "u.AllowMarketing", "u.Props", "u.NotifyProps", "u.LastPasswordUpdate", "u.LastPictureUpdate", "u.FailedAttempts", "u.Locale", "u.Timezone", "u.MfaActive", "u.MfaSecret",
"b.UserId IS NOT NULL AS IsBot", "COALESCE(b.Description, '') AS BotDescription", "COALESCE(b.LastIconUpdate, 0) AS BotLastIconUpdate", "u.RemoteId").
From("Users u").
LeftJoin("Bots b ON ( b.UserId = u.Id )")
return us
}
func (us SqlUserStore) validateAutoResponderMessageSize(notifyProps model.StringMap) error {
if notifyProps != nil {
maxPostSize := us.Post().GetMaxPostSize()
msg := notifyProps[model.AutoResponderMessageNotifyProp]
msgSize := utf8.RuneCountInString(msg)
if msgSize > maxPostSize {
mlog.Warn("auto_responder_message has size restrictions", mlog.Int("max_characters", maxPostSize), mlog.Int("received_size", msgSize))
return errors.New("Auto responder message size can't be more than the allowed Post size")
}
}
return nil
}
func (us SqlUserStore) insert(user *model.User) (sql.Result, error) {
if err := us.validateAutoResponderMessageSize(user.NotifyProps); err != nil {
return nil, err
}
query := `INSERT INTO Users
(Id, CreateAt, UpdateAt, DeleteAt, Username, Password, AuthData, AuthService,
Email, EmailVerified, Nickname, FirstName, LastName, Position, Roles, AllowMarketing,
Props, NotifyProps, LastPasswordUpdate, LastPictureUpdate, FailedAttempts,
Locale, Timezone, MfaActive, MfaSecret, RemoteId)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :Username, :Password, :AuthData, :AuthService,
:Email, :EmailVerified, :Nickname, :FirstName, :LastName, :Position, :Roles, :AllowMarketing,
:Props, :NotifyProps, :LastPasswordUpdate, :LastPictureUpdate, :FailedAttempts,
:Locale, :Timezone, :MfaActive, :MfaSecret, :RemoteId)`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
return us.GetMasterX().NamedExec(query, user)
}
func (us SqlUserStore) InsertUsers(users []*model.User) error {
for _, user := range users {
_, err := us.insert(user)
if err != nil {
return err
}
}
return nil
}
func (us SqlUserStore) Save(user *model.User) (*model.User, error) {
if user.Id != "" && !user.IsRemote() {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
user.PreSave()
if err := user.IsValid(); err != nil {
return nil, err
}
if _, err := us.insert(user); err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique"}) {
return nil, store.NewErrInvalidInput("User", "email", user.Email)
}
if IsUniqueConstraintError(err, []string{"Username", "users_username_key", "idx_users_username_unique"}) {
return nil, store.NewErrInvalidInput("User", "username", user.Username)
}
return nil, errors.Wrapf(err, "failed to save User with userId=%s", user.Id)
}
return user, nil
}
func (us SqlUserStore) DeactivateGuests() ([]string, error) {
curTime := model.GetMillis()
updateQuery := us.getQueryBuilder().Update("Users").
Set("UpdateAt", curTime).
Set("DeleteAt", curTime).
Where(sq.Eq{"Roles": "system_guest"}).
Where(sq.Eq{"DeleteAt": 0})
queryString, args, err := updateQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "deactivate_guests_tosql")
}
_, err = us.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update Users with roles=system_guest")
}
selectQuery := us.getQueryBuilder().Select("Id").From("Users").Where(sq.Eq{"DeleteAt": curTime})
queryString, args, err = selectQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "deactivate_guests_tosql")
}
userIds := []string{}
err = us.GetMasterX().Select(&userIds, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return userIds, nil
}
func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
user.PreUpdate()
if err := user.IsValid(); err != nil {
return nil, err
}
if err := us.validateAutoResponderMessageSize(user.NotifyProps); err != nil {
return nil, err
}
oldUser := model.User{}
err := us.GetMasterX().Get(&oldUser, "SELECT * FROM Users WHERE Id=?", user.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get User with userId=%s", user.Id)
}
if oldUser.Id == "" {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
user.CreateAt = oldUser.CreateAt
user.AuthData = oldUser.AuthData
user.AuthService = oldUser.AuthService
user.Password = oldUser.Password
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
user.LastPictureUpdate = oldUser.LastPictureUpdate
user.EmailVerified = oldUser.EmailVerified
user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret
user.MfaActive = oldUser.MfaActive
if !trustedUpdateData {
user.Roles = oldUser.Roles
user.DeleteAt = oldUser.DeleteAt
}
if user.IsOAuthUser() {
if !trustedUpdateData {
user.Email = oldUser.Email
}
} else if user.IsLDAPUser() && !trustedUpdateData {
if user.Username != oldUser.Username || user.Email != oldUser.Email {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
} else if user.Email != oldUser.Email {
user.EmailVerified = false
}
if user.Username != oldUser.Username {
user.UpdateMentionKeysFromUsername(oldUser.Username)
}
query := `UPDATE Users
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, Username=:Username, Password=:Password,
AuthData=:AuthData, AuthService=:AuthService,Email=:Email, EmailVerified=:EmailVerified,
Nickname=:Nickname, FirstName=:FirstName, LastName=:LastName, Position=:Position, Roles=:Roles,
AllowMarketing=:AllowMarketing, Props=:Props, NotifyProps=:NotifyProps,
LastPasswordUpdate=:LastPasswordUpdate, LastPictureUpdate=:LastPictureUpdate,
FailedAttempts=:FailedAttempts,Locale=:Locale, Timezone=:Timezone, MfaActive=:MfaActive,
MfaSecret=:MfaSecret, RemoteId=:RemoteId
WHERE Id=:Id`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
res, err := us.GetMasterX().NamedExec(query, user)
if err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique"}) {
return nil, store.NewErrConflict("Email", err, user.Email)
}
if IsUniqueConstraintError(err, []string{"Username", "users_username_key", "idx_users_username_unique"}) {
return nil, store.NewErrConflict("Username", err, user.Username)
}
return nil, errors.Wrapf(err, "failed to update User with userId=%s", user.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to get rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("multiple users were update: userId=%s, count=%d", user.Id, count)
}
user.Sanitize(map[string]bool{})
oldUser.Sanitize(map[string]bool{})
return &model.UserUpdate{New: user.DeepCopy(), Old: &oldUser}, nil
}
func (us SqlUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
if err := us.validateAutoResponderMessageSize(props); err != nil {
return err
}
buf, err := json.Marshal(props)
if err != nil {
return errors.Wrap(err, "failed marshalling session props")
}
if us.IsBinaryParamEnabled() {
buf = AppendBinaryFlag(buf)
}
if _, err := us.GetMasterX().Exec(`UPDATE Users
SET NotifyProps = ?
WHERE Id = ?`, buf, userID); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userID)
}
return nil
}
func (us SqlUserStore) UpdateLastPictureUpdate(userId string) error {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", curTime, curTime, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) ResetLastPictureUpdate(userId string) error {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", 0, curTime, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateUpdateAt(userId string) (int64, error) {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET UpdateAt = ? WHERE Id = ?", curTime, userId); err != nil {
return curTime, errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return curTime, nil
}
func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET Password = ?, LastPasswordUpdate = ?, UpdateAt = ?, AuthData = NULL, AuthService = '', FailedAttempts = 0 WHERE Id = ?", hashedPassword, updateAt, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int) error {
if _, err := us.GetMasterX().Exec("UPDATE Users SET FailedAttempts = ? WHERE Id = ?", attempts, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *string, email string, resetMfa bool) (string, error) {
updateAt := model.GetMillis()
updateQuery := us.getQueryBuilder().Update("Users").
Set("Password", "").
Set("LastPasswordUpdate", updateAt).
Set("UpdateAt", updateAt).
Set("FailedAttempts", 0).
Set("AuthService", service).
Set("AuthData", authData).
Where(sq.Eq{"Id": userId})
if email != "" {
updateQuery = updateQuery.Set("Email", sq.Expr("lower(?)", email))
}
if resetMfa {
updateQuery = updateQuery.Set("MfaActive", false).
Set("MfaSecret", "")
}
queryString, args, err := updateQuery.ToSql()
if err != nil {
return "", errors.Wrap(err, "update_auth_data_tosql")
}
if _, err := us.GetMasterX().Exec(queryString, args...); err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique", "AuthData", "users_authdata_key"}) {
return "", store.NewErrInvalidInput("User", "id", userId)
}
return "", errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return userId, nil
}
// ResetAuthDataToEmailForUsers resets the AuthData of users whose AuthService
// is |service| to their Email. If userIDs is non-empty, only the users whose
// IDs are in userIDs will be affected. If dryRun is true, only the number
// of users who *would* be affected is returned; otherwise, the number of
// users who actually were affected is returned.
func (us SqlUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
whereEquals := sq.Eq{"AuthService": service}
if len(userIDs) > 0 {
whereEquals["Id"] = userIDs
}
if !includeDeleted {
whereEquals["DeleteAt"] = 0
}
if dryRun {
builder := us.getQueryBuilder().
Select("COUNT(*)").
From("Users").
Where(whereEquals)
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "select_count_users_tosql")
}
var numAffected int
err = us.GetReplicaX().Get(&numAffected, query, args...)
return numAffected, err
}
builder := us.getQueryBuilder().
Update("Users").
Set("AuthData", sq.Expr("Email")).
Where(whereEquals)
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "update_users_tosql")
}
result, err := us.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to update users' AuthData")
}
numAffected, err := result.RowsAffected()
return int(numAffected), err
}
func (us SqlUserStore) UpdateMfaSecret(userId, secret string) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaSecret = ?, UpdateAt = ? WHERE Id = ?", secret, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateMfaActive(userId string, active bool) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaActive = ?, UpdateAt = ? WHERE Id = ?", active, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
// GetMany returns a list of users for the provided list of ids
func (us SqlUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
query := us.usersQuery.Where(sq.Eq{"Id": ids})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_many_tosql")
}
users := []*model.User{}
if err := us.SqlStore.DBXFromContext(ctx).Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "users_get_many_select")
}
return users, nil
}
func (us SqlUserStore) Get(ctx context.Context, id string) (*model.User, error) {
query := us.usersQuery.Where("Id = ?", id)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_tosql")
}
row := us.SqlStore.DBXFromContext(ctx).QueryRow(queryString, args...)
var user model.User
var props, notifyProps, timezone []byte
err = row.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username,
&user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified,
&user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles,
&user.AllowMarketing, &props, ¬ifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate,
&user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret,
&user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("User", id)
}
return nil, errors.Wrapf(err, "failed to get User with userId=%s", id)
}
if err = json.Unmarshal(props, &user.Props); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user props")
}
if err = json.Unmarshal(notifyProps, &user.NotifyProps); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user notify props")
}
if err = json.Unmarshal(timezone, &user.Timezone); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user timezone")
}
return &user, nil
}
func (us SqlUserStore) GetAll() ([]*model.User, error) {
query := us.usersQuery.OrderBy("Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_users_tosql")
}
data := []*model.User{}
if err := us.GetReplicaX().Select(&data, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return data, nil
}
func (us SqlUserStore) GetAllAfter(limit int, afterId string) ([]*model.User, error) {
query := us.usersQuery.
Where("Id > ?", afterId).
OrderBy("Id ASC").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_after_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
func (us SqlUserStore) GetEtagForAllProfiles() string {
var updateAt int64
err := us.GetReplicaX().Get(&updateAt, "SELECT UpdateAt FROM Users ORDER BY UpdateAt DESC LIMIT 1")
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
}
func (us SqlUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, []string{}, []string{}, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to get User profiles")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func applyRoleFilter(query sq.SelectBuilder, role string, isPostgreSQL bool) sq.SelectBuilder {
if role == "" {
return query
}
if isPostgreSQL {
roleParam := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(role, "\\"))
return query.Where("u.Roles LIKE LOWER(?)", roleParam)
}
roleParam := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(role, "*"))
return query.Where("u.Roles LIKE ? ESCAPE '*'", roleParam)
}
func applyMultiRoleFilters(query sq.SelectBuilder, systemRoles []string, teamRoles []string, channelRoles []string, isPostgreSQL bool) sq.SelectBuilder {
sqOr := sq.Or{}
if len(systemRoles) > 0 && systemRoles[0] != "" {
for _, role := range systemRoles {
queryRole := wildcardSearchTerm(role)
switch role {
case model.SystemUserRoleId:
// If querying for a `system_user` ensure that the user is only a system_user.
sqOr = append(sqOr, sq.Eq{"u.Roles": role})
case model.SystemGuestRoleId, model.SystemAdminRoleId, model.SystemUserManagerRoleId, model.SystemReadOnlyAdminRoleId, model.SystemManagerRoleId:
// If querying for any other roles search using a wildcard.
if isPostgreSQL {
sqOr = append(sqOr, sq.ILike{"u.Roles": queryRole})
} else {
sqOr = append(sqOr, sq.Like{"u.Roles": queryRole})
}
}
}
}
if len(channelRoles) > 0 && channelRoles[0] != "" {
for _, channelRole := range channelRoles {
switch channelRole {
case model.ChannelAdminRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeAdmin": true}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeAdmin": true}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.ChannelUserRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeUser": true}, sq.Eq{"cm.SchemeAdmin": false}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeUser": true}, sq.Eq{"cm.SchemeAdmin": false}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.ChannelGuestRoleId:
sqOr = append(sqOr, sq.Eq{"cm.SchemeGuest": true})
}
}
}
if len(teamRoles) > 0 && teamRoles[0] != "" {
for _, teamRole := range teamRoles {
switch teamRole {
case model.TeamAdminRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeAdmin": true}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeAdmin": true}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.TeamUserRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeUser": true}, sq.Eq{"tm.SchemeAdmin": false}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeUser": true}, sq.Eq{"tm.SchemeAdmin": false}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.TeamGuestRoleId:
sqOr = append(sqOr, sq.Eq{"tm.SchemeGuest": true})
}
}
}
if len(sqOr) > 0 {
return query.Where(sqOr)
}
return query
}
func applyChannelGroupConstrainedFilter(query sq.SelectBuilder, channelId string) sq.SelectBuilder {
if channelId == "" {
return query
}
return query.
Where(`u.Id IN (
SELECT
GroupMembers.UserId
FROM
Channels
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Channels.Id = ?
AND GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
GroupMembers.UserId
)`, channelId)
}
func applyTeamGroupConstrainedFilter(query sq.SelectBuilder, teamId string) sq.SelectBuilder {
if teamId == "" {
return query
}
return query.
Where(`u.Id IN (
SELECT
GroupMembers.UserId
FROM
Teams
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Teams.Id = ?
AND GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
GroupMembers.UserId
)`, teamId)
}
func (us SqlUserStore) GetEtagForProfiles(teamId string) string {
var updateAt int64
err := us.GetReplicaX().Get(&updateAt, "SELECT UpdateAt FROM Users, TeamMembers WHERE TeamMembers.TeamId = ? AND Users.Id = TeamMembers.UserId ORDER BY UpdateAt DESC LIMIT 1", teamId)
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
}
func (us SqlUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 )").
Where("tm.TeamId = ?", options.InTeamId).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_etag_for_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {}
func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) {}
func (us SqlUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, us.DriverName() == model.DatabaseDriverPostgres)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
LeftJoin("Status s ON ( s.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy(`
CASE s.Status
WHEN 'online' THEN 1
WHEN 'away' THEN 2
WHEN 'dnd' THEN 3
ELSE 4
END
`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive && !options.Active {
query = query.Where("u.DeleteAt != 0")
} else if options.Active && !options.Inactive {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_by_status_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy(`cm.SchemeAdmin DESC`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive && !options.Active {
query = query.Where("u.DeleteAt != 0")
} else if options.Active && !options.Inactive {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_by_admin_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", channelID).
Where("u.DeleteAt = 0").
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_profiles_in_channel_tosql")
}
users := []*model.User{}
rows, err := us.SqlStore.DBXFromContext(ctx).Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
defer rows.Close()
for rows.Next() {
var user model.User
var props, notifyProps, timezone []byte
if err = rows.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username, &user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified, &user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles, &user.AllowMarketing, &props, ¬ifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate, &user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId); err != nil {
return nil, errors.Wrap(err, "failed to scan values from rows into User entity")
}
if err = json.Unmarshal(props, &user.Props); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user props")
}
if err = json.Unmarshal(notifyProps, &user.NotifyProps); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user notify props")
}
if err = json.Unmarshal(timezone, &user.Timezone); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user timezone")
}
users = append(users, &user)
}
err = rows.Err()
if err != nil {
return nil, errors.Wrap(err, "error while iterating over rows")
}
userMap := make(map[string]*model.User)
for _, u := range users {
u.Sanitize(map[string]bool{})
userMap[u.Id] = u
}
return userMap, nil
}
func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId).
LeftJoin("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
Where("cm.UserId IS NULL").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
if groupConstrained {
query = applyChannelGroupConstrainedFilter(query, channelId)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_not_in_channel_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
Where(`(
SELECT
COUNT(0)
FROM
TeamMembers
WHERE
TeamMembers.UserId = u.Id
AND TeamMembers.DeleteAt = 0
) = 0`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_without_team_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
query = query.
Where(map[string]any{
"Username": usernames,
}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_by_usernames")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
type UserWithLastActivityAt struct {
model.User
LastActivityAt int64
}
func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Column("s.LastActivityAt").
Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId).
Join("Status s ON (s.UserId = u.Id)").
OrderBy("s.LastActivityAt DESC").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_recently_active_users_for_team_tosql")
}
users := []*UserWithLastActivityAt{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userList := []*model.User{}
for _, userWithLastActivityAt := range users {
u := userWithLastActivityAt.User
u.Sanitize(map[string]bool{})
u.LastActivityAt = userWithLastActivityAt.LastActivityAt
userList = append(userList, &u)
}
return userList, nil
}
func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId).
OrderBy("u.CreateAt DESC").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_new_users_for_team_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
if options == nil {
options = &store.UserGetByIdsOpts{}
}
users := []*model.User{}
query := us.usersQuery.
Where(map[string]any{
"u.Id": userIds,
}).
OrderBy("u.Username ASC")
if options.Since > 0 {
query = query.Where(sq.Gt(map[string]any{
"u.UpdateAt": options.Since,
}))
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profile_by_ids_tosql")
}
if err := us.SqlStore.DBXFromContext(ctx).Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
type UserWithChannel struct {
model.User
ChannelId string
}
func (us SqlUserStore) GetProfileByGroupChannelIdsForUser(userId string, channelIds []string) (map[string][]*model.User, error) {
if len(channelIds) > MaxGroupChannelsForProfiles {
channelIds = channelIds[0:MaxGroupChannelsForProfiles]
}
isMemberQuery := fmt.Sprintf(`
EXISTS(
SELECT
1
FROM
ChannelMembers
WHERE
UserId = '%s'
AND
ChannelId = cm.ChannelId
)`, userId)
query := us.getQueryBuilder().
Select("u.*, cm.ChannelId").
From("Users u").
Join("ChannelMembers cm ON u.Id = cm.UserId").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.Eq{"c.Type": model.ChannelTypeGroup, "cm.ChannelId": channelIds}).
Where(isMemberQuery).
Where(sq.NotEq{"u.Id": userId}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_by_group_channel_ids_for_user_tosql")
}
usersWithChannel := []*UserWithChannel{}
if err := us.GetReplicaX().Select(&usersWithChannel, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
usersByChannelId := map[string][]*model.User{}
for _, user := range usersWithChannel {
if val, ok := usersByChannelId[user.ChannelId]; ok {
usersByChannelId[user.ChannelId] = append(val, &user.User)
} else {
usersByChannelId[user.ChannelId] = []*model.User{&user.User}
}
}
return usersByChannelId, nil
}
func (us SqlUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
query := us.usersQuery.
Where("Roles LIKE ?", "%system_admin%").
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_system_admin_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userMap := make(map[string]*model.User)
for _, u := range users {
u.Sanitize(map[string]bool{})
userMap[u.Id] = u
}
return userMap, nil
}
func (us SqlUserStore) GetByEmail(email string) (*model.User, error) {
query := us.usersQuery.Where("Email = lower(?)", email)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_email_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("User", fmt.Sprintf("email=%s", email)), "failed to find User")
}
return nil, errors.Wrapf(err, "failed to get User with email=%s", email)
}
return &user, nil
}
func (us SqlUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
if authData == nil || *authData == "" {
return nil, store.NewErrInvalidInput("User", "<authData>", "empty or nil")
}
query := us.usersQuery.
Where("u.AuthData = ?", authData).
Where("u.AuthService = ?", authService)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_auth_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("User", fmt.Sprintf("authData=%s, authService=%s", *authData, authService))
} else if err != nil {
return nil, errors.Wrapf(err, "failed to find User with authData=%s and authService=%s", *authData, authService)
}
return &user, nil
}
func (us SqlUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
query := us.usersQuery.
Where("u.AuthService = ?", authService).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_using_auth_service_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with authService=%s", authService)
}
return users, nil
}
func (us SqlUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
query := us.usersQuery.
Where(sq.NotEq{"u.AuthService": authServices}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_not_in_auth_service_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with authServices in %v", authServices)
}
return users, nil
}
func (us SqlUserStore) GetByUsername(username string) (*model.User, error) {
query := us.usersQuery.Where("u.Username = lower(?)", username)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_username_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("User", fmt.Sprintf("username=%s", username)), "failed to find User")
}
return nil, errors.Wrapf(err, "failed to find User with username=%s", username)
}
return &user, nil
}
func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail bool) (*model.User, error) {
query := us.usersQuery
if allowSignInWithUsername && allowSignInWithEmail {
query = query.Where("Username = lower(?) OR Email = lower(?)", loginId, loginId)
} else if allowSignInWithUsername {
query = query.Where("Username = lower(?)", loginId)
} else if allowSignInWithEmail {
query = query.Where("Email = lower(?)", loginId)
} else {
return nil, errors.New("sign in with username and email are disabled")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_for_login_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
if len(users) == 0 {
return nil, errors.New("user not found")
}
if len(users) > 1 {
return nil, errors.New("multiple users found")
}
return users[0], nil
}
func (us SqlUserStore) VerifyEmail(userId, email string) (string, error) {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET Email = lower(?), EmailVerified = true, UpdateAt = ? WHERE Id = ?", email, curTime, userId); err != nil {
return "", errors.Wrapf(err, "failed to update Users with userId=%s and email=%s", userId, email)
}
return userId, nil
}
func (us SqlUserStore) PermanentDelete(userId string) error {
if _, err := us.GetMasterX().Exec("DELETE FROM Users WHERE Id = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) Count(options model.UserCountOptions) (int64, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.getQueryBuilder().Select("COUNT(DISTINCT u.Id)").From("Users AS u")
if !options.IncludeDeleted {
query = query.Where("u.DeleteAt = 0")
}
if options.IncludeBotAccounts {
if options.ExcludeRegularUsers {
query = query.Join("Bots ON u.Id = Bots.UserId")
}
} else {
query = query.LeftJoin("Bots ON u.Id = Bots.UserId").Where("Bots.UserId IS NULL")
if options.ExcludeRegularUsers {
// Currently this doesn't make sense because it will always return 0
return int64(0), errors.New("query with IncludeBotAccounts=false and excludeRegularUsers=true always return 0")
}
}
if options.TeamId != "" {
query = query.LeftJoin("TeamMembers AS tm ON u.Id = tm.UserId").Where("tm.TeamId = ? AND tm.DeleteAt = 0", options.TeamId)
} else if options.ChannelId != "" {
query = query.LeftJoin("ChannelMembers AS cm ON u.Id = cm.UserId").Where("cm.ChannelId = ?", options.ChannelId)
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, false)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if isPostgreSQL {
query = query.PlaceholderFormat(sq.Dollar)
}
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_tosql")
}
var count int64
err = us.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsActiveCount(timePeriod int64, options model.UserCountOptions) (int64, error) {
time := model.GetMillis() - timePeriod
query := us.getQueryBuilder().Select("COUNT(*)").From("Status AS s").Where("LastActivityAt > ?", time)
if !options.IncludeBotAccounts {
query = query.LeftJoin("Bots ON s.UserId = Bots.UserId").Where("Bots.UserId IS NULL")
}
if !options.IncludeDeleted {
query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0")
}
queryStr, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "analytics_active_count_tosql")
}
var v int64
err = us.GetReplicaX().Get(&v, queryStr, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Users")
}
return v, nil
}
func (us SqlUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
query := us.getQueryBuilder().Select("COUNT(*)").From("Status AS s").Where("LastActivityAt > ? AND LastActivityAt <= ?", startTime, endTime)
if !options.IncludeBotAccounts {
query = query.LeftJoin("Bots ON s.UserId = Bots.UserId").Where("Bots.UserId IS NULL")
}
if !options.IncludeDeleted {
query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0")
}
queryStr, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to build query.")
}
var v int64
err = us.GetReplicaX().Get(&v, queryStr, args...)
if err != nil {
return 0, errors.Wrap(err, "Unable to get the active users during the requested period.")
}
return v, nil
}
func (us SqlUserStore) GetUnreadCount(userId string, isCRTEnabled bool) (int64, error) {
var mentionCountColumn = "cm.MentionCount"
if isCRTEnabled {
mentionCountColumn = "cm.MentionCountRoot"
}
query := `
SELECT SUM(` + mentionCountColumn + `)
FROM Channels c
INNER JOIN ChannelMembers cm
ON cm.ChannelId = c.Id
AND cm.UserId = ?
AND c.DeleteAt = 0
`
var count int64
err := us.GetReplicaX().Get(&count, query, userId)
if err != nil {
return count, errors.Wrapf(err, "failed to count unread Channels for userId=%s", userId)
}
return count, nil
}
func (us SqlUserStore) GetUnreadCountForChannel(userId string, channelId string) (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT SUM(CASE WHEN c.Type = ? THEN (c.TotalMsgCount - cm.MsgCount) ELSE cm.MentionCount END) FROM Channels c INNER JOIN ChannelMembers cm ON c.Id = cm.ChannelId AND cm.ChannelId = ? AND cm.UserId = ?", model.ChannelTypeDirect, channelId, userId)
if err != nil {
return 0, errors.Wrapf(err, "failed to get unread count for channelId=%s and userId=%s", channelId, userId)
}
return count, nil
}
func (us SqlUserStore) GetAnyUnreadPostCountForChannel(userId string, channelId string) (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT SUM(c.TotalMsgCount - cm.MsgCount) FROM Channels c INNER JOIN ChannelMembers cm ON c.Id = cm.ChannelId AND cm.ChannelId = ? AND cm.UserId = ?", channelId, userId)
if err != nil {
return count, errors.Wrapf(err, "failed to get any unread count for channelId=%s and userId=%s", channelId, userId)
}
return count, nil
}
func (us SqlUserStore) Search(teamId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
OrderBy("Username ASC").
Limit(uint64(options.Limit))
if teamId != "" {
query = query.Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Where(`(
SELECT
COUNT(0)
FROM
TeamMembers
WHERE
TeamMembers.UserId = u.Id
AND TeamMembers.DeleteAt = 0
) = 0`).
OrderBy("u.Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", notInTeamId).
Where("tm.UserId IS NULL").
OrderBy("u.Username ASC").
Limit(uint64(options.Limit))
if options.GroupConstrained {
query = applyTeamGroupConstrainedFilter(query, notInTeamId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
Where("cm.UserId IS NULL").
OrderBy("Username ASC").
Limit(uint64(options.Limit))
if teamId != "" {
query = query.Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId)
}
if options.GroupConstrained {
query = applyChannelGroupConstrainedFilter(query, channelId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchInChannel(channelId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? AND gm.DeleteAt = 0 )", groupID).
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? )", groupID).
Where("(gm.UserId IS NULL OR gm.deleteat != 0)").
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string, isPostgreSQL bool) sq.SelectBuilder {
for _, term := range terms {
searchFields := []string{}
termArgs := []any{}
for _, field := range fields {
if isPostgreSQL {
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(?) escape '*' ", field))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE ? escape '*' ", field))
}
termArgs = append(termArgs, fmt.Sprintf("%s%%", strings.TrimLeft(term, "@")))
}
query = query.Where(fmt.Sprintf("(%s)", strings.Join(searchFields, " OR ")), termArgs...)
}
return query
}
func (us SqlUserStore) performSearch(query sq.SelectBuilder, term string, options *model.UserSearchOptions) ([]*model.User, error) {
term = sanitizeSearchTerm(term, "*")
var searchType []string
if options.AllowEmails {
if options.AllowFullNames {
searchType = UserSearchTypeAll
} else {
searchType = UserSearchTypeAllNoFullName
}
} else {
if options.AllowFullNames {
searchType = UserSearchTypeNames
} else {
searchType = UserSearchTypeNamesNoFullName
}
}
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if !options.AllowInactive {
query = query.Where("u.DeleteAt = 0")
}
if strings.TrimSpace(term) != "" {
query = generateSearchQuery(query, strings.Fields(term), searchType, isPostgreSQL)
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "perform_search_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with term=%s and searchType=%v", term, searchType)
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT COUNT(Id) FROM Users WHERE DeleteAt > 0")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count inactive Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT COUNT(Id) FROM Users WHERE LOWER(Email) NOT LIKE ?", "%@"+strings.ToLower(hostDomain))
if err != nil {
return false, errors.Wrap(err, "failed to count inactive Users")
}
return count > 0, nil
}
func (us SqlUserStore) AnalyticsGetGuestCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT count(*) FROM Users WHERE Roles LIKE ? and DeleteAt = 0", "%system_guest%")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count guest Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT count(*) FROM Users WHERE Roles LIKE ? and DeleteAt = 0", "%system_admin%")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count system admin Users")
}
return count, nil
}
func (us SqlUserStore) GetProfilesNotInTeam(teamId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
users := []*model.User{}
query := us.usersQuery.
LeftJoin("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId).
Where("tm.UserId IS NULL").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
if groupConstrained {
query = applyTeamGroupConstrainedFilter(query, teamId)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_not_in_team_tosql")
}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetEtagForProfilesNotInTeam(teamId string) string {
querystr := `
SELECT
CONCAT(MAX(UpdateAt), '.', COUNT(Id)) as etag
FROM
Users as u
LEFT JOIN TeamMembers tm
ON tm.UserId = u.Id
AND tm.TeamId = ?
AND tm.DeleteAt = 0
WHERE
tm.UserId IS NULL
`
var etag string
err := us.GetReplicaX().Get(&etag, querystr, teamId)
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, etag)
}
func (us SqlUserStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
var err error
if transaction, err = us.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
users := []*model.User{}
if err := transaction.Select(&users, "SELECT * from Users WHERE Id > ? ORDER BY Id LIMIT 1000", lastUserId); err != nil {
return errors.Wrapf(err, "failed to find Users with id > %s", lastUserId)
}
if len(users) == 0 {
break
}
for _, user := range users {
lastUserId = user.Id
var newRoles []string
for _, role := range strings.Fields(user.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != user.Roles {
if _, err := transaction.Exec("UPDATE Users SET Roles = ? WHERE Id = ?", newRolesString, user.Id); err != nil {
return errors.Wrap(err, "failed to update Users")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (us SqlUserStore) InferSystemInstallDate() (int64, error) {
var createAt int64
err := us.GetReplicaX().Get(&createAt, "SELECT CreateAt FROM Users WHERE CreateAt IS NOT NULL ORDER BY CreateAt ASC LIMIT 1")
if err != nil {
return 0, errors.Wrap(err, "failed to infer system install date")
}
return createAt, nil
}
func (us SqlUserStore) GetFirstSystemAdminID() (string, error) {
var id string
err := us.GetReplicaX().Get(&id, "SELECT Id FROM Users WHERE Roles LIKE ? ORDER BY CreateAt ASC LIMIT 1", "%system_admin%")
if err != nil {
return "", errors.Wrap(err, "failed to get first system admin")
}
return id, nil
}
func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
users := []*model.User{}
usersQuery, args, err := us.usersQuery.
Where(sq.Or{
sq.Gt{"u.CreateAt": startTime},
sq.And{
sq.Eq{"u.CreateAt": startTime},
sq.Gt{"u.Id": startFileID},
},
}).
OrderBy("u.CreateAt ASC, u.Id ASC").
Limit(uint64(limit)).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql1")
}
err = us.GetSearchReplicaX().Select(&users, usersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userIds := []string{}
for _, user := range users {
userIds = append(userIds, user.Id)
}
channelMembers := []*model.ChannelMember{}
channelMembersQuery, args, err := us.getQueryBuilder().
Select(`
cm.ChannelId,
cm.UserId,
cm.Roles,
cm.LastViewedAt,
cm.MsgCount,
cm.MentionCount,
cm.MentionCountRoot,
cm.NotifyProps,
cm.LastUpdateAt,
cm.SchemeUser,
cm.SchemeAdmin,
(cm.SchemeGuest IS NOT NULL AND cm.SchemeGuest) as SchemeGuest
`).
From("ChannelMembers cm").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.And{
sq.Eq{
"cm.UserId": userIds,
},
sq.Or{
sq.Eq{"c.Type": model.ChannelTypeOpen},
sq.Eq{"c.Type": model.ChannelTypeDirect},
sq.Eq{"c.Type": model.ChannelTypeGroup},
},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql2")
}
err = us.GetSearchReplicaX().Select(&channelMembers, channelMembersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
teamMembers := []*model.TeamMember{}
teamMembersQuery, args, err := us.getQueryBuilder().
Select("TeamId, UserId, Roles, DeleteAt, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest, SchemeUser, SchemeAdmin").
From("TeamMembers").
Where(sq.Eq{"UserId": userIds, "DeleteAt": 0}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql3")
}
err = us.GetSearchReplicaX().Select(&teamMembers, teamMembersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
userMap := map[string]*model.UserForIndexing{}
for _, user := range users {
userMap[user.Id] = &model.UserForIndexing{
Id: user.Id,
Username: user.Username,
Nickname: user.Nickname,
FirstName: user.FirstName,
LastName: user.LastName,
Roles: user.Roles,
CreateAt: user.CreateAt,
DeleteAt: user.DeleteAt,
TeamsIds: []string{},
ChannelsIds: []string{},
}
}
for _, c := range channelMembers {
if userMap[c.UserId] != nil {
userMap[c.UserId].ChannelsIds = append(userMap[c.UserId].ChannelsIds, c.ChannelId)
}
}
for _, t := range teamMembers {
if userMap[t.UserId] != nil {
userMap[t.UserId].TeamsIds = append(userMap[t.UserId].TeamsIds, t.TeamId)
}
}
usersForIndexing := []*model.UserForIndexing{}
for _, user := range userMap {
usersForIndexing = append(usersForIndexing, user)
}
sort.Slice(usersForIndexing, func(i, j int) bool {
return usersForIndexing[i].CreateAt < usersForIndexing[j].CreateAt
})
return usersForIndexing, nil
}
func (us SqlUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
query := applyTeamGroupConstrainedFilter(us.usersQuery, teamID)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_group_users_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
query := applyChannelGroupConstrainedFilter(us.usersQuery, channelID)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_channel_group_users_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func applyViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions, distinct bool) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = u.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = u.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
if distinct {
return resultQuery.Distinct()
}
return resultQuery
}
func (us SqlUserStore) PromoteGuestToUser(userId string) (err error) {
transaction, err := us.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
user, err := us.Get(context.Background(), userId)
if err != nil {
return err
}
roles := user.GetRoles()
for idx, role := range roles {
if role == "system_guest" {
roles[idx] = "system_user"
}
}
curTime := model.GetMillis()
query := us.getQueryBuilder().Update("Users").
Set("Roles", strings.Join(roles, " ")).
Set("UpdateAt", curTime).
Where(sq.Eq{"Id": userId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
query = us.getQueryBuilder().Update("ChannelMembers").
Set("SchemeUser", true).
Set("SchemeGuest", false).
Where(sq.Eq{"UserId": userId})
queryString, args, err = query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update ChannelMembers with userId=%s", userId)
}
query = us.getQueryBuilder().Update("TeamMembers").
Set("SchemeUser", true).
Set("SchemeGuest", false).
Where(sq.Eq{"UserId": userId})
queryString, args, err = query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err := transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update TeamMembers with userId=%s", userId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (us SqlUserStore) DemoteUserToGuest(userID string) (_ *model.User, err error) {
transaction, err := us.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
user, err := us.Get(context.Background(), userID)
if err != nil {
return nil, err
}
roles := user.GetRoles()
newRoles := []string{}
for _, role := range roles {
if role == model.SystemUserRoleId {
newRoles = append(newRoles, model.SystemGuestRoleId)
} else if role != model.SystemAdminRoleId {
newRoles = append(newRoles, role)
}
}
curTime := model.GetMillis()
newRolesDBStr := strings.Join(newRoles, " ")
query := us.getQueryBuilder().Update("Users").
Set("Roles", newRolesDBStr).
Set("UpdateAt", curTime).
Where(sq.Eq{"Id": userID})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update User with userId=%s", userID)
}
user.Roles = newRolesDBStr
user.UpdateAt = curTime
query = us.getQueryBuilder().Update("ChannelMembers").
Set("SchemeUser", false).
Set("SchemeAdmin", false).
Set("SchemeGuest", true).
Where(sq.Eq{"UserId": userID})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s", userID)
}
query = us.getQueryBuilder().Update("TeamMembers").
Set("SchemeUser", false).
Set("SchemeAdmin", false).
Set("SchemeGuest", true).
Where(sq.Eq{"UserId": userID})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err := transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update TeamMembers with userId=%s", userID)
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return user, nil
}
func (us SqlUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
var usersInChannel, usersNotInChannel []*model.User
g := errgroup.Group{}
g.Go(func() (err error) {
usersInChannel, err = us.SearchInChannel(channelId, term, options)
return err
})
g.Go(func() (err error) {
usersNotInChannel, err = us.SearchNotInChannel(teamId, channelId, term, options)
return err
})
err := g.Wait()
if err != nil {
return nil, err
}
return &model.UserAutocompleteInChannel{
InChannel: usersInChannel,
OutOfChannel: usersNotInChannel,
}, nil
}
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.
func (us SqlUserStore) GetKnownUsers(userId string) ([]string, error) {
userIds := []string{}
usersQuery, args, err := us.getQueryBuilder().
Select("DISTINCT ocm.UserId").
From("ChannelMembers AS cm").
Join("ChannelMembers AS ocm ON ocm.ChannelId = cm.ChannelId").
Where(sq.NotEq{"ocm.UserId": userId}).
Where(sq.Eq{"cm.UserId": userId}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetKnownUsers_ToSql")
}
err = us.GetSearchReplicaX().Select(&userIds, usersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
return userIds, nil
}
// IsEmpty returns whether or not the Users table is empty.
func (us SqlUserStore) IsEmpty(excludeBots bool) (bool, error) {
var hasRows bool
builder := us.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("Users")
if excludeBots {
builder = builder.LeftJoin("Bots ON Users.Id = Bots.UserId").Where("Bots.UserId IS NULL")
}
builder = builder.Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "users_is_empty_to_sql")
}
if err = us.GetReplicaX().Get(&hasRows, query, args...); err != nil {
return false, errors.Wrap(err, "failed to check if table is empty")
}
return !hasRows, nil
}
func (us SqlUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
domainArray := strings.Split(restrictedDomains, ",")
query := us.usersQuery.
LeftJoin("Bots ON u.Id = Bots.UserId").
Where("Bots.UserId IS NULL").
Where("u.Roles != 'system_guest'").
Where("u.DeleteAt = 0").
Where("(u.AuthService = '' OR u.AuthService IS NULL)")
for _, d := range domainArray {
if d != "" {
query = query.Where("u.Email NOT LIKE LOWER(?)", wildcardSearchTerm(d))
}
}
query = query.Offset(uint64(page * perPage)).Limit(uint64(perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_many_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "users_get_many_select")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUserTermsOfServiceStore struct {
*SqlStore
}
func newSqlUserTermsOfServiceStore(sqlStore *SqlStore) store.UserTermsOfServiceStore {
return SqlUserTermsOfServiceStore{sqlStore}
}
func (s SqlUserTermsOfServiceStore) GetByUser(userId string) (*model.UserTermsOfService, error) {
var userTermsOfService model.UserTermsOfService
query := `
SELECT *
FROM UserTermsOfService
WHERE UserId = ?
`
if err := s.GetReplicaX().Get(&userTermsOfService, query, userId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserTermsOfService", "userId="+userId)
}
return nil, errors.Wrapf(err, "failed to get UserTermsOfService with userId=%s", userId)
}
return &userTermsOfService, nil
}
func (s SqlUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
userTermsOfService.PreSave()
if err := userTermsOfService.IsValid(); err != nil {
return nil, err
}
query := `
UPDATE UserTermsOfService
SET UserId = :UserId, TermsOfServiceId = :TermsOfServiceId, CreateAt = :CreateAt
WHERE UserId = :UserId
`
result, err := s.GetMasterX().NamedExec(query, userTermsOfService)
if err != nil {
return nil, errors.Wrapf(err, "failed to update UserTermsOfService with userId=%s and termsOfServiceId=%s", userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
}
updatedRows, err := result.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve the number of affected rows for the update of UserTermsOfService")
}
if updatedRows == 0 {
query := `
INSERT INTO UserTermsOfService
(UserId, TermsOfServiceId, CreateAt)
VALUES
(:UserId, :TermsOfServiceId, :CreateAt)
`
if _, err := s.GetMasterX().NamedExec(query, userTermsOfService); err != nil {
return nil, errors.Wrapf(err, "failed to save UserTermsOfService with userId=%s and termsOfServiceId=%s", userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
}
}
return userTermsOfService, nil
}
func (s SqlUserTermsOfServiceStore) Delete(userId, termsOfServiceId string) error {
query := `
DELETE
FROM UserTermsOfService
WHERE UserId = ? AND TermsOfServiceId = ?
`
if _, err := s.GetMasterX().Exec(query, userId, termsOfServiceId); err != nil {
return errors.Wrapf(err, "failed to delete UserTermsOfService with userId=%s and termsOfServiceId=%s", userId, termsOfServiceId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"io"
"net/url"
"strconv"
"strings"
"unicode"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/go-sql-driver/mysql"
)
var escapeLikeSearchChar = []string{
"%",
"_",
}
func sanitizeSearchTerm(term string, escapeChar string) string {
term = strings.Replace(term, escapeChar, "", -1)
for _, c := range escapeLikeSearchChar {
term = strings.Replace(term, c, escapeChar+c, -1)
}
return term
}
// Converts a list of strings into a list of query parameters and a named parameter map that can
// be used as part of a SQL query.
func MapStringsToQueryParams(list []string, paramPrefix string) (string, map[string]any) {
var keys strings.Builder
params := make(map[string]any, len(list))
for i, entry := range list {
if keys.Len() > 0 {
keys.WriteString(",")
}
key := paramPrefix + strconv.Itoa(i)
keys.WriteString(":" + key)
params[key] = entry
}
return "(" + keys.String() + ")", params
}
// finalizeTransactionX ensures a transaction is closed after use, rolling back if not already committed.
func finalizeTransactionX(transaction *sqlxTxWrapper, perr *error) {
// Rollback returns sql.ErrTxDone if the transaction was already closed.
if err := transaction.Rollback(); err != nil && err != sql.ErrTxDone {
*perr = merror.Append(*perr, err)
}
}
func deferClose(c io.Closer, perr *error) {
err := c.Close()
*perr = merror.Append(*perr, err)
}
// removeNonAlphaNumericUnquotedTerms removes all unquoted words that only contain
// non-alphanumeric chars from given line
func removeNonAlphaNumericUnquotedTerms(line, separator string) string {
words := strings.Split(line, separator)
filteredResult := make([]string, 0, len(words))
for _, w := range words {
if isQuotedWord(w) || containsAlphaNumericChar(w) {
filteredResult = append(filteredResult, strings.TrimSpace(w))
}
}
return strings.Join(filteredResult, separator)
}
// containsAlphaNumericChar returns true in case any letter or digit is present, false otherwise
func containsAlphaNumericChar(s string) bool {
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return true
}
}
return false
}
// isQuotedWord return true if the input string is quoted, false otherwise. Ex :-
//
// "quoted string" - will return true
// unquoted string - will return false
func isQuotedWord(s string) bool {
if len(s) < 2 {
return false
}
return s[0] == '"' && s[len(s)-1] == '"'
}
// constructMySQLJSONArgs returns the arg list to pass to a query along with
// the string of placeholders which is needed to be to the JSON_SET function.
// Use this function in this way:
// UPDATE Table
// SET Col = JSON_SET(Col, `+argString+`)
// WHERE Id=?`, args...)
// after appending the Id param to the args slice.
func constructMySQLJSONArgs(props map[string]string) ([]any, string) {
if len(props) == 0 {
return nil, ""
}
// Unpack the keys and values to pass to MySQL.
args := make([]any, 0, len(props))
for k, v := range props {
args = append(args, "$."+k, v)
}
// We calculate the number of ? to set in the query string.
argString := strings.Repeat("?, ", len(props)*2)
// Strip off the trailing comma.
argString = strings.TrimSuffix(argString, ", ")
return args, argString
}
func makeStringArgs(params []string) []any {
args := make([]any, len(params))
for i, name := range params {
args[i] = name
}
return args
}
func constructArrayArgs(ids []string) (string, []any) {
var placeholder strings.Builder
values := make([]any, 0, len(ids))
for _, entry := range ids {
if placeholder.Len() > 0 {
placeholder.WriteString(",")
}
placeholder.WriteString("?")
values = append(values, entry)
}
return "(" + placeholder.String() + ")", values
}
func wrapBinaryParamStringMap(ok bool, props model.StringMap) model.StringMap {
if props == nil {
props = make(model.StringMap)
}
props[model.BinaryParamKey] = strconv.FormatBool(ok)
return props
}
// morphWriter is a target to pass to the logger instance of morph.
// For now, everything is just logged at a debug level. If we need to log
// errors/warnings from the library also, that needs to be seen later.
type morphWriter struct {
}
func (l *morphWriter) Write(in []byte) (int, error) {
mlog.Debug(string(in))
return len(in), nil
}
func DSNHasBinaryParam(dsn string) (bool, error) {
url, err := url.Parse(dsn)
if err != nil {
return false, err
}
return url.Query().Get("binary_parameters") == "yes", nil
}
// AppendBinaryFlag updates the byte slice to work using binary_parameters=yes.
func AppendBinaryFlag(buf []byte) []byte {
return append([]byte{0x01}, buf...)
}
// AppendMultipleStatementsFlag attached dsn parameters to MySQL dsn in order to make migrations work.
func AppendMultipleStatementsFlag(dataSource string) (string, error) {
config, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
if config.Params == nil {
config.Params = map[string]string{}
}
config.Params["multiStatements"] = "true"
return config.FormatDSN(), nil
}
// ResetReadTimeout removes the timeout constraint from the MySQL dsn.
func ResetReadTimeout(dataSource string) (string, error) {
config, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
config.ReadTimeout = 0
return config.FormatDSN(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlWebhookStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func (s SqlWebhookStore) ClearCaches() {
}
func newSqlWebhookStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.WebhookStore {
return &SqlWebhookStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
func (s SqlWebhookStore) InvalidateWebhookCache(webhookId string) {
}
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("IncomingWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO IncomingWebhooks
(Id, CreateAt, UpdateAt, DeleteAt, UserId, ChannelId, TeamId, DisplayName, Description, Username, IconURL, ChannelLocked)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :UserId, :ChannelId, :TeamId, :DisplayName, :Description, :Username, :IconURL, :ChannelLocked)`, webhook); err != nil {
return nil, errors.Wrapf(err, "failed to save IncomingWebhook with id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlWebhookStore) UpdateIncoming(hook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
hook.UpdateAt = model.GetMillis()
_, err := s.GetMasterX().NamedExec(`UPDATE IncomingWebhooks SET
CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, ChannelId=:ChannelId, TeamId=:TeamId, DisplayName=:DisplayName,
Description=:Description, Username=:Username, IconURL=:IconURL, ChannelLocked=:ChannelLocked
WHERE Id=:Id`, hook)
if err != nil {
return nil, errors.Wrapf(err, "failed to update IncomingWebhook with id=%s", hook.Id)
}
return hook, nil
}
func (s SqlWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
var webhook model.IncomingWebhook
if err := s.GetReplicaX().Get(&webhook, "SELECT * FROM IncomingWebhooks WHERE Id = ? AND DeleteAt = 0", id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("IncomingWebhook", id)
}
return nil, errors.Wrapf(err, "failed to get IncomingWebhook with id=%s", id)
}
return &webhook, nil
}
func (s SqlWebhookStore) DeleteIncoming(webhookId string, time int64) error {
_, err := s.GetMasterX().Exec("UPDATE IncomingWebhooks SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, webhookId)
if err != nil {
return errors.Wrapf(err, "failed to update IncomingWebhook with id=%s", webhookId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM IncomingWebhooks WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete IncomingWebhook with userId=%s", userId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM IncomingWebhooks WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete IncomingWebhook with channelId=%s", channelId)
}
return nil
}
func (s SqlWebhookStore) GetIncomingList(offset, limit int) ([]*model.IncomingWebhook, error) {
return s.GetIncomingListByUser("", offset, limit)
}
func (s SqlWebhookStore) GetIncomingListByUser(userId string, offset, limit int) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("IncomingWebhooks").
Where(sq.Eq{"DeleteAt": int(0)}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "incoming_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find IncomingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetIncomingByTeamByUser(teamId string, userId string, offset, limit int) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("IncomingWebhooks").
Where(sq.And{
sq.Eq{"TeamId": teamId},
sq.Eq{"DeleteAt": int(0)},
}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "incoming_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find IncomingWebhook with teamId=%s", teamId)
}
return webhooks, nil
}
func (s SqlWebhookStore) GetIncomingByTeam(teamId string, offset, limit int) ([]*model.IncomingWebhook, error) {
return s.GetIncomingByTeamByUser(teamId, "", offset, limit)
}
func (s SqlWebhookStore) GetIncomingByChannel(channelId string) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
if err := s.GetReplicaX().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE ChannelId = ? AND DeleteAt = 0", channelId); err != nil {
return nil, errors.Wrapf(err, "failed to find IncomingWebhooks with channelId=%s", channelId)
}
return webhooks, nil
}
func (s SqlWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("OutgoingWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO OutgoingWebhooks
(Id, Token, CreateAt, UpdateAt, DeleteAt, CreatorId, ChannelId, TeamId, TriggerWords, TriggerWhen,
CallbackURLs, DisplayName, Description, ContentType, Username, IconURL)
VALUES
(:Id, :Token, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :ChannelId, :TeamId, :TriggerWords, :TriggerWhen,
:CallbackURLs, :DisplayName, :Description, :ContentType, :Username, :IconURL)`, webhook); err != nil {
return nil, errors.Wrapf(err, "failed to save OutgoingWebhook with id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
var webhook model.OutgoingWebhook
if err := s.GetReplicaX().Get(&webhook, "SELECT * FROM OutgoingWebhooks WHERE Id = ? AND DeleteAt = 0", id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("OutgoingWebhook", id)
}
return nil, errors.Wrapf(err, "failed to get OutgoingWebhook with id=%s", id)
}
return &webhook, nil
}
func (s SqlWebhookStore) GetOutgoingListByUser(userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"DeleteAt": int(0)},
}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingList(offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingListByUser("", offset, limit)
}
func (s SqlWebhookStore) GetOutgoingByChannelByUser(channelId string, userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Eq{"DeleteAt": int(0)},
})
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
if limit >= 0 && offset >= 0 {
query = query.Limit(uint64(limit)).Offset(uint64(offset))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingByChannel(channelId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingByChannelByUser(channelId, "", offset, limit)
}
func (s SqlWebhookStore) GetOutgoingByTeamByUser(teamId string, userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"TeamId": teamId},
sq.Eq{"DeleteAt": int(0)},
})
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
if limit >= 0 && offset >= 0 {
query = query.Limit(uint64(limit)).Offset(uint64(offset))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingByTeam(teamId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingByTeamByUser(teamId, "", offset, limit)
}
func (s SqlWebhookStore) DeleteOutgoing(webhookId string, time int64) error {
_, err := s.GetMasterX().Exec("Update OutgoingWebhooks SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, webhookId)
if err != nil {
return errors.Wrapf(err, "failed to update OutgoingWebhook with id=%s", webhookId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteOutgoingByUser(userId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM OutgoingWebhooks WHERE CreatorId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete OutgoingWebhook with creatorId=%s", userId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteOutgoingByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM OutgoingWebhooks WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete OutgoingWebhook with channelId=%s", channelId)
}
s.ClearCaches()
return nil
}
func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
hook.UpdateAt = model.GetMillis()
_, err := s.GetMasterX().NamedExec(`UPDATE OutgoingWebhooks SET
CreateAt = :CreateAt, UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, Token = :Token, CreatorId = :CreatorId,
ChannelId = :ChannelId, TeamId = :TeamId, TriggerWords = :TriggerWords, TriggerWhen = :TriggerWhen,
CallbackURLs = :CallbackURLs, DisplayName = :DisplayName, Description = :Description,
ContentType = :ContentType, Username = :Username, IconURL = :IconURL WHERE Id = :Id`, hook)
if err != nil {
return nil, errors.Wrapf(err, "failed to update OutgoingWebhook with id=%s", hook.Id)
}
return hook, nil
}
func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) (int64, error) {
queryBuilder :=
s.getQueryBuilder().
Select("COUNT(*)").
From("IncomingWebhooks").
Where("DeleteAt = 0")
if teamId != "" {
queryBuilder = queryBuilder.Where("TeamId", teamId)
}
queryString, args, err := queryBuilder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "incoming_webhook_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count IncomingWebhooks")
}
return count, nil
}
func (s SqlWebhookStore) AnalyticsOutgoingCount(teamId string) (int64, error) {
queryBuilder :=
s.getQueryBuilder().
Select("COUNT(*)").
From("OutgoingWebhooks").
Where("DeleteAt = 0")
if teamId != "" {
queryBuilder = queryBuilder.Where("TeamId", teamId)
}
queryString, args, err := queryBuilder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "outgoing_webhook_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count OutgoingWebhooks")
}
return count, nil
}
//go:generate go run layer_generators/main.go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package store
import (
"context"
"database/sql"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
type StoreResult struct {
Data any
// NErr a temporary field used by the new code for the AppError migration. This will later become Err when the entire store is migrated.
NErr error
}
type Store interface {
Team() TeamStore
Channel() ChannelStore
Post() PostStore
RetentionPolicy() RetentionPolicyStore
Thread() ThreadStore
User() UserStore
Bot() BotStore
Audit() AuditStore
ClusterDiscovery() ClusterDiscoveryStore
RemoteCluster() RemoteClusterStore
Compliance() ComplianceStore
Session() SessionStore
OAuth() OAuthStore
System() SystemStore
Webhook() WebhookStore
Command() CommandStore
CommandWebhook() CommandWebhookStore
Preference() PreferenceStore
License() LicenseStore
Token() TokenStore
Emoji() EmojiStore
Status() StatusStore
FileInfo() FileInfoStore
UploadSession() UploadSessionStore
Reaction() ReactionStore
Role() RoleStore
Scheme() SchemeStore
Job() JobStore
UserAccessToken() UserAccessTokenStore
ChannelMemberHistory() ChannelMemberHistoryStore
Plugin() PluginStore
TermsOfService() TermsOfServiceStore
ProductNotices() ProductNoticesStore
Group() GroupStore
UserTermsOfService() UserTermsOfServiceStore
LinkMetadata() LinkMetadataStore
SharedChannel() SharedChannelStore
Draft() DraftStore
MarkSystemRanUnitTests()
Close()
LockToMaster()
UnlockFromMaster()
DropAllTables()
RecycleDBConnections(d time.Duration)
GetDBSchemaVersion() (int, error)
GetAppliedMigrations() ([]model.AppliedMigration, error)
GetDbVersion(numerical bool) (string, error)
// GetInternalMasterDB allows access to the raw master DB
// handle for the multi-product architecture.
GetInternalMasterDB() *sql.DB
// GetInternalReplicaDBs allows access to the raw replica DB
// handles for the multi-product architecture.
GetInternalReplicaDB() *sql.DB
GetInternalReplicaDBs() []*sql.DB
TotalMasterDbConnections() int
TotalReadDbConnections() int
TotalSearchDbConnections() int
ReplicaLagTime() error
ReplicaLagAbs() error
CheckIntegrity() <-chan model.IntegrityCheckResult
SetContext(context context.Context)
Context() context.Context
NotifyAdmin() NotifyAdminStore
PostPriority() PostPriorityStore
PostAcknowledgement() PostAcknowledgementStore
TrueUpReview() TrueUpReviewStore
}
type RetentionPolicyStore interface {
Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
GetAll(offset, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error)
GetCount() (int64, error)
Delete(id string) error
GetChannels(policyId string, offset, limit int) (model.ChannelListWithTeamData, error)
GetChannelsCount(policyId string) (int64, error)
AddChannels(policyId string, channelIds []string) error
RemoveChannels(policyId string, channelIds []string) error
GetTeams(policyId string, offset, limit int) ([]*model.Team, error)
GetTeamsCount(policyId string) (int64, error)
AddTeams(policyId string, teamIds []string) error
RemoveTeams(policyId string, teamIds []string) error
DeleteOrphanedRows(limit int) (int64, error)
GetTeamPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForTeam, error)
GetTeamPoliciesCountForUser(userID string) (int64, error)
GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error)
GetChannelPoliciesCountForUser(userID string) (int64, error)
}
type TeamStore interface {
Save(team *model.Team) (*model.Team, error)
Update(team *model.Team) (*model.Team, error)
Get(id string) (*model.Team, error)
GetMany(ids []string) ([]*model.Team, error)
GetByName(name string) (*model.Team, error)
GetByNames(name []string) ([]*model.Team, error)
SearchAll(opts *model.TeamSearch) ([]*model.Team, error)
SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error)
SearchOpen(opts *model.TeamSearch) ([]*model.Team, error)
SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error)
GetAll() ([]*model.Team, error)
GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error)
GetAllPrivateTeamListing() ([]*model.Team, error)
GetAllTeamListing() ([]*model.Team, error)
GetTeamsByUserId(userID string) ([]*model.Team, error)
GetByInviteId(inviteID string) (*model.Team, error)
GetByEmptyInviteID() ([]*model.Team, error)
PermanentDelete(teamID string) error
AnalyticsTeamCount(opts *model.TeamSearch) (int64, error)
SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error)
SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error)
UpdateMember(member *model.TeamMember) (*model.TeamMember, error)
UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error)
GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error)
GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error)
GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error)
GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error)
GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error)
GetTeamsForUser(ctx context.Context, userID, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error)
GetTeamsForUserWithPagination(userID string, page, perPage int) ([]*model.TeamMember, error)
GetChannelUnreadsForAllTeams(excludeTeamID, userID string) ([]*model.ChannelUnread, error)
GetChannelUnreadsForTeam(teamID, userID string) ([]*model.ChannelUnread, error)
RemoveMember(teamID string, userID string) error
RemoveMembers(teamID string, userIds []string) error
RemoveAllMembersByTeam(teamID string) error
RemoveAllMembersByUser(userID string) error
UpdateLastTeamIconUpdate(teamID string, curTime int64) error
GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error)
MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error)
ResetAllTeamSchemes() error
ClearAllCustomRoleAssignments() error
AnalyticsGetTeamCountForScheme(schemeID string) (int64, error)
GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error)
GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error)
UserBelongsToTeams(userID string, teamIds []string) (bool, error)
GetUserTeamIds(userID string, allowFromCache bool) ([]string, error)
InvalidateAllTeamIdsForUser(userID string)
ClearCaches()
// UpdateMembersRole sets all of the given team members to admins and all of the other members of the team to
// non-admin members.
UpdateMembersRole(teamID string, userIDs []string) error
// GroupSyncedTeamCount returns the count of non-deleted group-constrained teams.
GroupSyncedTeamCount() (int64, error)
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error)
GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error)
}
type ChannelStore interface {
Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error)
CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error)
SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error)
Update(channel *model.Channel) (*model.Channel, error)
UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error
ClearSidebarOnTeamLeave(userID, teamID string) error
Get(id string, allowFromCache bool) (*model.Channel, error)
GetMany(ids []string, allowFromCache bool) (model.ChannelList, error)
InvalidateChannel(id string)
InvalidateChannelByName(teamID, name string)
Delete(channelID string, timestamp int64) error
Restore(channelID string, timestamp int64) error
SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error
PermanentDelete(channelID string) error
PermanentDeleteByTeam(teamID string) error
GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error)
GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error)
GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error)
GetDeletedByName(team_id string, name string) (*model.Channel, error)
GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error)
GetChannels(teamID, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error)
GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error)
GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error)
GetAllChannelMembersById(id string) ([]string, error)
GetAllChannels(page, perPage int, opts ChannelSearchOpts) (model.ChannelListWithTeamData, error)
GetAllChannelsCount(opts ChannelSearchOpts) (int64, error)
GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error)
GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error)
GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error)
GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error)
GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error)
GetTeamChannels(teamID string) (model.ChannelList, error)
GetAll(teamID string) ([]*model.Channel, error)
GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error)
GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error)
GetForPost(postID string) (*model.Channel, error)
SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error)
SaveMember(member *model.ChannelMember) (*model.ChannelMember, error)
UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error)
UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error)
// UpdateMemberNotifyProps patches the notifyProps field with the given props map.
// It replaces existing fields and creates new ones which don't exist.
UpdateMemberNotifyProps(channelID, userID string, props map[string]string) (*model.ChannelMember, error)
GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error)
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
GetChannelMembersTimezones(channelID string) ([]model.StringMap, error)
GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error)
InvalidateAllChannelMembersForUser(userID string)
IsUserInChannelUseCache(userID string, channelID string) bool
GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)
InvalidateCacheForChannelMembersNotifyProps(channelID string)
GetMemberForPost(postID string, userID string) (*model.ChannelMember, error)
InvalidateMemberCount(channelID string)
GetMemberCountFromCache(channelID string) int64
GetFileCount(channelID string) (int64, error)
GetMemberCount(channelID string, allowFromCache bool) (int64, error)
GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error)
InvalidatePinnedPostCount(channelID string)
GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error)
InvalidateGuestCount(channelID string)
GetGuestCount(channelID string, allowFromCache bool) (int64, error)
GetPinnedPosts(channelID string) (*model.PostList, error)
RemoveMember(channelID string, userID string) error
RemoveMembers(channelID string, userIds []string) error
PermanentDeleteMembersByUser(userID string) error
PermanentDeleteMembersByChannel(channelID string) error
UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error)
UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error)
CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error)
CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error)
IncrementMentionCount(channelID string, userIDs []string, isRoot, isUrgent bool) error
AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error)
GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error)
GetTeamMembersForChannel(channelID string) ([]string, error)
GetMembersForUserWithPagination(userID string, page, perPage int) (model.ChannelMembersWithTeamData, error)
GetMembersForUserWithCursor(userID, teamID string, opts *ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error)
Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error)
AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error)
AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchAllChannels(term string, opts ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error)
SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error)
SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchMore(userID string, teamID string, term string) (model.ChannelList, error)
SearchGroupChannels(userID, term string) (model.ChannelList, error)
GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error)
GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error)
GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error)
AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error)
GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error)
ClearCaches()
ClearMembersForUserCache()
GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error)
MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error)
ResetAllChannelSchemes() error
ClearAllCustomRoleAssignments() error
CreateInitialSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error)
GetSidebarCategoriesForTeamForUser(userID, teamID string) (*model.OrderedSidebarCategories, error)
GetSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error)
GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error)
GetSidebarCategoryOrder(userID, teamID string) ([]string, error)
CreateSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error)
UpdateSidebarCategoryOrder(userID, teamID string, categoryOrder []string) error
UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error)
UpdateSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarCategory(categoryID string) error
GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error)
GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error)
GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error)
RemoveAllDeactivatedMembers(channelID string) error
GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error)
UserBelongsToChannels(userID string, channelIds []string) (bool, error)
// UpdateMembersRole sets all of the given team members to admins and all of the other members of the team to
// non-admin members.
UpdateMembersRole(channelID string, userIDs []string) error
// GroupSyncedChannelCount returns the count of non-deleted group-constrained channels.
GroupSyncedChannelCount() (int64, error)
SetShared(channelId string, shared bool) error
// GetTeamForChannel returns the team for a given channelID.
GetTeamForChannel(channelID string) (*model.Team, error)
// Insights - channels
GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error)
GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error)
PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error)
// Insights - inactive channels
GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
}
type ChannelMemberHistoryStore interface {
LogJoinEvent(userID string, channelID string, joinTime int64) error
LogLeaveEvent(userID string, channelID string, leaveTime int64) error
GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetChannelsLeftSince(userID string, since int64) ([]string, error)
}
type ThreadStore interface {
GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error)
Get(id string) (*model.Thread, error)
GetTotalUnreadThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalUnreadMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalUnreadUrgentMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetThreadsForUser(userId, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error)
GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityIsEnabled bool) (*model.ThreadResponse, error)
GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error)
MarkAllAsRead(userID string, threadIds []string) error
MarkAllAsReadByTeam(userID, teamID string) error
MarkAllAsReadByChannels(userID string, channelIDs []string) error
MarkAsRead(userID, threadID string, timestamp int64) error
UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error)
GetMembershipsForUser(userId, teamID string) ([]*model.ThreadMembership, error)
GetMembershipForUser(userId, postID string) (*model.ThreadMembership, error)
DeleteMembershipForUser(userId, postID string) error
MaintainMembership(userID, postID string, opts ThreadMembershipOpts) (*model.ThreadMembership, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
// Insights - threads
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
}
type PostStore interface {
SaveMultiple(posts []*model.Post) ([]*model.Post, int, error)
Save(post *model.Post) (*model.Post, error)
Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error)
Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error)
GetSingle(id string, inclDeleted bool) (*model.Post, error)
Delete(postID string, timestamp int64, deleteByID string) error
PermanentDeleteByUser(userID string) error
PermanentDeleteByChannel(channelID string) error
GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error)
GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error)
// @openTracingParams userID, teamID, offset, limit
GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, error)
GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, error)
GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsByThread(threadID string, since int64) ([]*model.Post, error)
GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error)
GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error)
GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error)
GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string
Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error)
AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error)
AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error)
AnalyticsPostCount(options *model.PostCountOptions) (int64, error)
ClearCaches()
InvalidateLastPostTimeCache(channelID string)
GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error)
Overwrite(post *model.Post) (*model.Post, error)
OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error)
GetPostsByIds(postIds []string) ([]*model.Post, error)
GetEditHistoryForPost(postId string) ([]*model.Post, error)
GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetOldest() (*model.Post, error)
GetMaxPostSize() int
GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error)
GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error)
GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error)
SearchPostsForUser(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.PostSearchResults, error)
GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error)
LogRecentSearch(userID string, searchQuery []byte, createAt int64) error
GetOldestEntityCreationTime() (int64, error)
HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error)
GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error)
SetPostReminder(reminder *model.PostReminder) error
GetPostReminders(now int64) ([]*model.PostReminder, error)
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
GetNthRecentPostTime(n int64) (int64, error)
// Insights - top DMs
GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error)
}
type UserStore interface {
Save(user *model.User) (*model.User, error)
Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error)
UpdateNotifyProps(userID string, props map[string]string) error
UpdateLastPictureUpdate(userID string) error
ResetLastPictureUpdate(userID string) error
UpdatePassword(userID, newPassword string) error
UpdateUpdateAt(userID string) (int64, error)
UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error)
ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error)
UpdateMfaSecret(userID, secret string) error
UpdateMfaActive(userID string, active bool) error
Get(ctx context.Context, id string) (*model.User, error)
GetMany(ctx context.Context, ids []string) ([]*model.User, error)
GetAll() ([]*model.User, error)
ClearCaches()
InvalidateProfilesInChannelCacheByUser(userID string)
InvalidateProfilesInChannelCache(channelID string)
GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error)
GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error)
GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error)
GetProfiles(options *model.UserGetOptions) ([]*model.User, error)
GetProfileByIds(ctx context.Context, userIds []string, options *UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error)
GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error)
InvalidateProfileCacheForUser(userID string)
GetByEmail(email string) (*model.User, error)
GetByAuth(authData *string, authService string) (*model.User, error)
GetAllUsingAuthService(authService string) ([]*model.User, error)
GetAllNotInAuthService(authServices []string) ([]*model.User, error)
GetByUsername(username string) (*model.User, error)
GetForLogin(loginID string, allowSignInWithUsername, allowSignInWithEmail bool) (*model.User, error)
VerifyEmail(userID, email string) (string, error)
GetEtagForAllProfiles() string
GetEtagForProfiles(teamID string) string
UpdateFailedPasswordAttempts(userID string, attempts int) error
GetSystemAdminProfiles() (map[string]*model.User, error)
PermanentDelete(userID string) error
AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error)
AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error)
GetUnreadCount(userID string, isCRTEnabled bool) (int64, error)
GetUnreadCountForChannel(userID string, channelID string) (int64, error)
GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error)
GetRecentlyActiveUsersForTeam(teamID string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetNewUsersForTeam(teamID string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
AnalyticsGetInactiveUsersCount() (int64, error)
AnalyticsGetExternalUsers(hostDomain string) (bool, error)
AnalyticsGetSystemAdminCount() (int64, error)
AnalyticsGetGuestCount() (int64, error)
GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetEtagForProfilesNotInTeam(teamID string) string
ClearAllCustomRoleAssignments() error
InferSystemInstallDate() (int64, error)
GetAllAfter(limit int, afterID string) ([]*model.User, error)
GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error)
Count(options model.UserCountOptions) (int64, error)
GetTeamGroupUsers(teamID string) ([]*model.User, error)
GetChannelGroupUsers(channelID string) ([]*model.User, error)
PromoteGuestToUser(userID string) error
DemoteUserToGuest(userID string) (*model.User, error)
DeactivateGuests() ([]string, error)
AutocompleteUsersInChannel(teamID, channelID, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error)
GetKnownUsers(userID string) ([]string, error)
IsEmpty(excludeBots bool) (bool, error)
GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error)
InsertUsers(users []*model.User) error
GetFirstSystemAdminID() (string, error)
}
type BotStore interface {
Get(userID string, includeDeleted bool) (*model.Bot, error)
GetAll(options *model.BotGetOptions) ([]*model.Bot, error)
Save(bot *model.Bot) (*model.Bot, error)
Update(bot *model.Bot) (*model.Bot, error)
PermanentDelete(userID string) error
}
type SessionStore interface {
Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error)
Save(session *model.Session) (*model.Session, error)
GetSessions(userID string) ([]*model.Session, error)
GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error)
GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error)
UpdateExpiredNotify(sessionid string, notified bool) error
Remove(sessionIDOrToken string) error
RemoveAllSessions() error
PermanentDeleteSessionsByUser(teamID string) error
UpdateExpiresAt(sessionID string, timestamp int64) error
UpdateLastActivityAt(sessionID string, timestamp int64) error
UpdateRoles(userID string, roles string) (string, error)
UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error)
UpdateProps(session *model.Session) error
AnalyticsSessionCount() (int64, error)
Cleanup(expiryTime int64, batchSize int64) error
}
type AuditStore interface {
Save(audit *model.Audit) error
Get(user_id string, offset int, limit int) (model.Audits, error)
PermanentDeleteByUser(userID string) error
}
type ClusterDiscoveryStore interface {
Save(discovery *model.ClusterDiscovery) error
Delete(discovery *model.ClusterDiscovery) (bool, error)
Exists(discovery *model.ClusterDiscovery) (bool, error)
GetAll(discoveryType, clusterName string) ([]*model.ClusterDiscovery, error)
SetLastPingAt(discovery *model.ClusterDiscovery) error
Cleanup() error
}
type RemoteClusterStore interface {
Save(rc *model.RemoteCluster) (*model.RemoteCluster, error)
Update(rc *model.RemoteCluster) (*model.RemoteCluster, error)
Delete(remoteClusterId string) (bool, error)
Get(remoteClusterId string) (*model.RemoteCluster, error)
GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)
UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error)
SetLastPingAt(remoteClusterId string) error
}
type ComplianceStore interface {
Save(compliance *model.Compliance) (*model.Compliance, error)
Update(compliance *model.Compliance) (*model.Compliance, error)
Get(id string) (*model.Compliance, error)
GetAll(offset, limit int) (model.Compliances, error)
ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error)
MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error)
}
type OAuthStore interface {
SaveApp(app *model.OAuthApp) (*model.OAuthApp, error)
UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error)
GetApp(id string) (*model.OAuthApp, error)
GetAppByUser(userID string, offset, limit int) ([]*model.OAuthApp, error)
GetApps(offset, limit int) ([]*model.OAuthApp, error)
GetAuthorizedApps(userID string, offset, limit int) ([]*model.OAuthApp, error)
DeleteApp(id string) error
SaveAuthData(authData *model.AuthData) (*model.AuthData, error)
GetAuthData(code string) (*model.AuthData, error)
RemoveAuthData(code string) error
RemoveAuthDataByClientId(clientId string, userId string) error
PermanentDeleteAuthDataByUser(userID string) error
SaveAccessData(accessData *model.AccessData) (*model.AccessData, error)
UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error)
GetAccessData(token string) (*model.AccessData, error)
GetAccessDataByUserForApp(userID, clientId string) ([]*model.AccessData, error)
GetAccessDataByRefreshToken(token string) (*model.AccessData, error)
GetPreviousAccessData(userID, clientId string) (*model.AccessData, error)
RemoveAccessData(token string) error
RemoveAllAccessData() error
}
type SystemStore interface {
Save(system *model.System) error
SaveOrUpdate(system *model.System) error
Update(system *model.System) error
Get() (model.StringMap, error)
GetByName(name string) (*model.System, error)
PermanentDeleteByName(name string) (*model.System, error)
InsertIfExists(system *model.System) (*model.System, error)
SaveOrUpdateWithWarnMetricHandling(system *model.System) error
}
type WebhookStore interface {
SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error)
GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error)
GetIncomingList(offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingListByUser(userID string, offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingByTeam(teamID string, offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingByTeamByUser(teamID string, userID string, offset, limit int) ([]*model.IncomingWebhook, error)
UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error)
GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error)
DeleteIncoming(webhookID string, timestamp int64) error
PermanentDeleteIncomingByChannel(channelID string) error
PermanentDeleteIncomingByUser(userID string) error
SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
GetOutgoing(id string) (*model.OutgoingWebhook, error)
GetOutgoingByChannel(channelID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByChannelByUser(channelID string, userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingList(offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingListByUser(userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByTeam(teamID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByTeamByUser(teamID string, userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
DeleteOutgoing(webhookID string, timestamp int64) error
PermanentDeleteOutgoingByChannel(channelID string) error
PermanentDeleteOutgoingByUser(userID string) error
UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
AnalyticsIncomingCount(teamID string) (int64, error)
AnalyticsOutgoingCount(teamID string) (int64, error)
InvalidateWebhookCache(webhook string)
ClearCaches()
}
type CommandStore interface {
Save(webhook *model.Command) (*model.Command, error)
GetByTrigger(teamID string, trigger string) (*model.Command, error)
Get(id string) (*model.Command, error)
GetByTeam(teamID string) ([]*model.Command, error)
Delete(commandID string, timestamp int64) error
PermanentDeleteByTeam(teamID string) error
PermanentDeleteByUser(userID string) error
Update(hook *model.Command) (*model.Command, error)
AnalyticsCommandCount(teamID string) (int64, error)
}
type CommandWebhookStore interface {
Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error)
Get(id string) (*model.CommandWebhook, error)
TryUse(id string, limit int) error
Cleanup()
}
type PreferenceStore interface {
Save(preferences model.Preferences) error
GetCategory(userID string, category string) (model.Preferences, error)
GetCategoryAndName(category string, nane string) (model.Preferences, error)
Get(userID string, category string, name string) (*model.Preference, error)
GetAll(userID string) (model.Preferences, error)
Delete(userID, category, name string) error
DeleteCategory(userID string, category string) error
DeleteCategoryAndName(category string, name string) error
PermanentDeleteByUser(userID string) error
DeleteOrphanedRows(limit int) (deleted int64, err error)
CleanupFlagsBatch(limit int64) (int64, error)
}
type LicenseStore interface {
Save(license *model.LicenseRecord) (*model.LicenseRecord, error)
Get(id string) (*model.LicenseRecord, error)
GetAll() ([]*model.LicenseRecord, error)
}
type TokenStore interface {
Save(recovery *model.Token) error
Delete(token string) error
GetByToken(token string) (*model.Token, error)
Cleanup(expiryTime int64)
GetAllTokensByType(tokenType string) ([]*model.Token, error)
RemoveAllTokensByType(tokenType string) error
}
type EmojiStore interface {
Save(emoji *model.Emoji) (*model.Emoji, error)
Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error)
GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error)
GetMultipleByName(names []string) ([]*model.Emoji, error)
GetList(offset, limit int, sort string) ([]*model.Emoji, error)
Delete(emoji *model.Emoji, timestamp int64) error
Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error)
}
type StatusStore interface {
SaveOrUpdate(status *model.Status) error
Get(userID string) (*model.Status, error)
GetByIds(userIds []string) ([]*model.Status, error)
ResetAll() error
GetTotalActiveUsersCount() (int64, error)
UpdateLastActivityAt(userID string, lastActivityAt int64) error
UpdateExpiredDNDStatuses() ([]*model.Status, error)
}
type FileInfoStore interface {
Save(info *model.FileInfo) (*model.FileInfo, error)
Upsert(info *model.FileInfo) (*model.FileInfo, error)
Get(id string) (*model.FileInfo, error)
GetFromMaster(id string) (*model.FileInfo, error)
GetByIds(ids []string) ([]*model.FileInfo, error)
GetByPath(path string) (*model.FileInfo, error)
GetForPost(postID string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error)
GetForUser(userID string) ([]*model.FileInfo, error)
GetWithOptions(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error)
InvalidateFileInfosForPostCache(postID string, deleted bool)
AttachToPost(fileID string, postID string, channelID, creatorID string) error
DeleteForPost(postID string) (string, error)
PermanentDelete(fileID string) error
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
PermanentDeleteByUser(userID string) (int64, error)
SetContent(fileID, content string) error
Search(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.FileInfoList, error)
CountAll() (int64, error)
GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error)
ClearCaches()
GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error)
// GetUptoNSizeFileTime returns the CreateAt time of the last accessible file with a running-total size upto n bytes.
GetUptoNSizeFileTime(n int64) (int64, error)
}
type UploadSessionStore interface {
Save(session *model.UploadSession) (*model.UploadSession, error)
Update(session *model.UploadSession) error
Get(ctx context.Context, id string) (*model.UploadSession, error)
GetForUser(userID string) ([]*model.UploadSession, error)
Delete(id string) error
}
type ReactionStore interface {
Save(reaction *model.Reaction) (*model.Reaction, error)
Delete(reaction *model.Reaction) (*model.Reaction, error)
GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error)
GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error)
DeleteAllWithEmojiName(emojiName string) error
BulkGetForPosts(postIds []string) ([]*model.Reaction, error)
DeleteOrphanedRows(limit int) (int64, error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error)
GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error)
}
type JobStore interface {
Save(job *model.Job) (*model.Job, error)
UpdateOptimistically(job *model.Job, currentStatus string) (bool, error)
UpdateStatus(id string, status string) (*model.Job, error)
UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error)
Get(id string) (*model.Job, error)
GetAllPage(offset int, limit int) ([]*model.Job, error)
GetAllByType(jobType string) ([]*model.Job, error)
GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error)
GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error)
GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error)
GetAllByStatus(status string) ([]*model.Job, error)
GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error)
GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error)
GetCountByStatusAndType(status string, jobType string) (int64, error)
Delete(id string) (string, error)
Cleanup(expiryTime int64, batchSize int) error
}
type UserAccessTokenStore interface {
Save(token *model.UserAccessToken) (*model.UserAccessToken, error)
DeleteAllForUser(userID string) error
Delete(tokenID string) error
Get(tokenID string) (*model.UserAccessToken, error)
GetAll(offset int, limit int) ([]*model.UserAccessToken, error)
GetByToken(tokenString string) (*model.UserAccessToken, error)
GetByUser(userID string, page, perPage int) ([]*model.UserAccessToken, error)
Search(term string) ([]*model.UserAccessToken, error)
UpdateTokenEnable(tokenID string) error
UpdateTokenDisable(tokenID string) error
}
type PluginStore interface {
SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error)
CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error)
CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error)
SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error)
Get(pluginID, key string) (*model.PluginKeyValue, error)
Delete(pluginID, key string) error
DeleteAllForPlugin(PluginID string) error
DeleteAllExpired() error
List(pluginID string, page, perPage int) ([]string, error)
}
type RoleStore interface {
Save(role *model.Role) (*model.Role, error)
Get(roleID string) (*model.Role, error)
GetAll() ([]*model.Role, error)
GetByName(ctx context.Context, name string) (*model.Role, error)
GetByNames(names []string) ([]*model.Role, error)
Delete(roleID string) (*model.Role, error)
PermanentDeleteAll() error
// HigherScopedPermissions retrieves the higher-scoped permissions of a list of role names. The higher-scope
// (either team scheme or system scheme) is determined based on whether the team has a scheme or not.
ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error)
// AllChannelSchemeRoles returns all of the roles associated to channel schemes.
AllChannelSchemeRoles() ([]*model.Role, error)
// ChannelRolesUnderTeamRole returns all of the non-deleted roles that are affected by updates to the
// given role.
ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error)
}
type SchemeStore interface {
Save(scheme *model.Scheme) (*model.Scheme, error)
Get(schemeID string) (*model.Scheme, error)
GetByName(schemeName string) (*model.Scheme, error)
GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error)
Delete(schemeID string) (*model.Scheme, error)
PermanentDeleteAll() error
CountByScope(scope string) (int64, error)
CountWithoutPermission(scope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error)
}
type TermsOfServiceStore interface {
Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error)
GetLatest(allowFromCache bool) (*model.TermsOfService, error)
Get(id string, allowFromCache bool) (*model.TermsOfService, error)
}
type ProductNoticesStore interface {
View(userID string, notices []string) error
Clear(notices []string) error
ClearOldNotices(currentNotices model.ProductNotices) error
GetViews(userID string) ([]model.ProductNoticeViewState, error)
}
type UserTermsOfServiceStore interface {
GetByUser(userID string) (*model.UserTermsOfService, error)
Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error)
Delete(userID, termsOfServiceId string) error
}
type GroupStore interface {
Create(group *model.Group) (*model.Group, error)
CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error)
Get(groupID string) (*model.Group, error)
GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error)
GetByIDs(groupIDs []string) ([]*model.Group, error)
GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error)
GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error)
GetByUser(userID string) ([]*model.Group, error)
Update(group *model.Group) (*model.Group, error)
Delete(groupID string) (*model.Group, error)
Restore(groupID string) (*model.Group, error)
GetMemberUsers(groupID string) ([]*model.User, error)
GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error)
GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error)
GetMemberCount(groupID string) (int64, error)
GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error)
GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error)
UpsertMember(groupID string, userID string) (*model.GroupMember, error)
DeleteMember(groupID string, userID string) (*model.GroupMember, error)
PermanentDeleteMembersByUser(userID string) error
CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error)
GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error)
GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error)
UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error)
DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error)
// TeamMembersToAdd returns a slice of UserTeamIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given team.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be included; otherwise, they will be excluded.
TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error)
// ChannelMembersToAdd returns a slice of UserChannelIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given channel.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error)
// TeamMembersToRemove returns all team members that should be removed based on group constraints.
TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error)
// ChannelMembersToRemove returns all channel members that should be removed based on group constraints.
ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error)
GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error)
CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error)
GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error)
GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error)
CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error)
GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error)
TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error)
CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error)
ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error)
CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error)
// AdminRoleGroupsForSyncableMember returns the IDs of all of the groups that the user is a member of that are
// configured as SchemeAdmin: true for the given syncable.
AdminRoleGroupsForSyncableMember(userID, syncableID string, syncableType model.GroupSyncableType) ([]string, error)
// PermittedSyncableAdmins returns the IDs of all of the user who are permitted by the group syncable to have
// the admin role for the given syncable.
PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error)
// GroupCount returns the total count of records in the UserGroups table.
GroupCount() (int64, error)
GroupCountBySource(source model.GroupSource) (int64, error)
// GroupTeamCount returns the total count of records in the GroupTeams table.
GroupTeamCount() (int64, error)
// GroupChannelCount returns the total count of records in the GroupChannels table.
GroupChannelCount() (int64, error)
// GroupMemberCount returns the total count of records in the GroupMembers table.
GroupMemberCount() (int64, error)
// DistinctGroupMemberCount returns the count of records in the GroupMembers table with distinct userID values.
DistinctGroupMemberCount() (int64, error)
DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error)
// GroupCountWithAllowReference returns the count of records in the Groups table with AllowReference set to true.
GroupCountWithAllowReference() (int64, error)
UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error)
DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error)
GetMember(groupID string, userID string) (*model.GroupMember, error)
}
type LinkMetadataStore interface {
Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error)
Get(url string, timestamp int64) (*model.LinkMetadata, error)
}
type NotifyAdminStore interface {
Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error)
GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error)
Get(trial bool) ([]*model.NotifyAdminData, error)
DeleteBefore(trial bool, now int64) error
Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error
}
type SharedChannelStore interface {
Save(sc *model.SharedChannel) (*model.SharedChannel, error)
Get(channelId string) (*model.SharedChannel, error)
HasChannel(channelID string) (bool, error)
GetAll(offset, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error)
GetAllCount(opts model.SharedChannelFilterOpts) (int64, error)
Update(sc *model.SharedChannel) (*model.SharedChannel, error)
Delete(channelId string) (bool, error)
SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
GetRemote(id string) (*model.SharedChannelRemote, error)
HasRemote(channelID string, remoteId string) (bool, error)
GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error)
GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error)
GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error)
UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error
DeleteRemote(remoteId string) (bool, error)
GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error)
SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error)
GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error)
GetUsersForUser(userID string) ([]*model.SharedChannelUser, error)
GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error)
UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error
SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error)
UpsertAttachment(remote *model.SharedChannelAttachment) (string, error)
GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error)
UpdateAttachmentLastSyncAt(id string, syncTime int64) error
}
type PostPriorityStore interface {
GetForPost(postId string) (*model.PostPriority, error)
GetForPosts(ids []string) ([]*model.PostPriority, error)
}
type DraftStore interface {
Save(d *model.Draft) (*model.Draft, error)
Get(userID, channelID, rootID string, includeDeleted bool) (*model.Draft, error)
Delete(userID, channelID, rootID string) error
GetDraftsForUser(userID, teamID string) ([]*model.Draft, error)
Update(d *model.Draft) (*model.Draft, error)
}
type PostAcknowledgementStore interface {
Get(postID, userID string) (*model.PostAcknowledgement, error)
GetForPost(postID string) ([]*model.PostAcknowledgement, error)
GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error)
Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error)
Delete(acknowledgement *model.PostAcknowledgement) error
}
type TrueUpReviewStore interface {
GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error)
CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.
// IncludeDeleted will include channel records where DeleteAt != 0.
// ExcludeChannelNames will exclude channels from the results by name.
// IncludeSearchById will include searching matches against channel IDs in the results
// Paginate whether to paginate the results.
// Page page requested, if results are paginated.
// PerPage number of results per page, if paginated.
type ChannelSearchOpts struct {
Term string
NotAssociatedToGroup string
IncludeDeleted bool
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeTeamInfo bool
IncludeSearchById bool
CountOnly bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int
LastUpdateAt int
}
func (c *ChannelSearchOpts) IsPaginated() bool {
return c.Page != nil && c.PerPage != nil
}
type UserGetByIdsOpts struct {
// IsAdmin tracks whether or not the request is being made by an administrator. Does nothing when provided by a client.
IsAdmin bool
// Restrict to search in a list of teams and channels. Does nothing when provided by a client.
ViewRestrictions *model.ViewUsersRestrictions
// Since filters the users based on their UpdateAt timestamp.
Since int64
}
// ThreadMembershipOpts defines some properties to be passed to
// ThreadStore.MaintainMembership()
type ThreadMembershipOpts struct {
// Following indicates whether or not the user is following the thread.
Following bool
// IncrementMentions indicates whether or not the mentions count for
// the thread should be incremented.
IncrementMentions bool
// UpdateFollowing indicates whether or not the following state should be changed.
UpdateFollowing bool
// UpdateViewedTimestamp indicates whether or not the LastViewed field of the
// membership should be updated.
UpdateViewedTimestamp bool
// UpdateParticipants indicates whether or not the thread's participants list
// should be updated.
UpdateParticipants bool
}
// ChannelMemberGraphQLSearchOpts contains the options for a graphQL query
// to get the channel members.
type ChannelMemberGraphQLSearchOpts struct {
AfterChannel string
AfterUser string
Limit int
LastUpdateAt int
ExcludeTeam bool
}
// PostReminderMetadata contains some info needed to send
// the reminder message to the user.
type PostReminderMetadata struct {
ChannelId string
TeamName string
UserLocale string
Username string
}
// SidebarCategorySearchOpts contains the options for a graphQL query
// to get the sidebar categories.
type SidebarCategorySearchOpts struct {
TeamID string
ExcludeTeam bool
}
// Ensure store service adapter implements `product.StoreService`
var _ product.StoreService = (*StoreServiceAdapter)(nil)
// StoreServiceAdapter provides a simple Store wrapper for use with products.
type StoreServiceAdapter struct {
store Store
}
func NewStoreServiceAdapter(store Store) *StoreServiceAdapter {
return &StoreServiceAdapter{
store: store,
}
}
func (a *StoreServiceAdapter) GetMasterDB() *sql.DB {
return a.store.GetInternalMasterDB()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestAuditStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testAuditStore(t, ss) })
}
func testAuditStore(t *testing.T, ss store.Store) {
audit := &model.Audit{UserId: model.NewId(), IpAddress: "ipaddress", Action: "Action"}
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
audit.ExtraInfo = "extra"
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
audits, err := ss.Audit().Get(audit.UserId, 0, 100)
require.NoError(t, err)
assert.Len(t, audits, 4)
assert.Equal(t, "extra", audits[0].ExtraInfo)
audits, err = ss.Audit().Get("missing", 0, 100)
require.NoError(t, err)
assert.Empty(t, audits)
audits, err = ss.Audit().Get("", 0, 100)
require.NoError(t, err)
require.Len(t, audits, 4, "Failed to save and retrieve 4 audit logs")
require.NoError(t, ss.Audit().PermanentDeleteByUser(audit.UserId))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func makeBotWithUser(t *testing.T, ss store.Store, bot *model.Bot) (*model.Bot, *model.User) {
user, err := ss.User().Save(model.UserFromBot(bot))
require.NoError(t, err)
bot.UserId = user.Id
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
return bot, user
}
func TestBotStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Get", func(t *testing.T) { testBotStoreGet(t, ss, s) })
t.Run("GetAll", func(t *testing.T) { testBotStoreGetAll(t, ss, s) })
t.Run("Save", func(t *testing.T) { testBotStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testBotStoreUpdate(t, ss) })
t.Run("PermanentDelete", func(t *testing.T) { testBotStorePermanentDelete(t, ss) })
}
func testBotStoreGet(t *testing.T, ss store.Store, s SqlStore) {
deletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "deleted_bot",
Description: "A deleted bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
})
deletedBot.DeleteAt = 1
deletedBot, err := ss.Bot().Update(deletedBot)
require.NoError(t, err)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(deletedBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedBot.UserId)) }()
permanentlyDeletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "permanently_deleted_bot",
Description: "A permanently deleted bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
DeleteAt: 0,
})
require.NoError(t, ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId))
defer func() { require.NoError(t, ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }()
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
Description: "The first bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
Description: "The second bot",
OwnerId: model.NewId(),
LastIconUpdate: 0,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
// Artificially set b2.LastIconUpdate to NULL to verify handling of same.
_, sqlErr := s.GetMasterX().Exec("UPDATE Bots SET LastIconUpdate = NULL WHERE UserId = '" + b2.UserId + "'")
require.NoError(t, sqlErr)
t.Run("get non-existent bot", func(t *testing.T) {
_, err := ss.Bot().Get("unknown", false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get deleted bot", func(t *testing.T) {
_, err := ss.Bot().Get(deletedBot.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get deleted bot, include deleted", func(t *testing.T) {
bot, err := ss.Bot().Get(deletedBot.UserId, true)
require.NoError(t, err)
require.Equal(t, deletedBot, bot)
})
t.Run("get permanently deleted bot", func(t *testing.T) {
_, err := ss.Bot().Get(permanentlyDeletedBot.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get bot 1", func(t *testing.T) {
bot, err := ss.Bot().Get(b1.UserId, false)
require.NoError(t, err)
require.Equal(t, b1, bot)
})
t.Run("get bot 2", func(t *testing.T) {
bot, err := ss.Bot().Get(b2.UserId, false)
require.NoError(t, err)
require.Equal(t, b2, bot)
})
}
func testBotStoreGetAll(t *testing.T, ss store.Store, s SqlStore) {
OwnerId1 := model.NewId()
OwnerId2 := model.NewId()
deletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "deleted_bot",
Description: "A deleted bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
})
deletedBot.DeleteAt = 1
deletedBot, err := ss.Bot().Update(deletedBot)
require.NoError(t, err)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(deletedBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedBot.UserId)) }()
permanentlyDeletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "permanently_deleted_bot",
Description: "A permanently deleted bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
DeleteAt: 0,
})
require.NoError(t, ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId))
defer func() { require.NoError(t, ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }()
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
Description: "The first bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
Description: "The second bot",
OwnerId: OwnerId1,
LastIconUpdate: 0,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
// Artificially set b2.LastIconUpdate to NULL to verify handling of same.
_, sqlErr := s.GetMasterX().Exec("UPDATE Bots SET LastIconUpdate = NULL WHERE UserId = '" + b2.UserId + "'")
require.NoError(t, sqlErr)
t.Run("get original bots", func(t *testing.T) {
bot, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
}, bot)
})
b3, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b3",
Description: "The third bot",
OwnerId: OwnerId1,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b3.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b3.UserId)) }()
b4, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b4",
Description: "The fourth bot",
OwnerId: OwnerId2,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b4.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b4.UserId)) }()
deletedUser := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err1 := ss.User().Save(&deletedUser)
require.NoError(t, err1, "couldn't save user")
deletedUser.DeleteAt = model.GetMillis()
_, err2 := ss.User().Update(&deletedUser, true)
require.NoError(t, err2, "couldn't delete user")
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedUser.Id)) }()
ob5, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "ob5",
Description: "Orphaned bot 5",
OwnerId: deletedUser.Id,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b4.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b4.UserId)) }()
t.Run("get newly created bot stoo", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
b3,
b4,
ob5,
}, bots)
})
t.Run("get orphaned", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OnlyOrphaned: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
ob5,
}, bots)
})
t.Run("get page=0, per_page=2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
}, bots)
})
t.Run("get page=1, limit=2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b3,
b4,
}, bots)
})
t.Run("get page=5, perpage=1000", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 5, PerPage: 1000})
require.NoError(t, err)
require.Equal(t, []*model.Bot{}, bots)
})
t.Run("get offset=0, limit=2, include deleted", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2, IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
deletedBot,
b1,
}, bots)
})
t.Run("get offset=2, limit=2, include deleted", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2, IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, creator id 1", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId1})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, creator id 2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b4,
}, bots)
})
t.Run("get offset=0, limit=10, include deleted, creator id 1", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId1})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
deletedBot,
b1,
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, include deleted, creator id 2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b4,
}, bots)
})
}
func testBotStoreSave(t *testing.T, ss store.Store) {
t.Run("invalid bot", func(t *testing.T) {
bot := &model.Bot{
UserId: model.NewId(),
Username: "invalid bot",
Description: "description",
}
_, err := ss.Bot().Save(bot)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
// require.Equal(t, "model.bot.is_valid.username.app_error", err.Id)
})
t.Run("normal bot", func(t *testing.T) {
bot := &model.Bot{
Username: "normal_bot",
Description: "description",
OwnerId: model.NewId(),
}
user, err := ss.User().Save(model.UserFromBot(bot))
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
bot.UserId = user.Id
returnedNewBot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot.UserId)) }()
// Verify the returned bot matches the saved bot, modulo expected changes
require.NotEqual(t, 0, returnedNewBot.CreateAt)
require.NotEqual(t, 0, returnedNewBot.UpdateAt)
require.Equal(t, returnedNewBot.CreateAt, returnedNewBot.UpdateAt)
bot.UserId = returnedNewBot.UserId
bot.CreateAt = returnedNewBot.CreateAt
bot.UpdateAt = returnedNewBot.UpdateAt
bot.DeleteAt = 0
require.Equal(t, bot, returnedNewBot)
// Verify the actual bot in the database matches the saved bot.
actualNewBot, nErr := ss.Bot().Get(bot.UserId, false)
require.NoError(t, nErr)
require.Equal(t, bot, actualNewBot)
})
}
func testBotStoreUpdate(t *testing.T, ss store.Store) {
t.Run("invalid bot should fail to update", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
bot := existingBot.Clone()
bot.Username = "invalid username"
_, err := ss.Bot().Update(bot)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, "model.bot.is_valid.username.app_error", appErr.Id)
})
t.Run("existing bot should update", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
bot := existingBot.Clone()
bot.OwnerId = model.NewId()
bot.Description = "updated description"
bot.CreateAt = 999999 // Ignored
bot.UpdateAt = 999999 // Ignored
bot.LastIconUpdate = 100000 // Allowed
bot.DeleteAt = 100000 // Allowed
returnedBot, err := ss.Bot().Update(bot)
require.NoError(t, err)
// Verify the returned bot matches the updated bot, modulo expected timestamp changes
require.Equal(t, existingBot.CreateAt, returnedBot.CreateAt)
require.NotEqual(t, bot.UpdateAt, returnedBot.UpdateAt, "update should have advanced UpdateAt")
require.True(t, returnedBot.UpdateAt > bot.UpdateAt, "update should have advanced UpdateAt")
require.NotEqual(t, 99999, returnedBot.UpdateAt, "should have ignored user-provided UpdateAt")
require.Equal(t, bot.LastIconUpdate, returnedBot.LastIconUpdate, "should have marked icon as updated")
require.Equal(t, bot.DeleteAt, returnedBot.DeleteAt, "should have marked bot as deleted")
bot.CreateAt = returnedBot.CreateAt
bot.UpdateAt = returnedBot.UpdateAt
// Verify the actual (now deleted) bot in the database
actualBot, err := ss.Bot().Get(bot.UserId, true)
require.NoError(t, err)
require.Equal(t, bot, actualBot)
})
t.Run("deleted bot should update, restoring", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
existingBot.DeleteAt = 100000
existingBot, err := ss.Bot().Update(existingBot)
require.NoError(t, err)
bot := existingBot.Clone()
bot.DeleteAt = 0
returnedBot, err := ss.Bot().Update(bot)
require.NoError(t, err)
// Verify the returned bot matches the updated bot, modulo expected timestamp changes
require.EqualValues(t, 0, returnedBot.DeleteAt)
bot.UpdateAt = returnedBot.UpdateAt
// Verify the actual bot in the database
actualBot, err := ss.Bot().Get(bot.UserId, false)
require.NoError(t, err)
require.Equal(t, bot, actualBot)
})
}
func testBotStorePermanentDelete(t *testing.T, ss store.Store) {
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
t.Run("permanently delete a non-existent bot", func(t *testing.T) {
err := ss.Bot().PermanentDelete("unknown")
require.NoError(t, err)
})
t.Run("permanently delete bot", func(t *testing.T) {
err := ss.Bot().PermanentDelete(b1.UserId)
require.NoError(t, err)
_, err = ss.Bot().Get(b1.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"math"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestChannelMemberHistoryStore(t *testing.T, ss store.Store) {
t.Run("TestLogJoinEvent", func(t *testing.T) { testLogJoinEvent(t, ss) })
t.Run("TestLogLeaveEvent", func(t *testing.T) { testLogLeaveEvent(t, ss) })
t.Run("TestGetUsersInChannelAtChannelMemberHistory", func(t *testing.T) { testGetUsersInChannelAtChannelMemberHistory(t, ss) })
t.Run("TestGetUsersInChannelAtChannelMembers", func(t *testing.T) { testGetUsersInChannelAtChannelMembers(t, ss) })
t.Run("TestPermanentDeleteBatch", func(t *testing.T) { testPermanentDeleteBatch(t, ss) })
t.Run("TestPermanentDeleteBatchForRetentionPolicies", func(t *testing.T) { testPermanentDeleteBatchForRetentionPolicies(t, ss) })
t.Run("TestGetChannelsLeftSince", func(t *testing.T) { testGetChannelsLeftSince(t, ss) })
}
func testLogJoinEvent(t *testing.T, ss store.Store) {
// create a test channel
ch := model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(&ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// log a join event
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
}
func testLogLeaveEvent(t *testing.T, ss store.Store) {
// create a test channel
ch := model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(&ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// log a join event, followed by a leave event
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
}
func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
// create a test channel
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// the user was previously in the channel a long time ago, before the export period starts
// the existence of this record makes it look like the MessageExport feature has been active for awhile, and prevents
// us from looking in the ChannelMembers table for data that isn't found in the ChannelMemberHistory table
leaveTime := model.GetMillis() - 20000
joinTime := leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// log a join event
leaveTime = model.GetMillis()
joinTime = leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
require.NoError(t, err)
assert.Empty(t, channelMembers)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
// add a leave time for the user
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
require.NoError(t, err)
assert.Empty(t, channelMembers)
}
func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
// create a test channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// clear any existing ChannelMemberHistory data that might interfere with our test
var tableDataTruncated = false
for !tableDataTruncated {
var count int64
count, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis(), 1000, model.RetentionPolicyCursor{})
require.NoError(t, err, "Failed to truncate ChannelMemberHistory contents")
tableDataTruncated = count == int64(0)
}
// in this test, we're pretending that Message Export was not activated during the export period, so there's no data
// available in the ChannelMemberHistory table. Instead, we'll fall back to the ChannelMembers table for a rough approximation
joinTime := int64(1000)
leaveTime := joinTime + 5000
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// in every single case, the user will be included in the export, because ChannelMembers says they were in the channel at some point in
// the past, even though the time that they were actually in the channel doesn't necessarily overlap with the export period
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-500, channelMembers[0].JoinTime)
assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, leaveTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+200, *channelMembers[0].LeaveTime)
}
func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
// create a test channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
// and two test users
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
user2 := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
user2Ptr, err := ss.User().Save(&user2)
require.NoError(t, err)
user2 = *user2Ptr
// user1 joins and leaves the channel
leaveTime := model.GetMillis()
joinTime := leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// user2 joins the channel but never leaves
err = ss.ChannelMemberHistory().LogJoinEvent(user2.Id, channel.Id, joinTime)
require.NoError(t, err)
// in between the join time and the leave time, both users were members of the channel
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 2)
// the permanent delete should delete at least one record
rowsDeleted, _, err := ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, leaveTime+1, math.MaxInt64, model.RetentionPolicyCursor{})
require.NoError(t, err)
assert.NotEqual(t, int64(0), rowsDeleted)
// after the delete, there should be one less member in the channel
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, user2.Id, channelMembers[0].UserId)
}
func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userID := model.NewId()
joinTime := int64(1000)
leaveTime := int64(1500)
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, leaveTime)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := leaveTime + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id)
require.NoError(t, err)
require.Empty(t, result, "history should have been deleted by channel policy")
}
func testGetChannelsLeftSince(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userID := model.NewId()
joinTime := int64(1000)
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime)
require.NoError(t, err)
// has not left
ids, err := ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime)
require.NoError(t, err)
assert.Empty(t, ids)
// left
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+100)
require.NoError(t, err)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+200)
require.NoError(t, err)
assert.Empty(t, ids)
// joined and left again.
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime+200)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+300)
require.NoError(t, err)
// should be same for both time stamps
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+300)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"database/sql"
"encoding/json"
"errors"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
)
type SqlStore interface {
GetMasterX() SqlXExecutor
DriverName() string
}
type SqlXExecutor interface {
Get(dest any, query string, args ...any) error
NamedExec(query string, arg any) (sql.Result, error)
Exec(query string, args ...any) (sql.Result, error)
ExecRaw(query string, args ...any) (sql.Result, error)
NamedQuery(query string, arg any) (*sqlx.Rows, error)
QueryRowX(query string, args ...any) *sqlx.Row
QueryX(query string, args ...any) (*sqlx.Rows, error)
Select(dest any, query string, args ...any) error
}
func cleanupChannels(t *testing.T, ss store.Store) {
list, err := ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, err, "error cleaning all channels", err)
for _, channel := range list {
err = ss.Channel().PermanentDelete(channel.Id)
assert.NoError(t, err)
}
}
func channelToJSON(t *testing.T, channel *model.Channel) string {
t.Helper()
js, err := json.Marshal(channel)
require.NoError(t, err)
return string(js)
}
func channelMemberToJSON(t *testing.T, cm *model.ChannelMember) string {
t.Helper()
js, err := json.Marshal(cm)
require.NoError(t, err)
return string(js)
}
func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) })
t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss, s) })
t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) })
t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) })
t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) })
t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss, s) })
t.Run("GetMany", func(t *testing.T) { testChannelStoreGetMany(t, ss, s) })
t.Run("GetChannelsByIds", func(t *testing.T) { testChannelStoreGetChannelsByIds(t, ss) })
t.Run("GetChannelsWithTeamDataByIds", func(t *testing.T) { testGetChannelsWithTeamDataByIds(t, ss) })
t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) })
t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) })
t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) })
t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) })
t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) })
t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) })
t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) })
t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) })
t.Run("SaveMember", func(t *testing.T) { testChannelSaveMember(t, ss) })
t.Run("SaveMultipleMembers", func(t *testing.T) { testChannelSaveMultipleMembers(t, ss) })
t.Run("UpdateMember", func(t *testing.T) { testChannelUpdateMember(t, ss) })
t.Run("UpdateMemberNotifyProps", func(t *testing.T) { testChannelUpdateMemberNotifyProps(t, ss) })
t.Run("UpdateMultipleMembers", func(t *testing.T) { testChannelUpdateMultipleMembers(t, ss) })
t.Run("RemoveMember", func(t *testing.T) { testChannelRemoveMember(t, ss) })
t.Run("RemoveMembers", func(t *testing.T) { testChannelRemoveMembers(t, ss) })
t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) })
t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) })
t.Run("GetChannelsWithCursor", func(t *testing.T) { testChannelStoreGetChannelsWithCursor(t, ss) })
t.Run("GetChannelsByUser", func(t *testing.T) { testChannelStoreGetChannelsByUser(t, ss) })
t.Run("GetAllChannels", func(t *testing.T) { testChannelStoreGetAllChannels(t, ss, s) })
t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) })
t.Run("GetPrivateChannelsForTeam", func(t *testing.T) { testChannelStoreGetPrivateChannelsForTeam(t, ss) })
t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) })
t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) })
t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) })
t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) })
t.Run("GetMembersForUserWithCursor", func(t *testing.T) { testChannelStoreGetMembersForUserWithCursor(t, ss) })
t.Run("GetMembersForUserWithPagination", func(t *testing.T) { testChannelStoreGetMembersForUserWithPagination(t, ss) })
t.Run("CountPostsAfter", func(t *testing.T) { testCountPostsAfter(t, ss) })
t.Run("CountUrgentPostsAfter", func(t *testing.T) { testCountUrgentPostsAfter(t, ss) })
t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) })
t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) })
t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) })
t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) })
t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) })
t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) })
t.Run("GetMemberCountsByGroup", func(t *testing.T) { testGetMemberCountsByGroup(t, ss) })
t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, ss) })
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) })
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) })
t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss) })
t.Run("SearchArchivedInTeam", func(t *testing.T) { testChannelStoreSearchArchivedInTeam(t, ss, s) })
t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) })
t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) })
t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) })
t.Run("GetMembersByChannelIds", func(t *testing.T) { testChannelStoreGetMembersByChannelIds(t, ss) })
t.Run("GetMembersInfoByChannelIds", func(t *testing.T) { testChannelStoreGetMembersInfoByChannelIds(t, ss) })
t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) })
t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) })
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
t.Run("GetPinnedPostCount", func(t *testing.T) { testChannelStoreGetPinnedPostCount(t, ss) })
t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) })
t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) })
t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) })
t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) })
t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) })
t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) })
t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss, s) })
t.Run("ExportAllDirectChannels", func(t *testing.T) { testChannelStoreExportAllDirectChannels(t, ss, s) })
t.Run("ExportAllDirectChannelsExcludePrivateAndPublic", func(t *testing.T) { testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t, ss, s) })
t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, ss, s) })
t.Run("GetChannelsBatchForIndexing", func(t *testing.T) { testChannelStoreGetChannelsBatchForIndexing(t, ss) })
t.Run("GroupSyncedChannelCount", func(t *testing.T) { testGroupSyncedChannelCount(t, ss) })
t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, ss) })
t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, ss) })
t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, ss, s) })
t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, ss) })
t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, ss) })
t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, ss, s) })
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) })
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testChannelPostCountsByDuration(t, ss) })
t.Run("GetTopInactiveChannels", func(t *testing.T) { testGetTopInactiveChannels(t, ss) })
}
func testChannelStoreSave(t *testing.T, ss store.Store) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr, "couldn't save item", nErr)
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "shouldn't be able to update from save")
o1.Id = ""
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "should be unique name")
o1.Id = ""
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "should not be able to save direct channel")
o1 = model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr, "should have saved channel")
o2 := o1
o2.Id = ""
_, nErr = ss.Channel().Save(&o2, -1)
require.Error(t, nErr, "should have failed to save a duplicate channel")
var cErr *store.ErrConflict
require.True(t, errors.As(nErr, &cErr))
err := ss.Channel().Delete(o1.Id, 100)
require.NoError(t, err, "should have deleted channel")
o2.Id = ""
_, nErr = ss.Channel().Save(&o2, -1)
require.Error(t, nErr, "should have failed to save a duplicate of an archived channel")
require.True(t, errors.As(nErr, &cErr))
}
func testChannelStoreSaveDirectChannel(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.NoError(t, nErr, "couldn't save direct channel", nErr)
members, nErr := ss.Channel().GetMembers(o1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 2, "should have saved 2 members")
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.Error(t, nErr, "shouldn't be a able to update from save")
// Attempt to save a direct channel that already exists
o1a := model.Channel{
TeamId: o1.TeamId,
DisplayName: o1.DisplayName,
Name: o1.Name,
Type: o1.Type,
}
returnedChannel, nErr := ss.Channel().SaveDirectChannel(&o1a, &m1, &m2)
require.Error(t, nErr, "should've failed to save a duplicate direct channel")
var cErr *store.ErrConflict
require.Truef(t, errors.As(nErr, &cErr), "should've returned ChannelExistsError")
require.Equal(t, o1.Id, returnedChannel.Id, "should've failed to save a duplicate direct channel")
// Attempt to save a non-direct channel
o1.Id = ""
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.Error(t, nErr, "Should not be able to save non-direct channel")
// Save yourself Direct Message
o1.Id = ""
o1.DisplayName = "Myself"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m1)
require.NoError(t, nErr, "couldn't save direct channel", nErr)
members, nErr = ss.Channel().GetMembers(o1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 1, "should have saved just 1 member")
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreCreateDirectChannel(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr, "couldn't create direct channel", nErr)
defer func() {
ss.Channel().PermanentDeleteMembersByChannel(c1.Id)
ss.Channel().PermanentDelete(c1.Id)
}()
members, nErr := ss.Channel().GetMembers(c1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 2, "should have saved 2 members")
}
func testChannelStoreUpdate(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Name"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
_, err := ss.Channel().Update(&o1)
require.NoError(t, err, err)
o1.DeleteAt = 100
_, err = ss.Channel().Update(&o1)
require.Error(t, err, "update should have failed because channel is archived")
o1.DeleteAt = 0
o1.Id = "missing"
_, err = ss.Channel().Update(&o1)
require.Error(t, err, "Update should have failed because of missing key")
o2.Name = o1.Name
_, err = ss.Channel().Update(&o2)
require.Error(t, err, "update should have failed because of existing name")
}
func testGetChannelUnread(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m2 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
notifyPropsModel := model.GetDefaultChannelNotifyProps()
// Setup Channel 1
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Downtown", Type: model.ChannelTypeOpen, TotalMsgCount: 100, TotalMsgCountRoot: 99}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: notifyPropsModel, MsgCount: 90, MsgCountRoot: 80}
_, err := ss.Channel().SaveMember(cm1)
require.NoError(t, err)
// Setup Channel 2
c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Cultural", Type: model.ChannelTypeOpen, TotalMsgCount: 100, TotalMsgCountRoot: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: notifyPropsModel, MsgCount: 90, MsgCountRoot: 90, MentionCount: 5, MentionCountRoot: 1}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
// Check for Channel 1
ch, nErr := ss.Channel().GetChannelUnread(c1.Id, uid)
require.NoError(t, nErr, nErr)
require.Equal(t, c1.Id, ch.ChannelId, "Wrong channel id")
require.Equal(t, teamId1, ch.TeamId, "Wrong team id for channel 1")
require.NotNil(t, ch.NotifyProps, "wrong props for channel 1")
require.EqualValues(t, 0, ch.MentionCount, "wrong MentionCount for channel 1")
require.EqualValues(t, 10, ch.MsgCount, "wrong MsgCount for channel 1")
require.EqualValues(t, 19, ch.MsgCountRoot, "wrong MsgCountRoot for channel 1")
// Check for Channel 2
ch2, nErr := ss.Channel().GetChannelUnread(c2.Id, uid)
require.NoError(t, nErr, nErr)
require.Equal(t, c2.Id, ch2.ChannelId, "Wrong channel id")
require.Equal(t, teamId2, ch2.TeamId, "Wrong team id")
require.EqualValues(t, 5, ch2.MentionCount, "wrong MentionCount for channel 2")
require.EqualValues(t, 1, ch2.MentionCountRoot, "wrong MentionCountRoot for channel 2")
require.EqualValues(t, 10, ch2.MsgCount, "wrong MsgCount for channel 2")
}
func testChannelStoreGet(t *testing.T, ss store.Store, s SqlStore) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
c1, err := ss.Channel().Get(o1.Id, false)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, c1), "invalid returned channel")
_, err = ss.Channel().Get("", false)
require.Error(t, err, "missing id should have failed")
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Direct Name"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeDirect
m1 := model.ChannelMember{}
m1.ChannelId = o2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o2, &m1, &m2)
require.NoError(t, nErr)
c2, err := ss.Channel().Get(o2.Id, false)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, c2), "invalid returned channel")
c4, err := ss.Channel().Get(o2.Id, true)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, c4), "invalid returned channel")
channels, chanErr := ss.Channel().GetAll(o1.TeamId)
require.NoError(t, chanErr, chanErr)
require.Greater(t, len(channels), 0, "too little")
channelsTeam, err := ss.Channel().GetTeamChannels(o1.TeamId)
require.NoError(t, err, err)
require.Greater(t, len(channelsTeam), 0, "too little")
_, err = ss.Channel().GetTeamChannels("notfound")
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetMany(t *testing.T, ss store.Store, s SqlStore) {
o1, nErr := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
o2, nErr := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: "Name2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
res, err := ss.Channel().GetMany([]string{o1.Id, o2.Id}, true)
require.NoError(t, err)
assert.Len(t, res, 2)
res, err = ss.Channel().GetMany([]string{o1.Id, "notexists"}, true)
require.NoError(t, err)
assert.Len(t, res, 1)
_, err = ss.Channel().GetMany([]string{"notexists"}, true)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetChannelsByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = "aa" + model.NewId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Direct Name"
o2.Name = "bb" + model.NewId()
o2.Type = model.ChannelTypeDirect
o3 := model.Channel{}
o3.TeamId = model.NewId()
o3.DisplayName = "Deleted channel"
o3.Name = "cc" + model.NewId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 123)
require.NoError(t, nErr)
o3.DeleteAt = 123
o3.UpdateAt = 123
m1 := model.ChannelMember{}
m1.ChannelId = o2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o2, &m1, &m2)
require.NoError(t, nErr)
t.Run("Get 2 existing channels", func(t *testing.T) {
r1, err := ss.Channel().GetChannelsByIds([]string{o1.Id, o2.Id}, false)
require.NoError(t, err, err)
require.Len(t, r1, 2, "invalid returned channels, expected 2 and got "+strconv.Itoa(len(r1)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r1[0]))
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, r1[1]))
})
t.Run("Get 1 existing and 1 not existing channel", func(t *testing.T) {
nonexistentId := "abcd1234"
r2, err := ss.Channel().GetChannelsByIds([]string{o1.Id, nonexistentId}, false)
require.NoError(t, err, err)
require.Len(t, r2, 1, "invalid returned channels, expected 1 and got "+strconv.Itoa(len(r2)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r2[0]), "invalid returned channel")
})
t.Run("Get 2 existing and 1 deleted channel", func(t *testing.T) {
r1, err := ss.Channel().GetChannelsByIds([]string{o1.Id, o2.Id, o3.Id}, true)
require.NoError(t, err, err)
require.Len(t, r1, 3, "invalid returned channels, expected 3 and got "+strconv.Itoa(len(r1)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r1[0]))
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, r1[1]))
require.Equal(t, channelToJSON(t, &o3), channelToJSON(t, r1[2]))
})
}
func testGetChannelsWithTeamDataByIds(t *testing.T, ss store.Store) {
t1 := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err, "couldn't save item")
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Name"
c1.Name = "aa" + model.NewId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Direct Name"
c2.Name = "bb" + model.NewId()
c2.Type = model.ChannelTypeDirect
c3 := model.Channel{}
c3.TeamId = t1.Id
c3.DisplayName = "Deleted channel"
c3.Name = "cc" + model.NewId()
c3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(c3.Id, 123)
require.NoError(t, nErr)
c3.DeleteAt = 123
c3.UpdateAt = 123
m1 := model.ChannelMember{}
m1.ChannelId = c2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&c2, &m1, &m2)
require.NoError(t, nErr)
res, err := ss.Channel().GetChannelsWithTeamDataByIds([]string{c1.Id, c2.Id}, false)
require.NoError(t, err)
require.Len(t, res, 2)
assert.Equal(t, res[0].Id, c1.Id)
assert.Equal(t, res[0].TeamName, t1.Name)
assert.Equal(t, res[1].Id, c2.Id)
assert.Equal(t, res[1].TeamName, "")
}
func testChannelStoreGetForPost(t *testing.T, ss store.Store) {
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch, -1)
require.NoError(t, nErr)
p1, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
})
require.NoError(t, err)
channel, chanErr := ss.Channel().GetForPost(p1.Id)
require.NoError(t, chanErr, chanErr)
require.Equal(t, o1.Id, channel.Id, "incorrect channel returned")
}
func testChannelStoreRestore(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, err)
c, _ := ss.Channel().Get(o1.Id, false)
require.NotEqual(t, 0, c.DeleteAt, "should have been deleted")
err = ss.Channel().Restore(o1.Id, model.GetMillis())
require.NoError(t, err, err)
c, _ = ss.Channel().Get(o1.Id, false)
require.EqualValues(t, 0, c.DeleteAt, "should have been restored")
}
func testChannelStoreDelete(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{}
o4.TeamId = o1.TeamId
o4.DisplayName = "Channel4"
o4.Name = NewTestId()
o4.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
nErr = ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr, nErr)
c, _ := ss.Channel().Get(o1.Id, false)
require.NotEqual(t, 0, c.DeleteAt, "should have been deleted")
nErr = ss.Channel().Delete(o3.Id, model.GetMillis())
require.NoError(t, nErr, nErr)
list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 1, "invalid number of channels")
list, nErr = ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100)
require.NoError(t, nErr)
require.Len(t, list, 1, "invalid number of channels")
cresult := ss.Channel().PermanentDelete(o2.Id)
require.NoError(t, cresult)
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if assert.Error(t, nErr) {
var nfErr *store.ErrNotFound
require.True(t, errors.As(nErr, &nfErr))
} else {
require.Equal(t, model.ChannelList{}, list)
}
nErr = ss.Channel().PermanentDeleteByTeam(o1.TeamId)
require.NoError(t, nErr, nErr)
}
func testChannelStoreGetByName(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
result, err := ss.Channel().GetByName(o1.TeamId, o1.Name, true)
require.NoError(t, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, result), "invalid returned channel")
channelID := result.Id
_, err = ss.Channel().GetByName(o1.TeamId, "", true)
require.Error(t, err, "Missing id should have failed")
result, err = ss.Channel().GetByName(o1.TeamId, o1.Name, false)
require.NoError(t, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, result), "invalid returned channel")
_, err = ss.Channel().GetByName(o1.TeamId, "", false)
require.Error(t, err, "Missing id should have failed")
nErr = ss.Channel().Delete(channelID, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
_, err = ss.Channel().GetByName(o1.TeamId, o1.Name, false)
require.Error(t, err, "Deleted channel should not be returned by GetByName()")
}
func testChannelStoreGetByNames(t *testing.T, ss store.Store) {
o1 := model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: o1.TeamId,
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
for index, tc := range []struct {
TeamId string
Names []string
ExpectedIds []string
}{
{o1.TeamId, []string{o1.Name}, []string{o1.Id}},
{o1.TeamId, []string{o1.Name, o2.Name}, []string{o1.Id, o2.Id}},
{o1.TeamId, nil, nil},
{o1.TeamId, []string{"foo"}, nil},
{o1.TeamId, []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}},
{"", []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}},
{"asd", []string{o1.Name, "foo", o2.Name, o2.Name}, nil},
} {
var channels []*model.Channel
channels, err := ss.Channel().GetByNames(tc.TeamId, tc.Names, true)
require.NoError(t, err)
var ids []string
for _, channel := range channels {
ids = append(ids, channel.Id)
}
sort.Strings(ids)
sort.Strings(tc.ExpectedIds)
assert.Equal(t, tc.ExpectedIds, ids, "tc %v", index)
}
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
err = ss.Channel().Delete(o2.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
channels, nErr := ss.Channel().GetByNames(o1.TeamId, []string{o1.Name}, false)
require.NoError(t, nErr)
assert.Empty(t, channels)
}
func testChannelStoreGetDeletedByName(t *testing.T, ss store.Store) {
o1 := &model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
now := model.GetMillis()
err := ss.Channel().Delete(o1.Id, now)
require.NoError(t, err, "channel should have been deleted")
o1.DeleteAt = now
o1.UpdateAt = now
r1, nErr := ss.Channel().GetDeletedByName(o1.TeamId, o1.Name)
require.NoError(t, nErr)
require.Equal(t, o1, r1)
_, nErr = ss.Channel().GetDeletedByName(o1.TeamId, "")
require.Error(t, nErr, "missing id should have failed")
}
func testChannelStoreGetDeleted(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
userId := model.NewId()
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
list, nErr := ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list")
require.Equal(t, o1.Name, list[0].Name, "missing channel")
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list")
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
err = ss.Channel().Delete(o3.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 2, "wrong list length")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 1, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list length")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 1, 1, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list length")
}
func testChannelMemberStore(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "NameName"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c1t1, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o1)
require.NoError(t, nErr)
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o2)
require.NoError(t, nErr)
c1t2, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count, nErr := ss.Channel().GetMemberCount(o1.ChannelId, true)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, true)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
require.EqualValues(
t,
2,
ss.Channel().GetMemberCountFromCache(o1.ChannelId),
"should have saved 2 members")
require.EqualValues(
t,
0,
ss.Channel().GetMemberCountFromCache("junk"),
"should have saved 0 members")
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
nErr = ss.Channel().RemoveMember(o2.ChannelId, o2.UserId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 1, count, "should have removed 1 member")
c1t3, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t3.ExtraUpdateAt, "ExtraUpdateAt should be 0")
member, _ := ss.Channel().GetMember(context.Background(), o1.ChannelId, o1.UserId)
require.Equal(t, o1.ChannelId, member.ChannelId, "should have go member")
_, nErr = ss.Channel().SaveMember(&o1)
require.Error(t, nErr, "should have been a duplicate")
c1t4, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t4.ExtraUpdateAt, "ExtraUpdateAt should be 0")
}
func testChannelSaveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("not valid channel member", func(t *testing.T) {
member := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMember(m1)
require.NoError(t, nErr)
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr = ss.Channel().SaveMember(m2)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert member correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelSaveMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("any not valid channel member", func(t *testing.T) {
m1 := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: model.NewId(), UserId: u2.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
members, err := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("not valid channel member", func(t *testing.T) {
member := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().UpdateMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("insert member correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("any not valid channel member", func(t *testing.T) {
m1 := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: model.NewId(), UserId: u2.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.ChannelMember
members, nErr = ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.ChannelMember
members, nErr = ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
members, err := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
members, err := ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMemberNotifyProps(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
props := member.NotifyProps
props["hello"] = "world"
props[model.DesktopNotifyProp] = model.ChannelNotifyAll
member, nErr = ss.Channel().UpdateMemberNotifyProps(member.ChannelId, member.UserId, props)
require.NoError(t, nErr)
// Verify props.
assert.Equal(t, props, member.NotifyProps)
}
func testChannelRemoveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
channelID := model.NewId()
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
m1 := &model.ChannelMember{ChannelId: channelID, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID, UserId: u2.Id, NotifyProps: defaultNotifyProps}
m3 := &model.ChannelMember{ChannelId: channelID, UserId: u3.Id, NotifyProps: defaultNotifyProps}
m4 := &model.ChannelMember{ChannelId: channelID, UserId: u4.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3, m4})
require.NoError(t, nErr)
t.Run("remove member from not existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember("not-existing-channel", u1.Id)
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing member from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember(channelID, model.NewId())
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove existing member from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember(channelID, u1.Id)
require.NoError(t, nErr)
defer ss.Channel().SaveMember(m1)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(3), membersCount)
})
}
func testChannelRemoveMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
channelID := model.NewId()
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
m1 := &model.ChannelMember{ChannelId: channelID, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID, UserId: u2.Id, NotifyProps: defaultNotifyProps}
m3 := &model.ChannelMember{ChannelId: channelID, UserId: u3.Id, NotifyProps: defaultNotifyProps}
m4 := &model.ChannelMember{ChannelId: channelID, UserId: u4.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3, m4})
require.NoError(t, nErr)
t.Run("remove members from not existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers("not-existing-channel", []string{u1.Id, u2.Id, u3.Id, u4.Id})
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{model.NewId(), model.NewId()})
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing and not existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{u1.Id, u2.Id, model.NewId(), model.NewId()})
require.NoError(t, nErr)
defer ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(2), membersCount)
})
t.Run("remove existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{u1.Id, u2.Id, u3.Id})
require.NoError(t, nErr)
defer ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3})
membersCount, err := ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, err)
require.Equal(t, int64(1), membersCount)
})
}
func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "NameName"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c1t1, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o1)
require.NoError(t, nErr)
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o2)
require.NoError(t, nErr)
c1t2, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count, nErr := ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
nErr = ss.Channel().PermanentDeleteMembersByUser(o2.UserId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 1, count, "should have removed 1 member")
nErr = ss.Channel().PermanentDeleteMembersByChannel(o1.ChannelId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 0, count, "should have removed all members")
}
func testChannelStoreGetChannels(t *testing.T, ss store.Store) {
team := model.NewId()
o1 := &model.Channel{}
o1.TeamId = team
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
var nErr error
o1, nErr = ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = team
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = team
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
ids, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, false, false)
require.NoError(t, err)
_, ok := ids[o1.Id]
require.True(t, ok, "missing channel")
ids2, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, false)
require.NoError(t, err)
_, ok = ids2[o1.Id]
require.True(t, ok, "missing channel")
ids3, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, false)
require.NoError(t, err)
_, ok = ids3[o1.Id]
require.True(t, ok, "missing channel")
ids4, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, true)
require.NoError(t, err)
_, ok = ids4[o1.Id]
require.True(t, ok, "missing channel")
// Sleeping to guarantee that the
// UpdateAt is different.
// The proper way would be to set UpdateAt during channel creation itself,
// but the *Channel.PreSave method ignores any existing CreateAt value.
// TODO: check if using an existing CreateAt breaks anything.
time.Sleep(time.Millisecond)
now := model.GetMillis()
_, nErr = ss.Channel().Update(o1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastUpdateAt: int(now),
})
require.NoError(t, nErr)
// should return 1
require.Len(t, list, 1)
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
// Should return all
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
// Should still return all
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 10,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
// Should return 2
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 20,
})
require.NoError(t, nErr)
require.Len(t, list, 2)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o3.Id, list[1].Id, "missing channel")
require.True(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, o1.Id),
"missing channel")
require.True(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, o2.Id),
"missing channel")
require.False(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, "blahblah"),
"missing channel")
require.False(
t,
ss.Channel().IsUserInChannelUseCache("blahblah", "blahblah"),
"missing channel")
ss.Channel().InvalidateAllChannelMembersForUser(m1.UserId)
}
func testChannelStoreGetChannelsWithCursor(t *testing.T, ss store.Store) {
teamID := model.NewId()
o1 := &model.Channel{}
o1.TeamId = teamID
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
var nErr error
o1, nErr = ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = teamID
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = teamID
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
require.Equal(t, teamID, list[0].TeamId, "incorrect teamID")
require.Equal(t, teamID, list[1].TeamId, "incorrect teamID")
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, list[1].Id)
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, teamID, list[0].TeamId, "incorrect teamID")
// all channels should be returned
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
// should return empty list
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, list[2].Id)
require.NoError(t, nErr)
require.Len(t, list, 0)
// Sleeping to guarantee that the
// UpdateAt is different.
// The proper way would be to set UpdateAt during channel creation itself,
// but the *Channel.PreSave method ignores any existing CreateAt value.
// TODO: check if using an existing CreateAt breaks anything.
time.Sleep(time.Millisecond)
now := model.GetMillis()
_, nErr = ss.Channel().Update(o1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastUpdateAt: int(now),
}, "")
require.NoError(t, nErr)
// should return 1
require.Len(t, list, 1)
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 1)
// Should return all
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, list[1].Id)
require.NoError(t, nErr)
require.Len(t, list, 1)
// Should still return all
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 10,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
// Should return 2
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 20,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
}
func testChannelStoreGetChannelsByUser(t *testing.T, ss store.Store) {
team := model.NewId()
team2 := model.NewId()
o1 := model.Channel{}
o1.TeamId = team
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = team
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = team2
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannelsByUser(m1.UserId, false, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, false, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
// Should return all
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
// Should still return all
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 10, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
// Should return 2
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 20, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
require.ElementsMatch(t, []string{o1.Id, o3.Id}, []string{list[0].Id, list[1].Id}, "channels did not match")
}
func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlStore) {
cleanupChannels(t, ss)
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Name2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1" + model.NewId()
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(group)
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, c1.Id, true))
require.NoError(t, err)
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Channel2" + model.NewId()
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
c2.DeleteAt = model.GetMillis()
c2.UpdateAt = c2.DeleteAt
nErr = ss.Channel().Delete(c2.Id, c2.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
c3 := model.Channel{}
c3.TeamId = t2.Id
c3.DisplayName = "Channel3" + model.NewId()
c3.Name = NewTestId()
c3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, nErr)
userIds := []string{model.NewId(), model.NewId(), model.NewId()}
c5 := model.Channel{}
c5.Name = model.GetGroupNameFromUserIds(userIds)
c5.DisplayName = "GroupChannel" + model.NewId()
c5.Name = NewTestId()
c5.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&c5, -1)
require.NoError(t, nErr)
list, nErr := ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{})
require.NoError(t, nErr)
assert.Len(t, list, 2)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
assert.Equal(t, c3.Id, list[1].Id)
assert.Equal(t, "Name2", list[1].TeamDisplayName)
count1, nErr := ss.Channel().GetAllChannelsCount(store.ChannelSearchOpts{})
require.NoError(t, nErr)
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
assert.Len(t, list, 3)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
assert.Equal(t, c2.Id, list[1].Id)
assert.Equal(t, c3.Id, list[2].Id)
count2, nErr := ss.Channel().GetAllChannelsCount(store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
require.True(t, func() bool {
return count2 > count1
}())
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
// Not associated to group
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{NotAssociatedToGroup: group.Id})
require.NoError(t, nErr)
assert.Len(t, list, 1)
// Exclude channel names
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludeChannelNames: []string{c1.Name}})
require.NoError(t, nErr)
assert.Len(t, list, 1)
// Exclude policy constrained
policy, nErr := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, nErr)
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludePolicyConstrained: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c3.Id, list[0].Id)
// Without the policy ID
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Nil(t, list[0].PolicyID)
// With the policy ID
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludePolicyID: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, *list[0].PolicyID, policy.ID)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetMoreChannels(t *testing.T, ss store.Store) {
teamId := model.NewId()
otherTeamId := model.NewId()
userId := model.NewId()
otherUserId1 := model.NewId()
otherUserId2 := model.NewId()
// o1 is a channel on the team to which the user (and the other user 1) belongs
o1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: otherUserId1,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// o2 is a channel on the other team to which the user belongs
o2 := model.Channel{
TeamId: otherTeamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o2.Id,
UserId: otherUserId2,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// o3 is a channel on the team to which the user does not belong, and thus should show up
// in "more channels"
o3 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
// o4 is a private channel on the team to which the user does not belong
o4 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
// o5 is another private channel on the team to which the user does belong
o5 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o5.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("only o3 listed in more channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetMoreChannels(teamId, userId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o3}, list)
})
// o6 is another channel on the team to which the user does not belong, and would thus
// start showing up in "more channels".
o6 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelD",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
// o7 is another channel on the team to which the user does not belong, but is deleted,
// and thus would not start showing up in "more channels"
o7 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelD",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o7.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
t.Run("both o3 and o6 listed in more channels", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3, &o6}, list)
})
t.Run("only o3 listed in more channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3}, list)
})
t.Run("only o6 listed in more channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o6}, list)
})
t.Run("verify analytics for open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 4, count)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 2, count)
})
t.Run("verify analytics for all channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, "")
require.NoError(t, err)
require.EqualValues(t, 6, count)
})
}
func testChannelStoreGetPrivateChannelsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// p1 is a private channel on the team
p1 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr := ss.Channel().Save(&p1, -1)
require.NoError(t, nErr)
// p2 is a private channel on another team
p2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "PrivateChannel1Team2",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p2, -1)
require.NoError(t, nErr)
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
t.Run("only p1 initially listed in private channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&p1}, list)
})
// p3 is another private channel on the team
p3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel2Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p3, -1)
require.NoError(t, nErr)
// p4 is another private, but deleted channel on the team
p4 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p4, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(p4.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("both p1 and p3 listed in private channels", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p1, &p3}, list)
})
t.Run("only p1 listed in private channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p1}, list)
})
t.Run("only p3 listed in private channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p3}, list)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 3, count)
})
t.Run("verify analytics for open open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 1, count)
})
}
func testChannelStoreGetPublicChannelsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
// o2 is a public channel on another team
o2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "OpenChannel1Team2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
// o3 is a private channel on the team
o3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
t.Run("only o1 initially listed in public channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1}, list)
})
// o4 is another public channel on the team
o4 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel2Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
// o5 is another public, but deleted channel on the team
o5 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o5.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("both o1 and o4 listed in public channels", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o1, &o4}, list)
})
t.Run("only o1 listed in public channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o1}, list)
})
t.Run("only o4 listed in public channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o4}, list)
})
t.Run("verify analytics for open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 3, count)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 1, count)
})
}
func testChannelStoreGetPublicChannelsByIdsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// oc1 is a public channel on the team
oc1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&oc1, -1)
require.NoError(t, nErr)
// oc2 is a public channel on another team
oc2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "OpenChannel2TeamOther",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc2, -1)
require.NoError(t, nErr)
// pc3 is a private channel on the team
pc3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&pc3, -1)
require.NoError(t, nErr)
t.Run("oc1 by itself should be found as a public channel in the team", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id})
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&oc1}, list)
})
t.Run("only oc1, among others, should be found as a public channel in the team", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id})
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&oc1}, list)
})
// oc4 is another public channel on the team
oc4 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel4Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc4, -1)
require.NoError(t, nErr)
// oc4 is another public, but deleted channel on the team
oc5 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel4Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc5, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(oc5.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("only oc1 and oc4, among others, should be found as a public channel in the team", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id, oc4.Id})
require.NoError(t, err)
require.Equal(t, model.ChannelList{&oc1, &oc4}, list)
})
t.Run("random channel id should not be found as a public channel in the team", func(t *testing.T) {
_, err := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{model.NewId()})
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testChannelStoreGetChannelCounts(t *testing.T, ss store.Store) {
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
counts, _ := ss.Channel().GetChannelCounts(o1.TeamId, m1.UserId)
require.Len(t, counts.Counts, 1, "wrong number of counts")
require.Len(t, counts.UpdateTimes, 1, "wrong number of update times")
}
func testChannelStoreGetMembersForUser(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
o1 := model.Channel{}
o1.TeamId = t1.Id
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
t.Run("with channels", func(t *testing.T) {
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 2)
})
t.Run("with channels and direct messages", func(t *testing.T) {
user := model.User{Id: m1.UserId}
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
u3 := model.User{Id: model.NewId()}
u4 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
_, nErr = ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
// other user direct message
_, nErr = ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 4)
})
t.Run("with channels, direct channels and group messages", func(t *testing.T) {
userIds := []string{model.NewId(), model.NewId(), model.NewId(), m1.UserId}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIds),
DisplayName: "test",
Type: model.ChannelTypeGroup,
}
var channel *model.Channel
channel, nErr = ss.Channel().Save(group, 10000)
require.NoError(t, nErr)
for _, userId := range userIds {
cm := &model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeUser: true,
}
_, err = ss.Channel().SaveMember(cm)
require.NoError(t, err)
}
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 5)
})
}
func testChannelStoreGetMembersForUserWithCursor(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Team1"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Team2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o1 := model.Channel{}
o1.TeamId = t1.Id
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = t2.Id
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o3.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
t.Run("with channels", func(t *testing.T) {
var members model.ChannelMembers
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 1,
}
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 1)
opts.Limit = 3
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 3)
opts.AfterChannel = members[0].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 1
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 1)
})
t.Run("with channels and direct messages", func(t *testing.T) {
user := model.User{Id: m1.UserId}
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
u3 := model.User{Id: model.NewId()}
u4 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
_, nErr = ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
// other user direct message
_, nErr = ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 5)
opts.Limit = 2
members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
opts.AfterChannel = members[1].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 2
members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
})
t.Run("for a specific team", func(t *testing.T) {
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, t2.Id, opts)
require.NoError(t, err2)
assert.Len(t, members, 3)
})
t.Run("excluding a team", func(t *testing.T) {
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
ExcludeTeam: true,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, t2.Id, opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
})
t.Run("with channels, direct channels and group messages", func(t *testing.T) {
userIds := []string{model.NewId(), model.NewId(), model.NewId(), m1.UserId}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIds),
DisplayName: "test",
Type: model.ChannelTypeGroup,
}
var channel *model.Channel
channel, nErr = ss.Channel().Save(group, 10000)
require.NoError(t, nErr)
for _, userId := range userIds {
cm := &model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeUser: true,
}
_, err = ss.Channel().SaveMember(cm)
require.NoError(t, err)
}
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 6)
opts.Limit = 2
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 2)
opts.AfterChannel = members[1].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 10
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 4)
})
}
func testChannelStoreGetMembersForUserWithPagination(t *testing.T, ss store.Store) {
t1 := model.Team{
DisplayName: "team1",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
o1 := model.Channel{
TeamId: t1.Id,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o1, -1)
require.NoError(t, err)
t2 := model.Team{
DisplayName: "team2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o2 := model.Channel{
TeamId: t2.Id,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o2, -1)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
members, err := ss.Channel().GetMembersForUserWithPagination(m1.UserId, 0, 2)
require.NoError(t, err)
assert.Len(t, members, 2)
teamNames := make([]string, 0, 2)
for _, member := range members {
teamNames = append(teamNames, member.TeamDisplayName)
}
assert.ElementsMatch(t, teamNames, []string{t1.DisplayName, t2.DisplayName})
members, err = ss.Channel().GetMembersForUserWithPagination(m1.UserId, 1, 1)
require.NoError(t, err)
assert.Len(t, members, 1)
}
func testCountPostsAfter(t *testing.T, ss store.Store) {
t.Run("should count all posts with or without the given user ID", func(t *testing.T) {
userId1 := model.NewId()
userId2 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId2,
ChannelId: channelId,
CreateAt: 1002,
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 3, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, userId1)
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, userId1)
require.NoError(t, err)
assert.Equal(t, 1, count)
})
t.Run("should not count deleted posts", func(t *testing.T) {
userId1 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
DeleteAt: 1001,
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 0, count)
})
t.Run("should count system/bot messages, but not join/leave messages", func(t *testing.T) {
userId1 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
Type: model.PostTypeJoinChannel,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1002,
Type: model.PostTypeRemoveFromChannel,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1003,
Type: model.PostTypeLeaveTeam,
})
require.NoError(t, err)
p5, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1004,
Type: model.PostTypeHeaderChange,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1005,
Type: "custom_nps_survey",
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 3, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p5.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p5.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
})
}
func testCountUrgentPostsAfter(t *testing.T, ss store.Store) {
t.Run("should count all posts with or without the given user ID", func(t *testing.T) {
userId1 := model.NewId()
userId2 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId2,
ChannelId: channelId,
CreateAt: 1002,
})
require.NoError(t, err)
count, err := ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 0, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, userId1)
require.NoError(t, err)
assert.Equal(t, 1, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, userId1)
require.NoError(t, err)
assert.Equal(t, 0, count)
})
}
func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
o1.TotalMsgCount = 25
o1.LastPostAt = 12345
o1.LastRootPostAt = 12345
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel1"
o2.Name = NewTestId() + "c"
o2.Type = model.ChannelTypeOpen
o2.TotalMsgCount = 26
o2.LastPostAt = 123456
o2.LastRootPostAt = 123456
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
var times map[string]int64
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId)
require.NoError(t, err, "failed to update ", err)
require.Equal(t, o1.LastPostAt, times[o1.Id], "last viewed at time incorrect")
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId)
require.NoError(t, err, "failed to update ", err)
require.Equal(t, o2.LastPostAt, times[o2.Id], "last viewed at time incorrect")
rm1, err := ss.Channel().GetMember(context.Background(), m1.ChannelId, m1.UserId)
assert.NoError(t, err)
assert.Equal(t, o1.LastPostAt, rm1.LastViewedAt)
assert.Equal(t, o1.LastPostAt, rm1.LastUpdateAt)
assert.Equal(t, o1.TotalMsgCount, rm1.MsgCount)
rm2, err := ss.Channel().GetMember(context.Background(), m2.ChannelId, m2.UserId)
assert.NoError(t, err)
assert.Equal(t, o2.LastPostAt, rm2.LastViewedAt)
assert.Equal(t, o2.LastPostAt, rm2.LastUpdateAt)
assert.Equal(t, o2.TotalMsgCount, rm2.MsgCount)
_, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id")
require.NoError(t, err, "failed to update")
}
func testChannelStoreIncrementMentionCount(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
o1.TotalMsgCount = 25
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{m1.UserId}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{"missing id"}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", []string{m1.UserId}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", []string{"missing id"}, false, false)
require.NoError(t, err, "failed to update")
}
func testUpdateChannelMember(t *testing.T, ss store.Store) {
userId := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
m1.NotifyProps["test"] = "sometext"
_, err = ss.Channel().UpdateMember(m1)
require.NoError(t, err, err)
m1.UserId = ""
_, err = ss.Channel().UpdateMember(m1)
require.Error(t, err, "bad user id - should fail")
}
func testGetMember(t *testing.T, ss store.Store) {
userId := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{
TeamId: c1.TeamId,
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
m2 := &model.ChannelMember{
ChannelId: c2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
_, err = ss.Channel().GetMember(context.Background(), model.NewId(), userId)
require.Error(t, err, "should've failed to get member for non-existent channel")
_, err = ss.Channel().GetMember(context.Background(), c1.Id, model.NewId())
require.Error(t, err, "should've failed to get member for non-existent user")
member, err := ss.Channel().GetMember(context.Background(), c1.Id, userId)
require.NoError(t, err, "shouldn't have errored when getting member", err)
require.Equal(t, c1.Id, member.ChannelId, "should've gotten member of channel 1")
require.Equal(t, userId, member.UserId, "should've have gotten member for user")
member, err = ss.Channel().GetMember(context.Background(), c2.Id, userId)
require.NoError(t, err, "shouldn't have errored when getting member", err)
require.Equal(t, c2.Id, member.ChannelId, "should've gotten member of channel 2")
require.Equal(t, userId, member.UserId, "should've gotten member for user")
props, err := ss.Channel().GetAllChannelMembersNotifyPropsForChannel(c2.Id, false)
require.NoError(t, err, err)
require.NotEqual(t, 0, len(props), "should not be empty")
props, err = ss.Channel().GetAllChannelMembersNotifyPropsForChannel(c2.Id, true)
require.NoError(t, err, err)
require.NotEqual(t, 0, len(props), "should not be empty")
ss.Channel().InvalidateCacheForChannelMembersNotifyProps(c2.Id)
}
func testChannelStoreGetMemberForPost(t *testing.T, ss store.Store) {
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch, -1)
require.NoError(t, nErr)
m1, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
p1, nErr := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
})
require.NoError(t, nErr)
r1, err := ss.Channel().GetMemberForPost(p1.Id, m1.UserId)
require.NoError(t, err, err)
require.Equal(t, channelMemberToJSON(t, m1), channelMemberToJSON(t, r1), "invalid returned channel member")
_, err = ss.Channel().GetMemberForPost(p1.Id, model.NewId())
require.Error(t, err, "shouldn't have returned a member")
}
func testGetMemberCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{
TeamId: teamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
u1 := &model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetMemberCount(c1.Id, false)
require.NoError(t, channelErr, "failed to get member count", channelErr)
require.EqualValuesf(t, 1, count, "got incorrect member count %v", count)
u2 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
count, channelErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoErrorf(t, channelErr, "failed to get member count: %v", channelErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
// make sure members of other channels aren't counted
u3 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
count, channelErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoErrorf(t, channelErr, "failed to get member count: %v", channelErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
// make sure inactive users aren't counted
u4 := &model.User{
Email: MakeEmail(),
DeleteAt: 10000,
}
_, err = ss.User().Save(u4)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
m4 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m4)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoError(t, nErr, "failed to get member count", nErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
}
func testGetMemberCountsByGroup(t *testing.T, ss store.Store) {
var memberCounts []*model.ChannelMemberCountByGroup
teamId := model.NewId()
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err := ss.Group().Create(g1)
require.NoError(t, err)
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := &model.User{
Timezone: timezones.DefaultUserTimezone(),
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
t.Run("empty slice for channel with no groups", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
t.Run("returns memberCountsByGroup without timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
t.Run("returns memberCountsByGroup with timezones when no timezones set", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, true)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
// create 5 different users with 2 different timezones for group 2
for i := 1; i <= 5; i++ {
timeZone := timezones.DefaultUserTimezone()
if i == 1 {
timeZone["manualTimezone"] = "EDT"
timeZone["useAutomaticTimezone"] = "false"
}
u := &model.User{
Timezone: timeZone,
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, -1)
require.NoError(t, nErr)
m := model.ChannelMember{
ChannelId: c1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g2.Id, u.Id)
require.NoError(t, err)
}
g3 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g3)
require.NoError(t, err)
// create 10 different users with 3 different timezones for group 3
for i := 1; i <= 10; i++ {
timeZone := timezones.DefaultUserTimezone()
if i == 1 || i == 2 {
timeZone["manualTimezone"] = "EDT"
timeZone["useAutomaticTimezone"] = "false"
} else if i == 3 || i == 4 {
timeZone["manualTimezone"] = "PST"
timeZone["useAutomaticTimezone"] = "false"
} else if i == 5 {
timeZone["autoTimezone"] = "CET"
timeZone["useAutomaticTimezone"] = "true"
} else if i == 6 {
timeZone["automaticTimezone"] = "CET"
timeZone["useAutomaticTimezone"] = "true"
} else {
// Give every user with auto timezone set to true a random manual timezone to ensure that manual timezone is not looked at if auto is set
timeZone["useAutomaticTimezone"] = "true"
timeZone["manualTimezone"] = "PST" + utils.RandomName(utils.Range{Begin: 5, End: 5}, utils.ALPHANUMERIC)
}
u := &model.User{
Timezone: timeZone,
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, -1)
require.NoError(t, nErr)
m := model.ChannelMember{
ChannelId: c1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g3.Id, u.Id)
require.NoError(t, err)
}
t.Run("returns memberCountsByGroup for multiple groups with lots of users without timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g2.Id,
ChannelMemberCount: 5,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g3.Id,
ChannelMemberCount: 10,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.ElementsMatch(t, expectedMemberCounts, memberCounts)
})
t.Run("returns memberCountsByGroup for multiple groups with lots of users with timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, true)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g2.Id,
ChannelMemberCount: 5,
ChannelMemberTimezonesCount: 1,
},
{
GroupId: g3.Id,
ChannelMemberCount: 10,
ChannelMemberTimezonesCount: 3,
},
}
require.NoError(t, nErr)
require.ElementsMatch(t, expectedMemberCounts, memberCounts)
})
}
func testGetGuestCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{
TeamId: teamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
t.Run("Regular member doesn't count", func(t *testing.T) {
u1 := &model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemUserRoleId,
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(0), count)
})
t.Run("Guest member does count", func(t *testing.T) {
u2 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
t.Run("make sure members of other channels aren't counted", func(t *testing.T) {
u3 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(&u3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
t.Run("make sure inactive users aren't counted", func(t *testing.T) {
u4 := &model.User{
Email: MakeEmail(),
DeleteAt: 10000,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(u4)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
m4 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m4)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
}
func testChannelStoreSearchMore(t *testing.T, ss store.Store) {
teamId := model.NewId()
otherTeamId := model.NewId()
o1 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
o2 := model.Channel{
TeamId: otherTeamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
o8 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
o9 := model.Channel{
TeamId: teamId,
DisplayName: "Channel With Purpose",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: "channel-a-deleted",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o10.DeleteAt = model.GetMillis()
o10.UpdateAt = o10.DeleteAt
nErr = ss.Channel().Delete(o10.Id, o10.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
t.Run("three public channels matching 'ChannelA', but already a member of one and one deleted", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "ChannelA")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3}, channels)
})
t.Run("one public channels, but already a member", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, o4.Name)
require.NoError(t, err)
require.Equal(t, model.ChannelList{}, channels)
})
t.Run("three matching channels, but only two public", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "off-")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o7, &o6}, channels)
})
t.Run("one channel matching 'off-topic'", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "off-topic")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o6}, channels)
})
t.Run("search purpose", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "now searchable")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o9}, channels)
})
}
type ByChannelDisplayName model.ChannelList
func (s ByChannelDisplayName) Len() int { return len(s) }
func (s ByChannelDisplayName) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ByChannelDisplayName) Less(i, j int) bool {
if s[i].DisplayName != s[j].DisplayName {
return s[i].DisplayName < s[j].DisplayName
}
return s[i].Id < s[j].Id
}
func testChannelStoreSearchArchivedInTeam(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
userId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
nErr = ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, nErr)
t.Run("empty result", func(t *testing.T) {
list, err := ss.Channel().SearchArchivedInTeam(teamId, "term", userId)
require.NoError(t, err)
require.NotNil(t, list)
require.Empty(t, list)
})
t.Run("error", func(t *testing.T) {
// trigger a SQL error
s.GetMasterX().Exec("ALTER TABLE Channels RENAME TO Channels_renamed")
defer s.GetMasterX().Exec("ALTER TABLE Channels_renamed RENAME TO Channels")
list, err := ss.Channel().SearchArchivedInTeam(teamId, "term", userId)
require.Error(t, err)
require.Nil(t, list)
})
t.Run("find term", func(t *testing.T) {
list, err := ss.Channel().SearchArchivedInTeam(teamId, "Channel", userId)
require.NoError(t, err)
require.NotNil(t, list)
require.Equal(t, len(list), 1)
require.Equal(t, "Channel1", list[0].DisplayName)
})
}
func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
teamID := model.NewId()
otherTeamID := model.NewId()
o1 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA (alternate)",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamID,
DisplayName: "Channel B",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
m4 := &model.ChannelMember{
ChannelId: o4.Id,
UserId: m3.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
o5 := model.Channel{
TeamId: teamID,
DisplayName: "Channel C",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
o8 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
m5 := &model.ChannelMember{
ChannelId: o8.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m5)
require.NoError(t, err)
o9 := model.Channel{
TeamId: teamID,
DisplayName: "Town Square",
Name: "town-square",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: teamID,
DisplayName: "The",
Name: "thename",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o11 := model.Channel{
TeamId: teamID,
DisplayName: "Native Mobile Apps",
Name: "native-mobile-apps",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o11, -1)
require.NoError(t, nErr)
o12 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelZ",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o12, -1)
require.NoError(t, nErr)
o13 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA (deleted)",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o13, -1)
require.NoError(t, nErr)
o13.DeleteAt = model.GetMillis()
o13.UpdateAt = o13.DeleteAt
nErr = ss.Channel().Delete(o13.Id, o13.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
testCases := []struct {
Description string
TeamID string
UserID string
Term string
IncludeDeleted bool
ExpectedResults model.ChannelList
}{
{"ChannelA", teamID, m1.UserId, "ChannelA", false, model.ChannelList{&o1, &o3}},
{"ChannelA, include deleted", teamID, m1.UserId, "ChannelA", true, model.ChannelList{&o1, &o3, &o13}},
{"ChannelA, other team", otherTeamID, m3.UserId, "ChannelA", false, model.ChannelList{&o2}},
{"empty string", teamID, m1.UserId, "", false, model.ChannelList{&o1, &o3, &o12, &o11, &o7, &o6, &o10, &o9}},
{"no matches", teamID, m1.UserId, "blargh", false, model.ChannelList{}},
{"prefix", teamID, m1.UserId, "off-", false, model.ChannelList{&o7, &o6}},
{"full match with dash", teamID, m1.UserId, "off-topic", false, model.ChannelList{&o6}},
{"town square", teamID, m1.UserId, "town square", false, model.ChannelList{&o9}},
{"the in name", teamID, m1.UserId, "thename", false, model.ChannelList{&o10}},
{"Mobile", teamID, m1.UserId, "Mobile", false, model.ChannelList{&o11}},
{"search purpose", teamID, m1.UserId, "now searchable", false, model.ChannelList{&o12}},
{"pipe ignored", teamID, m1.UserId, "town square |", false, model.ChannelList{&o9}},
}
for _, testCase := range testCases {
t.Run("SearchInTeam/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().SearchInTeam(testCase.TeamID, testCase.Term, testCase.IncludeDeleted)
require.NoError(t, err)
require.Equal(t, testCase.ExpectedResults, channels)
})
}
testCases = append(testCases, []struct {
Description string
TeamID string
UserID string
Term string
IncludeDeleted bool
ExpectedResults model.ChannelList
}{
{"Channel A", teamID, m4.UserId, "Channel ", false, model.ChannelList{&o4, &o1, &o3, &o12}},
{"off limit (private)", teamID, m5.UserId, "off limit", false, model.ChannelList{&o8}},
}...,
)
for _, testCase := range testCases {
t.Run("AutoCompleteInTeam/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().AutocompleteInTeam(testCase.TeamID, testCase.UserID, testCase.Term, testCase.IncludeDeleted, false)
require.NoError(t, err)
sort.Sort(ByChannelDisplayName(channels))
require.Equal(t, testCase.ExpectedResults, channels)
})
}
}
func testAutocomplete(t *testing.T, ss store.Store) {
t1 := &model.Team{
DisplayName: "t1",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
teamID := t1.Id
t2 := &model.Team{
DisplayName: "t2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
otherTeamID := t2.Id
o1 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o1, -1)
require.NoError(t, err)
o2 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelA2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o2, -1)
require.NoError(t, err)
o6 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o6, -1)
require.NoError(t, err)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o2.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
tm1 := &model.TeamMember{TeamId: teamID, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm1, -1)
require.NoError(t, err)
tm2 := &model.TeamMember{TeamId: otherTeamID, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm2, -1)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
tm3 := &model.TeamMember{TeamId: otherTeamID, UserId: m3.UserId}
_, err = ss.Team().SaveMember(tm3, -1)
require.NoError(t, err)
tm4 := &model.TeamMember{TeamId: teamID, UserId: m3.UserId}
_, err = ss.Team().SaveMember(tm4, -1)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA private",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, err = ss.Channel().Save(&o3, -1)
require.NoError(t, err)
o4 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, err = ss.Channel().Save(&o4, -1)
require.NoError(t, err)
m4 := &model.ChannelMember{
ChannelId: o3.Id,
UserId: m3.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
m5 := &model.ChannelMember{
ChannelId: o4.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m5)
require.NoError(t, err)
t3 := &model.Team{
DisplayName: "t3",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t3, err = ss.Team().Save(t3)
require.NoError(t, err)
leftTeamId := t3.Id
o5 := model.Channel{
TeamId: leftTeamId,
DisplayName: "ChannelA3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o5, -1)
require.NoError(t, err)
m6 := model.ChannelMember{
ChannelId: o5.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m6)
require.NoError(t, err)
tm5 := &model.TeamMember{TeamId: leftTeamId, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm5, -1)
require.NoError(t, err)
err = ss.Channel().RemoveMember(o5.Id, m1.UserId)
require.NoError(t, err)
tm5.Roles = ""
tm5.DeleteAt = model.GetMillis()
_, err = ss.Team().UpdateMember(tm5)
require.NoError(t, err)
testCases := []struct {
Description string
UserID string
Term string
IncludeDeleted bool
IsGuest bool
ExpectedChannelIds []string
ExpectedTeamNames []string
}{
{"user 1, Channel A", m1.UserId, "ChannelA", false, false, []string{o1.Id, o2.Id, o6.Id}, []string{t1.Name, t2.Name, t1.Name}},
{"user 1, Channel B", m1.UserId, "ChannelB", false, false, []string{o4.Id}, []string{t2.Name}},
{"user 2, Channel A", m3.UserId, "ChannelA", false, false, []string{o3.Id, o1.Id, o2.Id, o6.Id}, []string{t2.Name, t1.Name, t1.Name, t1.Name}},
{"user 2 guest, Channel A", m3.UserId, "ChannelA", false, true, []string{o2.Id, o3.Id}, []string{t2.Name, t1.Name}},
{"user 2, Channel B", m3.UserId, "ChannelB", false, false, nil, nil},
{"user 1, empty string", m1.UserId, "", false, false, []string{o1.Id, o2.Id, o4.Id, o6.Id}, []string{t1.Name, t2.Name, t2.Name, t1.Name}},
{"user 2, empty string", m3.UserId, "", false, false, []string{o1.Id, o2.Id, o3.Id, o6.Id}, []string{t1.Name, t2.Name, t1.Name, t1.Name}},
}
for _, testCase := range testCases {
t.Run("Autocomplete/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest)
require.NoError(t, err)
var gotChannelIds []string
var gotTeamNames []string
for _, ch := range channels {
gotChannelIds = append(gotChannelIds, ch.Id)
gotTeamNames = append(gotTeamNames, ch.TeamName)
}
require.ElementsMatch(t, testCase.ExpectedChannelIds, gotChannelIds, "channels IDs are not as expected")
require.ElementsMatch(t, testCase.ExpectedTeamNames, gotTeamNames, "team names are not as expected")
})
}
}
func testChannelStoreSearchForUserInTeam(t *testing.T, ss store.Store) {
userId := model.NewId()
teamId := model.NewId()
otherTeamId := model.NewId()
// create 4 channels for the same team and one for other team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "test-dev-1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: teamId,
DisplayName: "test-dev-2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{
TeamId: teamId,
DisplayName: "dev-3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamId,
DisplayName: "dev-4",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: otherTeamId,
DisplayName: "other-team-dev-5",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
// add the user to the first 3 channels and the other team channel
for _, c := range []model.Channel{o1, o2, o3, o5} {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
searchAndCheck := func(t *testing.T, term string, includeDeleted bool, expectedDisplayNames []string) {
res, searchErr := ss.Channel().SearchForUserInTeam(userId, teamId, term, includeDeleted)
require.NoError(t, searchErr)
require.Len(t, res, len(expectedDisplayNames))
resultDisplayNames := []string{}
for _, c := range res {
resultDisplayNames = append(resultDisplayNames, c.DisplayName)
}
require.ElementsMatch(t, expectedDisplayNames, resultDisplayNames)
}
t.Run("Search for test, get channels 1 and 2", func(t *testing.T) {
searchAndCheck(t, "test", false, []string{o1.DisplayName, o2.DisplayName})
})
t.Run("Search for dev, get channels 1, 2 and 3", func(t *testing.T) {
searchAndCheck(t, "dev", false, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName})
})
t.Run("After adding user to channel 4, search for dev, get channels 1, 2, 3 and 4", func(t *testing.T) {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o4.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
searchAndCheck(t, "dev", false, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
t.Run("Mark channel 1 as deleted, search for dev, get channels 2, 3 and 4", func(t *testing.T) {
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
err := ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, err)
searchAndCheck(t, "dev", false, []string{o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
t.Run("With includeDeleted, search for dev, get channels 1, 2, 3 and 4", func(t *testing.T) {
searchAndCheck(t, "dev", true, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
}
func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) {
cleanupChannels(t, ss)
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Name2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o1 := model.Channel{
TeamId: t1.Id,
DisplayName: "A1 ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: t2.Id,
DisplayName: "A2 ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: t1.Id,
DisplayName: "A3 ChannelA (alternate)",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: t1.Id,
DisplayName: "A4 ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: t1.Id,
DisplayName: "A5 ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: t1.Id,
DisplayName: "A6 Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: t1.Id,
DisplayName: "A7 Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(group)
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, o7.Id, true))
require.NoError(t, err)
o8 := model.Channel{
TeamId: t1.Id,
DisplayName: "A8 Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
o9 := model.Channel{
TeamId: t1.Id,
DisplayName: "A9 Town Square",
Name: "town-square",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: t1.Id,
DisplayName: "B10 Which",
Name: "which",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o11 := model.Channel{
TeamId: t1.Id,
DisplayName: "B11 Native Mobile Apps",
Name: "native-mobile-apps",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o11, -1)
require.NoError(t, nErr)
o12 := model.Channel{
TeamId: t1.Id,
DisplayName: "B12 ChannelZ",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o12, -1)
require.NoError(t, nErr)
o13 := model.Channel{
TeamId: t1.Id,
DisplayName: "B13 ChannelA (deleted)",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o13, -1)
require.NoError(t, nErr)
o13.DeleteAt = model.GetMillis()
o13.UpdateAt = o13.DeleteAt
nErr = ss.Channel().Delete(o13.Id, o13.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
o14 := model.Channel{
TeamId: t2.Id,
DisplayName: "B14 FOOBARDISPLAYNAME",
Name: "whatever",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o14, -1)
require.NoError(t, nErr)
_, nErr = ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{o14.Id},
})
require.NoError(t, nErr)
testCases := []struct {
Description string
Term string
Opts store.ChannelSearchOpts
ExpectedResults model.ChannelList
TotalCount int
}{
{"Search FooBar by display name", "bardisplay", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by display name2", "foobar", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by display name3", "displayname", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by name", "what", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by name2", "ever", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"ChannelA", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o1, &o2, &o3}, 0},
{"ChannelA, include deleted", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: true}, model.ChannelList{&o1, &o2, &o3, &o13}, 0},
{"empty string", "", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o6, &o7, &o8, &o9, &o10, &o11, &o12, &o14}, 0},
{"no matches", "blargh", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{}, 0},
{"prefix", "off-", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o6, &o7, &o8}, 0},
{"full match with dash", "off-topic", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o6}, 0},
{"town square", "town square", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o9}, 0},
{"which in name", "which", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o10}, 0},
{"Mobile", "Mobile", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o11}, 0},
{"search purpose", "now searchable", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o12}, 0},
{"pipe ignored", "town square |", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o9}, 0},
{"exclude defaults search 'off'", "off-", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"off-topic"}}, model.ChannelList{&o7, &o8}, 0},
{"exclude defaults search 'town'", "town", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"town-square"}}, model.ChannelList{}, 0},
{"exclude by group association", "off-", store.ChannelSearchOpts{IncludeDeleted: false, NotAssociatedToGroup: group.Id}, model.ChannelList{&o6, &o8}, 0},
{"paginate includes count", "off-", store.ChannelSearchOpts{IncludeDeleted: false, PerPage: model.NewInt(100)}, model.ChannelList{&o6, &o7, &o8}, 3},
{"paginate, page 2 correct entries and count", "off-", store.ChannelSearchOpts{IncludeDeleted: false, PerPage: model.NewInt(2), Page: model.NewInt(1)}, model.ChannelList{&o8}, 3},
{"Filter private", "", store.ChannelSearchOpts{IncludeDeleted: false, Private: true}, model.ChannelList{&o4, &o5, &o8}, 3},
{"Filter public", "", store.ChannelSearchOpts{IncludeDeleted: false, Public: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o6, &o7}, 10},
{"Filter public and private", "", store.ChannelSearchOpts{IncludeDeleted: false, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 13},
{"Filter public and private and include deleted", "", store.ChannelSearchOpts{IncludeDeleted: true, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 14},
{"Filter group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, GroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o5}, 1},
{"Filter exclude group constrained and include deleted", "", store.ChannelSearchOpts{IncludeDeleted: true, ExcludeGroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o6}, 13},
{"Filter private and exclude group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeGroupConstrained: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o4, &o8}, 2},
{"Exclude policy constrained", "", store.ChannelSearchOpts{ExcludePolicyConstrained: true}, model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o6, &o7, &o8, &o9, &o10, &o11, &o12}, 0},
{"Filter team 2", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t2.Id}, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o2, &o14}, 2},
{"Filter team 2, private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t2.Id}, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{}, 0},
{"Filter team 1 and team 2, private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o4, &o5, &o8}, 3},
{"Filter team 1 and team 2, public and private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 13},
{"Filter team 1 and team 2, public and private and group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, GroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o5}, 1},
{"Filter team 1 and team 2, public and private and exclude group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, ExcludeGroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o6}, 12},
{"Filter deleted returns only deleted channels", "", store.ChannelSearchOpts{Deleted: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o13}, 1},
{"Search ChannelA by id", o1.Id, store.ChannelSearchOpts{IncludeDeleted: false, Page: model.NewInt(0), PerPage: model.NewInt(5), IncludeSearchById: true}, model.ChannelList{&o1}, 1},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
channels, count, err := ss.Channel().SearchAllChannels(testCase.Term, testCase.Opts)
require.NoError(t, err)
require.Equal(t, len(testCase.ExpectedResults), len(channels))
for i, expected := range testCase.ExpectedResults {
require.Equal(t, expected.Id, channels[i].Id)
}
if testCase.Opts.Page != nil || testCase.Opts.PerPage != nil {
require.Equal(t, int64(testCase.TotalCount), count)
}
})
}
}
func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{ChannelId: o1.Id, UserId: model.NewId(), NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
var members model.ChannelMembers
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId})
require.NoError(t, nErr, nErr)
rm1 := members[0]
require.Equal(t, m1.ChannelId, rm1.ChannelId, "bad team id")
require.Equal(t, m1.UserId, rm1.UserId, "bad user id")
m2 := &model.ChannelMember{ChannelId: o1.Id, UserId: model.NewId(), NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId, m2.UserId, model.NewId()})
require.NoError(t, nErr, nErr)
require.Len(t, members, 2, "return wrong number of results")
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{})
require.NoError(t, nErr)
require.Len(t, members, 0)
}
func testChannelStoreGetMembersByChannelIds(t *testing.T, ss store.Store) {
userId := model.NewId()
// Create a couple channels and add the user to them
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("should return the user's members for the given channels", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, userId)
require.NoError(t, nErr)
assert.Len(t, result, 2)
assert.Equal(t, userId, result[0].UserId)
assert.True(t, result[0].ChannelId == channel1.Id || result[1].ChannelId == channel1.Id)
assert.Equal(t, userId, result[1].UserId)
assert.True(t, result[0].ChannelId == channel2.Id || result[1].ChannelId == channel2.Id)
})
t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{model.NewId(), model.NewId()}, userId)
require.NoError(t, nErr)
assert.Len(t, result, 0)
})
t.Run("should not error or return anything for invalid user IDs", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, model.NewId())
require.NoError(t, nErr)
assert.Len(t, result, 0)
})
}
func testChannelStoreGetMembersInfoByChannelIds(t *testing.T, ss store.Store) {
u, err := ss.User().Save(&model.User{
Username: "user.test",
Email: MakeEmail(),
Nickname: model.NewId(),
})
require.NoError(t, err)
// Create a couple channels and add the user to them
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("should return the user's members for the given channels", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersInfoByChannelIds([]string{channel1.Id, channel2.Id})
require.NoError(t, nErr)
assert.Len(t, result, 2)
for _, item := range result {
assert.Len(t, item, 1)
assert.Equal(t, u.Id, item[0].Id)
}
})
t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) {
_, err := ss.Channel().GetMembersInfoByChannelIds([]string{model.NewId(), model.NewId()})
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testChannelStoreSearchGroupChannels(t *testing.T, ss store.Store) {
// Users
u1 := &model.User{}
u1.Username = "user.one"
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Username = "user.two"
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
u3 := &model.User{}
u3.Username = "user.three"
u3.Email = MakeEmail()
u3.Nickname = model.NewId()
_, err = ss.User().Save(u3)
require.NoError(t, err)
u4 := &model.User{}
u4.Username = "user.four"
u4.Email = MakeEmail()
u4.Nickname = model.NewId()
_, err = ss.User().Save(u4)
require.NoError(t, err)
// Group channels
userIds := []string{u1.Id, u2.Id, u3.Id}
gc1 := model.Channel{}
gc1.Name = model.GetGroupNameFromUserIds(userIds)
gc1.DisplayName = "GroupChannel" + model.NewId()
gc1.Type = model.ChannelTypeGroup
_, nErr := ss.Channel().Save(&gc1, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
userIds = []string{u1.Id, u4.Id}
gc2 := model.Channel{}
gc2.Name = model.GetGroupNameFromUserIds(userIds)
gc2.DisplayName = "GroupChannel" + model.NewId()
gc2.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&gc2, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
userIds = []string{u1.Id, u2.Id, u3.Id, u4.Id}
gc3 := model.Channel{}
gc3.Name = model.GetGroupNameFromUserIds(userIds)
gc3.DisplayName = "GroupChannel" + model.NewId()
gc3.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&gc3, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc3.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
defer func() {
for _, gc := range []model.Channel{gc1, gc2, gc3} {
ss.Channel().PermanentDeleteMembersByChannel(gc3.Id)
ss.Channel().PermanentDelete(gc.Id)
}
}()
testCases := []struct {
Name string
UserId string
Term string
ExpectedResult []string
}{
{
Name: "Get all group channels for user1",
UserId: u1.Id,
Term: "",
ExpectedResult: []string{gc1.Id, gc2.Id, gc3.Id},
},
{
Name: "Get group channels for user1 and term 'three'",
UserId: u1.Id,
Term: "three",
ExpectedResult: []string{gc1.Id, gc3.Id},
},
{
Name: "Get group channels for user1 and term 'four two'",
UserId: u1.Id,
Term: "four two",
ExpectedResult: []string{gc3.Id},
},
{
Name: "Get all group channels for user2",
UserId: u2.Id,
Term: "",
ExpectedResult: []string{gc1.Id, gc3.Id},
},
{
Name: "Get group channels for user2 and term 'four'",
UserId: u2.Id,
Term: "four",
ExpectedResult: []string{gc3.Id},
},
{
Name: "Get all group channels for user4",
UserId: u4.Id,
Term: "",
ExpectedResult: []string{gc2.Id, gc3.Id},
},
{
Name: "Get group channels for user4 and term 'one five'",
UserId: u4.Id,
Term: "one five",
ExpectedResult: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result, err := ss.Channel().SearchGroupChannels(tc.UserId, tc.Term)
require.NoError(t, err)
resultIds := []string{}
for _, gc := range result {
resultIds = append(resultIds, gc.Id)
}
require.ElementsMatch(t, tc.ExpectedResult, resultIds)
})
}
}
func testChannelStoreAnalyticsDeletedTypeCount(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
p3 := model.Channel{}
p3.TeamId = model.NewId()
p3.DisplayName = "Channel3"
p3.Name = NewTestId()
p3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&p3, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
d4, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr)
defer func() {
ss.Channel().PermanentDeleteMembersByChannel(d4.Id)
ss.Channel().PermanentDelete(d4.Id)
}()
var openStartCount int64
openStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen)
require.NoError(t, nErr, nErr)
var privateStartCount int64
privateStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate)
require.NoError(t, nErr, nErr)
var directStartCount int64
directStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeDirect)
require.NoError(t, nErr, nErr)
nErr = ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(o2.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(p3.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(d4.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
var count int64
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen)
require.NoError(t, err, nErr)
assert.Equal(t, openStartCount+2, count, "Wrong open channel deleted count.")
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate)
require.NoError(t, nErr, nErr)
assert.Equal(t, privateStartCount+1, count, "Wrong private channel deleted count.")
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeDirect)
require.NoError(t, nErr, nErr)
assert.Equal(t, directStartCount+1, count, "Wrong direct channel deleted count.")
}
func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) {
ch1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
p1, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
pl, errGet := ss.Channel().GetPinnedPosts(o1.Id)
require.NoError(t, errGet, errGet)
require.NotNil(t, pl.Posts[p1.Id], "didn't return relevant pinned posts")
ch2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
pl, errGet = ss.Channel().GetPinnedPosts(o2.Id)
require.NoError(t, errGet, errGet)
require.Empty(t, pl.Posts, "wasn't supposed to return posts")
t.Run("with correct ReplyCount", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
RootId: post1.Id,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
posts, err := ss.Channel().GetPinnedPosts(channel.Id)
require.NoError(t, err)
require.Len(t, posts.Posts, 3)
require.Equal(t, posts.Posts[post1.Id].ReplyCount, int64(1))
require.Equal(t, posts.Posts[post2.Id].ReplyCount, int64(0))
require.Equal(t, posts.Posts[post3.Id].ReplyCount, int64(1))
})
}
func testChannelStoreGetPinnedPostCount(t *testing.T, ss store.Store) {
ch1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
_, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
count, errGet := ss.Channel().GetPinnedPostCount(o1.Id, true)
require.NoError(t, errGet, errGet)
require.EqualValues(t, 2, count, "didn't return right count")
ch2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
count, errGet = ss.Channel().GetPinnedPostCount(o2.Id, true)
require.NoError(t, errGet, errGet)
require.EqualValues(t, 0, count, "should return 0")
}
func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(channel, 0)
assert.Error(t, nErr)
var ltErr *store.ErrLimitExceeded
assert.True(t, errors.As(nErr, <Err))
channel.Id = ""
_, nErr = ss.Channel().Save(channel, 1)
assert.NoError(t, nErr)
}
func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) {
// Create some schemes.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
// Create and save some teams.
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c3 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, _ = ss.Channel().Save(c1, 100)
_, _ = ss.Channel().Save(c2, 100)
_, _ = ss.Channel().Save(c3, 100)
// Get the channels by a valid Scheme ID.
d1, err := ss.Channel().GetChannelsByScheme(s1.Id, 0, 100)
assert.NoError(t, err)
assert.Len(t, d1, 2)
// Get the channels by a valid Scheme ID where there aren't any matching Channel.
d2, err := ss.Channel().GetChannelsByScheme(s2.Id, 0, 100)
assert.NoError(t, err)
assert.Empty(t, d2)
// Get the channels by an invalid Scheme ID.
d3, err := ss.Channel().GetChannelsByScheme(model.NewId(), 0, 100)
assert.NoError(t, err)
assert.Empty(t, d3)
}
func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1,
}
c1, _ = ss.Channel().Save(c1, 100)
cm1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_admin channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm3 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "something_else",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm1, _ = ss.Channel().SaveMember(cm1)
cm2, _ = ss.Channel().SaveMember(cm2)
cm3, _ = ss.Channel().SaveMember(cm3)
lastDoneChannelId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
data, err := ss.Channel().MigrateChannelMembers(lastDoneChannelId, lastDoneUserId)
if assert.NoError(t, err) {
if data == nil {
break
}
lastDoneChannelId = data["ChannelId"]
lastDoneUserId = data["UserId"]
}
}
ss.Channel().ClearCaches()
cm1b, err := ss.Channel().GetMember(context.Background(), cm1.ChannelId, cm1.UserId)
assert.NoError(t, err)
assert.Equal(t, "", cm1b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.True(t, cm1b.SchemeUser)
assert.True(t, cm1b.SchemeAdmin)
cm2b, err := ss.Channel().GetMember(context.Background(), cm2.ChannelId, cm2.UserId)
assert.NoError(t, err)
assert.Equal(t, "", cm2b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.True(t, cm2b.SchemeUser)
assert.False(t, cm2b.SchemeAdmin)
cm3b, err := ss.Channel().GetMember(context.Background(), cm3.ChannelId, cm3.UserId)
assert.NoError(t, err)
assert.Equal(t, "something_else", cm3b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.False(t, cm3b.SchemeUser)
assert.False(t, cm3b.SchemeAdmin)
}
func testResetAllChannelSchemes(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c1, _ = ss.Channel().Save(c1, 100)
c2, _ = ss.Channel().Save(c2, 100)
assert.Equal(t, s1.Id, *c1.SchemeId)
assert.Equal(t, s1.Id, *c2.SchemeId)
err = ss.Channel().ResetAllChannelSchemes()
assert.NoError(t, err)
c1, _ = ss.Channel().Get(c1.Id, true)
c2, _ = ss.Channel().Get(c2.Id, true)
assert.Equal(t, "", *c1.SchemeId)
assert.Equal(t, "", *c2.SchemeId)
}
func testChannelStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
c := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
c, _ = ss.Channel().Save(c, 100)
m1 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "system_user_access_token channel_user channel_admin",
}
m2 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_user custom_role channel_admin another_custom_role",
}
m3 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_user",
}
m4 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "custom_only",
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m3)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
require.NoError(t, ss.Channel().ClearAllCustomRoleAssignments())
member, err := ss.Channel().GetMember(context.Background(), m1.ChannelId, m1.UserId)
require.NoError(t, err)
assert.Equal(t, m1.ExplicitRoles, member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m2.ChannelId, m2.UserId)
require.NoError(t, err)
assert.Equal(t, "channel_user channel_admin", member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m3.ChannelId, m3.UserId)
require.NoError(t, err)
assert.Equal(t, m3.ExplicitRoles, member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m4.ChannelId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, "", member.Roles)
}
// testMaterializedPublicChannels tests edge cases involving the triggers and stored procedures
// that materialize the PublicChannels table.
func testMaterializedPublicChannels(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
// o2 is another public channel on the team
o2 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel 2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
t.Run("o1 and o2 initially listed in public channels", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1, &o2}, channels)
})
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
e := ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, e, "channel should have been deleted")
t.Run("o1 still listed in public channels when marked as deleted", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1, &o2}, channels)
})
ss.Channel().PermanentDelete(o1.Id)
t.Run("o1 no longer listed in public channels when permanently deleted", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2}, channels)
})
o2.Type = model.ChannelTypePrivate
_, err := ss.Channel().Update(&o2)
require.NoError(t, err)
t.Run("o2 no longer listed since now private", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{}, channels)
})
o2.Type = model.ChannelTypeOpen
_, err = ss.Channel().Update(&o2)
require.NoError(t, err)
t.Run("o2 listed once again since now public", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2}, channels)
})
// o3 is a public channel on the team that already existed in the PublicChannels table.
o3 := model.Channel{
Id: model.NewId(),
TeamId: teamId,
DisplayName: "Open Channel 3",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, execerr := s.GetMasterX().NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose);
`, map[string]any{
"id": o3.Id,
"deleteat": o3.DeleteAt,
"teamid": o3.TeamId,
"displayname": o3.DisplayName,
"name": o3.Name,
"header": o3.Header,
"purpose": o3.Purpose,
})
require.NoError(t, execerr)
o3.DisplayName = "Open Channel 3 - Modified"
_, execerr = s.GetMasterX().NamedExec(`
INSERT INTO
Channels(Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, DisplayName, Name, Header, Purpose, LastPostAt, LastRootPostAt, TotalMsgCount, ExtraUpdateAt, CreatorId, TotalMsgCountRoot)
VALUES
(:id, :createat, :updateat, :deleteat, :teamid, :type, :displayname, :name, :header, :purpose, :lastpostat, :lastrootpostat, :totalmsgcount, :extraupdateat, :creatorid, 0);
`, map[string]any{
"id": o3.Id,
"createat": o3.CreateAt,
"updateat": o3.UpdateAt,
"deleteat": o3.DeleteAt,
"teamid": o3.TeamId,
"type": o3.Type,
"displayname": o3.DisplayName,
"name": o3.Name,
"header": o3.Header,
"purpose": o3.Purpose,
"lastpostat": o3.LastPostAt,
"lastrootpostat": o3.LastRootPostAt,
"totalmsgcount": o3.TotalMsgCount,
"extraupdateat": o3.ExtraUpdateAt,
"creatorid": o3.CreatorId,
})
require.NoError(t, execerr)
t.Run("verify o3 INSERT converted to UPDATE", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2, &o3}, channels)
})
// o4 is a public channel on the team that existed in the Channels table but was omitted from the PublicChannels table.
o4 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel 4",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
_, execerr = s.GetMasterX().Exec(`
DELETE FROM
PublicChannels
WHERE
Id = ?
`, o4.Id)
require.NoError(t, execerr)
o4.DisplayName += " - Modified"
_, err = ss.Channel().Update(&o4)
require.NoError(t, err)
t.Run("verify o4 UPDATE converted to INSERT", func(t *testing.T) {
channels, err := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o2, &o3, &o4}, channels)
})
}
func testChannelStoreGetAllChannelsForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
d1, err := ss.Channel().GetAllChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, c := range d1 {
if c.Id == c1.Id {
found = true
assert.Equal(t, t1.Id, c.TeamId)
assert.Nil(t, c.SchemeId)
assert.Equal(t, t1.Name, c.TeamName)
}
}
assert.True(t, found)
}
func testChannelStoreGetChannelMembersForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = u1.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
d1, err := ss.Channel().GetChannelMembersForExport(u1.Id, t1.Id)
assert.NoError(t, err)
assert.Len(t, d1, 1)
cmfe1 := d1[0]
assert.Equal(t, c1.Name, cmfe1.ChannelName)
assert.Equal(t, c1.Id, cmfe1.ChannelId)
assert.Equal(t, u1.Id, cmfe1.UserId)
}
func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store, s SqlStore) {
// Set up all the objects needed in the store.
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{}
u3.Email = MakeEmail()
u3.Nickname = model.NewId()
_, err = ss.User().Save(&u3)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = c1.Id
m3.UserId = u3.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
// Get all the channel members. Check there are 3.
d1, err := ss.Channel().GetMembers(c1.Id, 0, 1000)
assert.NoError(t, err)
assert.Len(t, d1, 3)
// Deactivate users 1 & 2.
u1.DeleteAt = model.GetMillis()
u2.DeleteAt = model.GetMillis()
_, err = ss.User().Update(&u1, true)
require.NoError(t, err)
_, err = ss.User().Update(&u2, true)
require.NoError(t, err)
// Remove all deactivated users from the channel.
assert.NoError(t, ss.Channel().RemoveAllDeactivatedMembers(c1.Id))
// Get all the channel members. Check there is now only 1: m3.
d2, err := ss.Channel().GetMembers(c1.Id, 0, 1000)
assert.NoError(t, err)
assert.Len(t, d2, 1)
assert.Equal(t, u3.Id, d2[0].UserId)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannels(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
userIds := []string{model.NewId(), model.NewId(), model.NewId()}
o2 := model.Channel{}
o2.Name = model.GetGroupNameFromUserIds(userIds)
o2.DisplayName = "GroupChannel" + model.NewId()
o2.Name = NewTestId()
o2.Type = model.ChannelTypeGroup
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Len(t, d1, 2)
assert.ElementsMatch(t, []string{o1.DisplayName, o2.DisplayName}, []string{d1[0].DisplayName, d1[1].DisplayName})
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "The Direct Channel" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
o2 := model.Channel{}
o2.TeamId = teamId
o2.DisplayName = "Channel2" + model.NewId()
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = teamId
o3.DisplayName = "Channel3" + model.NewId()
o3.Name = NewTestId()
o3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Len(t, d1, 1)
assert.Equal(t, o1.DisplayName, d1[0].DisplayName)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannelsDeletedChannel(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Different Name" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
o1.DeleteAt = 1
nErr = ss.Channel().SetDeleteAt(o1.Id, 1, 1)
require.NoError(t, nErr, "channel should have been deleted")
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, 0, len(d1))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetChannelsBatchForIndexing(t *testing.T, ss store.Store) {
// Set up all the objects needed
c1 := &model.Channel{}
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c2 := &model.Channel{}
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c3 := &model.Channel{}
c3.DisplayName = "Channel3"
c3.Name = NewTestId()
c3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c3, -1)
require.NoError(t, nErr)
c4 := &model.Channel{}
c4.DisplayName = "Channel4"
c4.Name = NewTestId()
c4.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(c4, -1)
require.NoError(t, nErr)
c5 := &model.Channel{}
c5.DisplayName = "Channel5"
c5.Name = NewTestId()
c5.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c5, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c6 := &model.Channel{}
c6.DisplayName = "Channel6"
c6.Name = NewTestId()
c6.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c6, -1)
require.NoError(t, nErr)
// First and last channel should be outside the range
channels, err := ss.Channel().GetChannelsBatchForIndexing(c1.CreateAt, "", 4)
assert.NoError(t, err)
assert.Len(t, channels, 4)
// From 4th createat+id
channels, err = ss.Channel().GetChannelsBatchForIndexing(channels[3].CreateAt, channels[3].Id, 5)
assert.NoError(t, err)
assert.Len(t, channels, 2)
// Testing the limit
channels, err = ss.Channel().GetChannelsBatchForIndexing(channels[1].CreateAt, channels[1].Id, 1)
assert.NoError(t, err)
assert.Len(t, channels, 0)
}
func testGroupSyncedChannelCount(t *testing.T, ss store.Store) {
channel1, nErr := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}, 999)
require.NoError(t, nErr)
require.True(t, channel1.IsGroupConstrained())
defer ss.Channel().PermanentDelete(channel1.Id)
channel2, nErr := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}, 999)
require.NoError(t, nErr)
require.False(t, channel2.IsGroupConstrained())
defer ss.Channel().PermanentDelete(channel2.Id)
count, err := ss.Channel().GroupSyncedChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
channel2.GroupConstrained = model.NewBool(true)
channel2, err = ss.Channel().Update(channel2)
require.NoError(t, err)
require.True(t, channel2.IsGroupConstrained())
countAfter, err := ss.Channel().GroupSyncedChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testSetShared(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
t.Run("Check default", func(t *testing.T) {
assert.False(t, channelSaved.IsShared())
})
t.Run("Set Shared flag", func(t *testing.T) {
err := ss.Channel().SetShared(channelSaved.Id, true)
require.NoError(t, err)
channelMod, err := ss.Channel().Get(channelSaved.Id, false)
require.NoError(t, err)
assert.True(t, channelMod.IsShared())
})
t.Run("Set Shared for invalid id", func(t *testing.T) {
err := ss.Channel().SetShared(model.NewId(), true)
require.Error(t, err)
})
}
func testGetTeamForChannel(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: "myteam",
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
got, err := ss.Channel().GetTeamForChannel(channelSaved.Id)
require.NoError(t, err)
assert.Equal(t, team.Id, got.Id)
_, err = ss.Channel().GetTeamForChannel("notfound")
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testChannelPostCountsByDuration(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved.Id) }()
userID := model.NewId()
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
dpc, err := ss.Channel().PostCountsByDuration([]string{channelSaved.Id}, 0, &userID, model.PostsByDay, time.Now().Location())
require.NoError(t, err)
require.Len(t, dpc, 1)
require.Equal(t, channel.Id, dpc[0].ChannelID)
require.Equal(t, 1, dpc[0].PostCount)
}
func testGetTopInactiveChannels(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channelPublic0 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag asdf",
Name: "test_share_flag_public0",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved0, err := ss.Channel().Save(channelPublic0, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved0.Id) }()
channelPublic1 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved1, err := ss.Channel().Save(channelPublic1, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved1.Id) }()
// create private channel
c3 := model.Channel{}
c3.TeamId = team.Id
c3.DisplayName = "Channel3" + model.NewId()
c3.Name = NewTestId()
c3.Type = model.ChannelTypePrivate
c3.CreateAt = 1
channelPrivate, nErr := ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
// create private channel with post
c3NoPost := model.Channel{}
c3NoPost.TeamId = team.Id
c3NoPost.DisplayName = "Channel3" + model.NewId()
c3NoPost.Name = NewTestId()
c3NoPost.Type = model.ChannelTypePrivate
c3NoPost.CreateAt = 1
channelPrivateNoPost, nErr := ss.Channel().Save(&c3NoPost, -1)
require.NoError(t, nErr)
// create dm channel
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
uBot := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, nErr)
// add u1, u2 to channels
cm1 := &model.ChannelMember{ChannelId: channelPrivate.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1)
require.NoError(t, err)
cm1NoPost := &model.ChannelMember{ChannelId: channelPrivateNoPost.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1NoPost)
require.NoError(t, err)
cm1Public := &model.ChannelMember{ChannelId: channelPublic1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1Public)
require.NoError(t, err)
cm2 := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
cmBot := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: uBot.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cmBot)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test1",
})
require.NoError(t, err)
// create posts in channel public 0
postToCheckLastUpdateAt, err := ss.Post().Save(&model.Post{
UserId: u2.Id,
ChannelId: channelSaved0.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
// create posts in channel public 1
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
})
require.NoError(t, err)
}
// for u1
t.Run("top inactive channels for team - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 4)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[1].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[3].ID, channelPublic1.Id)
// test bot posts are counted
require.Equal(t, topInactiveChannels.Items[3].MessageCount, int64(4))
// participants
require.Equal(t, topInactiveChannels.Items[2].Participants[0], u1.Id)
require.Equal(t, topInactiveChannels.Items[3].Participants[0], u1.Id)
})
t.Run("top inactive channels for user - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 3)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPublic1.Id)
})
// for u2
t.Run("top inactive channels for team - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 2)
require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[0].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPublic1.Id)
})
t.Run("top inactive channels for user - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 1)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPublic0.Id)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestChannelStoreCategories(t *testing.T, ss store.Store, s SqlStore) {
t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, ss) })
t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, ss) })
t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, ss, s) })
t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, ss) })
t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, ss) })
t.Run("ClearSidebarOnTeamLeave", func(t *testing.T) { testClearSidebarOnTeamLeave(t, ss, s) })
t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, ss, s) })
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
t.Run("SidebarCategoryDeadlock", func(t *testing.T) { testSidebarCategoryDeadlock(t, ss) })
}
func setupTeam(t *testing.T, ss store.Store, userIds ...string) *model.Team {
team, err := ss.Team().Save(&model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
assert.NoError(t, err)
members := make([]*model.TeamMember, 0, len(userIds))
for _, userId := range userIds {
members = append(members, &model.TeamMember{
TeamId: team.Id,
UserId: userId,
})
}
if len(members) > 0 {
_, err = ss.Team().SaveMultipleMembers(members, len(userIds)+1)
assert.NoError(t, err)
}
return team
}
func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) {
t.Run("should create initial favorites/channels/DMs categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
require.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("should create initial favorites/channels/DMs categories for multiple users", func(t *testing.T) {
userId := model.NewId()
userId2 := model.NewId()
team := setupTeam(t, ss, userId, userId2)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts)
assert.NoError(t, nErr)
assert.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("should create initial favorites/channels/DMs categories on different teams", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
opts = &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
assert.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, res, initialCategories)
// Calling CreateInitialSidebarCategories a second time shouldn't create any new categories
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
assert.NotEmpty(t, res)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Equal(t, initialCategories.Categories, res.Categories)
})
t.Run("shouldn't create additional categories when ones already exist even when ran simultaneously", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
_, _ = ss.Channel().CreateInitialSidebarCategories(userId, opts)
}()
}
wg.Wait()
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Len(t, res.Categories, 3)
})
t.Run("should populate the Favorites category with regular channels", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Set up two channels, one favorited and one not
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
channel2, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel2",
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{channel1.Id}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Set up two channels
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
DisplayName: "zebra",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
channel2, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel2",
DisplayName: "aardvark",
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel2.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
otherUserId1 := model.NewId()
otherUserId2 := model.NewId()
// Set up two direct channels, one favorited and one not
dmChannel1, err := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId1),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId1,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, err)
dmChannel2, err := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId2),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId2,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, err)
err = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel1.Id,
Value: "true",
},
})
require.NoError(t, err)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{dmChannel1.Id}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels)
// Get and check the categories for channels
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, categories, categories2)
})
t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
// Set up a channel on another team and favorite it
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Equal(t, []string{}, categories.Categories[1].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("graphQL path to create initial favorites/channels/DMs categories on different teams", func(t *testing.T) {
userId := model.NewId()
t1 := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: t1.Id, UserId: userId}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
t2 := &model.Team{
DisplayName: "DisplayName2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
}
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
m2 := &model.TeamMember{TeamId: t2.Id, UserId: userId}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: t1.Id,
ExcludeTeam: true,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
for _, cat := range res.Categories {
assert.Equal(t, t2.Id, cat.TeamId)
}
})
}
func testCreateSidebarCategory(t *testing.T, ss store.Store) {
t.Run("Creating category without initial categories should fail", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.Error(t, err)
var errNotFound *store.ErrNotFound
require.ErrorAs(t, err, &errNotFound)
require.Nil(t, created)
})
t.Run("should place the new category second if Favorites comes first", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.NoError(t, err)
// Confirm that it comes second
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
assert.Equal(t, created.Id, res.Categories[1].Id)
})
t.Run("should place the new category first if Favorites is not first", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Re-arrange the categories so that Favorites comes last
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{
categories.Categories[1].Id,
categories.Categories[2].Id,
categories.Categories[0].Id,
})
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.NoError(t, err)
// Confirm that it comes first
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type)
assert.Equal(t, created.Id, res.Categories[0].Id)
})
t.Run("should create the category with its channels", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create some channels
channel1, err := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
Channels: []string{channel2.Id, channel1.Id},
})
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels)
// Get the channel again to ensure that the SidebarChannels were saved correctly
res2, err := ss.Channel().GetSidebarCategory(created.Id)
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels)
})
t.Run("should remove any channels from their previous categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Create some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, nErr)
// Assign them to categories
favoritesCategory.Channels = []string{channel1.Id}
channelsCategory.Channels = []string{channel2.Id}
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
favoritesCategory,
channelsCategory,
})
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
Channels: []string{channel2.Id, channel1.Id},
})
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels)
// Confirm that the channels were removed from their original categories
res2, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, res2.Channels)
res2, err = ss.Channel().GetSidebarCategory(channelsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, res2.Channels)
})
}
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should return a custom category with its Channels field set", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
channelId1 := model.NewId()
channelId2 := model.NewId()
channelId3 := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create a category and assign some channels to it
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userId,
TeamId: team.Id,
DisplayName: model.NewId(),
},
Channels: []string{channelId1, channelId2, channelId3},
})
require.NoError(t, err)
require.NotNil(t, created)
// Ensure that they're returned in order
res2, err := ss.Channel().GetSidebarCategory(created.Id)
assert.NoError(t, err)
assert.Equal(t, created.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryCustom, res2.Type)
assert.Equal(t, created.DisplayName, res2.DisplayName)
assert.Equal(t, []string{channelId1, channelId2, channelId3}, res2.Channels)
})
t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the channels category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Join some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "channel1",
DisplayName: "DEF",
TeamId: team.Id,
Type: model.ChannelTypePrivate,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: "channel2",
DisplayName: "ABC",
TeamId: team.Id,
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Confirm that they're not in the Channels category in the DB
var count int64
countErr := s.GetMasterX().Get(&count, `
SELECT
COUNT(*)
FROM
SidebarChannels
WHERE
CategoryId = ?`, channelsCategory.Id)
require.NoError(t, countErr)
assert.Equal(t, int64(0), count)
// Ensure that the Channels are returned in alphabetical order
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels)
})
t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the channels category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
channelsCategory := categories.Categories[1]
// Join a channel on another team
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "abc",
TeamId: model.NewId(),
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel1.Id)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Ensure that no channels are returned
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Len(t, res2.Channels, 0)
})
t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
// Create the initial categories and find the channels category
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Join some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "channel1",
DisplayName: "DEF",
TeamId: team.Id,
Type: model.ChannelTypePrivate,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: "channel2",
DisplayName: "ABC",
TeamId: team.Id,
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// And assign one to another category
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel2.Id},
},
})
require.NoError(t, err)
// Ensure that the correct channel is returned in the Channels category
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Equal(t, []string{channel1.Id}, res2.Channels)
})
t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a DM
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
// Ensure that the DM is returned
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{dmChannel.Id}, res2.Channels)
})
t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a GM
gmChannel, nErr := ss.Channel().Save(&model.Channel{
Name: "abc",
TeamId: "",
Type: model.ChannelTypeGroup,
}, 10)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(gmChannel.Id)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: gmChannel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Ensure that the DM is returned
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{gmChannel.Id}, res2.Channels)
})
t.Run("should return orphaned DM channels in the DMs category which are in a custom category on another team", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a DM
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
// Create another team and assign the DM to a custom category on that team
otherTeam := setupTeam(t, ss, userId)
opts = &store.SidebarCategorySearchOpts{
TeamID: otherTeam.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
_, err = ss.Channel().CreateSidebarCategory(userId, otherTeam.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userId,
TeamId: team.Id,
},
Channels: []string{dmChannel.Id},
})
require.NoError(t, err)
// Ensure that the DM is returned with the DMs category on the original team
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{dmChannel.Id}, res2.Channels)
})
}
func testGetSidebarCategories(t *testing.T, ss store.Store) {
t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
channelIds := []string{
model.NewId(),
model.NewId(),
model.NewId(),
}
newCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
Channels: channelIds,
})
require.NoError(t, err)
require.NotNil(t, newCategory)
gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id)
require.NoError(t, err)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
// This looks unnecessary, but I was getting different results from some of these before
assert.Equal(t, newCategory.Channels, res.Categories[1].Channels)
assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels)
assert.Equal(t, channelIds, res.Categories[1].Channels)
})
t.Run("should not return categories for teams deleted, or no longer a member", func(t *testing.T) {
userId := model.NewId()
teamMember1 := setupTeam(t, ss, userId)
teamMember2 := setupTeam(t, ss, userId)
teamDeleted := setupTeam(t, ss, userId)
teamDeleted.DeleteAt = model.GetMillis()
ss.Team().Update(teamDeleted)
teamNotMember := setupTeam(t, ss)
teamDeletedMember := setupTeam(t, ss, userId)
members, err := ss.Team().GetMembersByIds(teamDeletedMember.Id, []string{userId}, nil)
require.NoError(t, err)
require.NotEmpty(t, members)
member := members[0]
member.DeleteAt = model.GetMillis()
ss.Team().UpdateMember(member)
teamIds := []string{
teamMember1.Id,
teamMember2.Id,
teamDeleted.Id,
teamNotMember.Id,
teamDeletedMember.Id,
}
for _, id := range teamIds {
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, &store.SidebarCategorySearchOpts{TeamID: id})
require.NoError(t, nErr)
require.NotEmpty(t, res)
}
opts := &store.SidebarCategorySearchOpts{
TeamID: teamMember1.Id,
ExcludeTeam: false,
}
// Team member and not exclude
res, err := ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 3, len(res.Categories))
// No team member and not exclude
opts.TeamID = teamDeleted.Id
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 0, len(res.Categories))
// No team member and exclude
opts.ExcludeTeam = true
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 6, len(res.Categories))
// Team member and exclude
opts.TeamID = teamMember1.Id
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 3, len(res.Categories))
})
}
func testUpdateSidebarCategories(t *testing.T, ss store.Store) {
t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
// And then update one of them
updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
channelsCategory,
})
require.NoError(t, err)
assert.Equal(t, channelsCategory, updated[0])
assert.Equal(t, "Channels", updated[0].DisplayName)
// And then reorder the categories
err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id})
require.NoError(t, err)
// Which somehow blanks out stuff because ???
got, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id)
require.NoError(t, err)
assert.Equal(t, "Favorites", got.DisplayName)
})
t.Run("categories should be returned in their original order", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
// And then update them
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
favoritesCategory,
channelsCategory,
dmsCategory,
})
assert.NoError(t, err)
assert.Equal(t, favoritesCategory.Id, updatedCategories[0].Id)
assert.Equal(t, channelsCategory.Id, updatedCategories[1].Id)
assert.Equal(t, dmsCategory.Id, updatedCategories[2].Id)
})
t.Run("should silently fail to update read only fields", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
// Try to change the type of Favorites
{
SidebarCategory: model.SidebarCategory{
Id: favoritesCategory.Id,
DisplayName: "something else",
},
Channels: favoritesCategory.Channels,
},
// Try to change the type of Channels
{
SidebarCategory: model.SidebarCategory{
Id: channelsCategory.Id,
Type: model.SidebarCategoryDirectMessages,
},
Channels: channelsCategory.Channels,
},
// Try to change the Channels of DMs
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{"fakechannel"},
},
// Try to change the UserId/TeamId of a custom category
{
SidebarCategory: model.SidebarCategory{
Id: customCategory.Id,
UserId: model.NewId(),
TeamId: model.NewId(),
Sorting: customCategory.Sorting,
DisplayName: customCategory.DisplayName,
},
Channels: customCategory.Channels,
},
}
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName)
assert.Equal(t, "Favorites", updatedCategories[0].DisplayName)
assert.NotEqual(t, model.SidebarCategoryChannels, categoriesToUpdate[1].Type)
assert.Equal(t, model.SidebarCategoryChannels, updatedCategories[1].Type)
assert.NotEqual(t, []string{}, categoriesToUpdate[2].Channels)
assert.Equal(t, []string{}, updatedCategories[2].Channels)
assert.NotEqual(t, userId, categoriesToUpdate[3].UserId)
assert.Equal(t, userId, updatedCategories[3].UserId)
})
t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Assign it to favorites
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then remove it
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.Error(t, nErr)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
// Create a direct channel
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
assert.NoError(t, nErr)
// Assign it to favorites
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then remove it
dmsCategory := categories.Categories[2]
require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type)
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.Error(t, nErr)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites categories in each team
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
opts = &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
favoritesCategory2 := categories2.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type)
// Create a direct channel
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
assert.NoError(t, nErr)
// Assign it to favorites on the first team. The favorites preference gets set for all teams.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// Assign it to favorites on the second team. The favorites preference is already set.
updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updated[0].Channels)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// Remove it from favorites on the first team. This clears the favorites preference for all teams.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
require.Error(t, nErr)
assert.Nil(t, res2)
// Remove it from favorites on the second team. The favorites preference was already deleted.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
require.Error(t, nErr)
assert.Nil(t, res2)
})
t.Run("should not affect other users' favorites preferences", func(t *testing.T) {
userId := model.NewId()
userId2 := model.NewId()
team := setupTeam(t, ss, userId, userId2)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Create the other users' categories
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id)
require.NoError(t, err)
favoritesCategory2 := categories2.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type)
channelsCategory2 := categories2.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory2.Type)
// Have both users join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId2,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Have user1 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
// And user2 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: channelsCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then user1 unfavorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And finally user2 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory2.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create some channels
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// And some categories
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels)
// Try to save the categories with no channels in them
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{},
},
}
updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, nErr)
// The channels should still exist in the category because they would otherwise be orphaned
assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels)
})
t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// The DM should start in the DMs category
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
dmsCategory := initialCategories.Categories[2]
require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels)
// Now move the DM into a custom category
customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
}
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, customCategory.Id, updatedCategories[1].Id)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels)
updatedDmsCategory, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, updatedDmsCategory.Channels)
updatedCustomCategory, err := ss.Channel().GetSidebarCategory(customCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updatedCustomCategory.Channels)
// And move it back out of the custom category
categoriesToUpdate = []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{},
},
}
updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels)
assert.Equal(t, customCategory.Id, updatedCategories[1].Id)
assert.Equal(t, []string{}, updatedCategories[1].Channels)
updatedDmsCategory, err = ss.Channel().GetSidebarCategory(dmsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updatedDmsCategory.Channels)
updatedCustomCategory, err = ss.Channel().GetSidebarCategory(customCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, updatedCustomCategory.Channels)
})
t.Run("should successfully move channels between categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that it includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, nErr)
// Move the channel one way
updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, nErr)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels)
// And then the other
updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, nErr)
assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels)
assert.Equal(t, []string{}, updatedCategories[1].Channels)
})
t.Run("should correctly return the original categories that were modified", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that Channels includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: "originalName",
},
})
require.NoError(t, nErr)
// Rename the custom category
updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Id: customCategory.Id,
DisplayName: "updatedName",
},
},
})
require.NoError(t, nErr)
require.Equal(t, len(updatedCategories), len(originalCategories))
assert.Equal(t, "originalName", originalCategories[0].DisplayName)
assert.Equal(t, "updatedName", updatedCategories[0].DisplayName)
// Move a channel
updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
require.NoError(t, nErr)
require.Equal(t, len(updatedCategories), len(originalCategories))
require.Equal(t, updatedCategories[0].Id, originalCategories[0].Id)
require.Equal(t, updatedCategories[1].Id, originalCategories[1].Id)
assert.Equal(t, []string{channel.Id}, originalCategories[0].Channels)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, []string{}, originalCategories[1].Channels)
assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels)
})
}
func setupInitialSidebarCategories(t *testing.T, ss store.Store) (string, string) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
return userId, team.Id
}
func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should delete all sidebar categories and channels on the team", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
// Create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
_, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(2), count)
// Leave the team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId)
assert.NoError(t, err)
// Confirm that all the categories and SidebarChannel entries have been deleted
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
})
t.Run("should not delete sidebar categories and channels on another the team", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
// Create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
_, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(2), count)
// Leave another team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, model.NewId())
assert.NoError(t, err)
// Confirm that nothing has been deleted
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(2), count)
})
t.Run("MM-30314 should not delete channels on another team under specific circumstances", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
user2 := &model.User{
Id: model.NewId(),
}
// Create a second team and set up the sidebar categories for it
team2 := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
// On the first team, create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, user2)
require.NoError(t, nErr)
_, err = ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Do the same on the second team
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: team2.Id,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().CreateSidebarCategory(userId, team2.Id, &model.SidebarCategoryWithChannels{
Channels: []string{channel2.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(8), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
// Leave the first team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId)
assert.NoError(t, err)
// Confirm that we have the correct number of categories and SidebarChannels entries left over
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(2), count)
// Confirm that the categories on the second team are unchanged
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
assert.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
assert.Equal(t, []string{channel2.Id, dmChannel1.Id}, res.Categories[1].Channels)
})
}
func testDeleteSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should correctly remove an empty category", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
require.NotNil(t, newCategory)
// Ensure that the category was created properly
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
// Then delete it and confirm that was done correctly
err = ss.Channel().DeleteSidebarCategory(newCategory.Id)
assert.NoError(t, err)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
})
t.Run("should correctly remove a category and its channels", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
user := &model.User{
Id: userId,
}
// Create some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel1.Id)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypePrivate,
}, 1000)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel2.Id)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(dmChannel1.Id)
// Assign some of those channels to a custom category
newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, channel2.Id, dmChannel1.Id},
})
require.NoError(t, err)
require.NotNil(t, newCategory)
// Ensure that the categories are set up correctly
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
require.Equal(t, []string{channel1.Id, channel2.Id, dmChannel1.Id}, res.Categories[1].Channels)
// Actually delete the channel
err = ss.Channel().DeleteSidebarCategory(newCategory.Id)
assert.NoError(t, err)
// Confirm that the category was deleted...
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
assert.NoError(t, err)
assert.Len(t, res.Categories, 3)
// ...and that the corresponding SidebarChannel entries were deleted
var count int64
countErr := s.GetMasterX().Get(&count, `
SELECT
COUNT(*)
FROM
SidebarChannels
WHERE
CategoryId = ?`, newCategory.Id)
require.NoError(t, countErr)
assert.Equal(t, int64(0), count)
})
t.Run("should not allow you to remove non-custom categories", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
require.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
require.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
require.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
err = ss.Channel().DeleteSidebarCategory(res.Categories[0].Id)
assert.Error(t, err)
err = ss.Channel().DeleteSidebarCategory(res.Categories[1].Id)
assert.Error(t, err)
err = ss.Channel().DeleteSidebarCategory(res.Categories[2].Id)
assert.Error(t, err)
})
}
func testUpdateSidebarChannelsByPreferences(t *testing.T, ss store.Store) {
t.Run("Should be able to update sidebar channels", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: teamId,
}, 10)
require.NoError(t, nErr)
err := ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{
model.Preference{
Name: channel.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
},
})
assert.NoError(t, err)
})
t.Run("Should not panic if channel is not found", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
require.NotEmpty(t, res)
require.NotPanics(t, func() {
_ = ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{
model.Preference{
Name: "fakeid",
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
},
})
})
})
}
// testSidebarCategoryDeadlock tries to delete and update a category at the same time
// in the hope of triggering a deadlock. This is a best-effort test case, and is not guaranteed
// to catch a bug.
func testSidebarCategoryDeadlock(t *testing.T, ss store.Store) {
userID := model.NewId()
team := setupTeam(t, ss, userID)
// Join a channel
channel, err := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userID,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that it includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userID, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, team.Id)
require.NoError(t, err)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, err := ss.Channel().CreateSidebarCategory(userID, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _, err := ss.Channel().UpdateSidebarCategories(userID, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
if err != nil {
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
}()
go func() {
defer wg.Done()
err := ss.Channel().DeleteSidebarCategory(customCategory.Id)
require.NoError(t, err)
}()
wg.Wait()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestClusterDiscoveryStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testClusterDiscoveryStore(t, ss) })
t.Run("Delete", func(t *testing.T) { testClusterDiscoveryStoreDelete(t, ss) })
t.Run("LastPing", func(t *testing.T) { testClusterDiscoveryStoreLastPing(t, ss) })
t.Run("Exists", func(t *testing.T) { testClusterDiscoveryStoreExists(t, ss) })
t.Run("ClusterDiscoveryGetStore", func(t *testing.T) { testClusterDiscoveryGetStore(t, ss) })
}
func testClusterDiscoveryStore(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname" + model.NewId(),
Type: "test_test",
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
err = ss.ClusterDiscovery().Cleanup()
require.NoError(t, err)
}
func testClusterDiscoveryStoreDelete(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname" + model.NewId(),
Type: "test_test",
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
_, err = ss.ClusterDiscovery().Delete(discovery)
require.NoError(t, err)
}
func testClusterDiscoveryStoreLastPing(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name_lastPing",
Hostname: "hostname" + model.NewId(),
Type: "test_test_lastPing" + model.NewId(),
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
err = ss.ClusterDiscovery().SetLastPingAt(discovery)
require.NoError(t, err)
ttime := model.GetMillis()
time.Sleep(1 * time.Second)
err = ss.ClusterDiscovery().SetLastPingAt(discovery)
require.NoError(t, err)
list, err := ss.ClusterDiscovery().GetAll(discovery.Type, "cluster_name_lastPing")
require.NoError(t, err)
assert.Len(t, list, 1)
require.Less(t, int64(500), list[0].LastPingAt-ttime)
discovery2 := &model.ClusterDiscovery{
ClusterName: "cluster_name_missing",
Hostname: "hostname" + model.NewId(),
Type: "test_test_missing",
}
err = ss.ClusterDiscovery().SetLastPingAt(discovery2)
require.NoError(t, err)
}
func testClusterDiscoveryStoreExists(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name_Exists",
Hostname: "hostname" + model.NewId(),
Type: "test_test_Exists" + model.NewId(),
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
val, err := ss.ClusterDiscovery().Exists(discovery)
require.NoError(t, err)
assert.True(t, val)
discovery.ClusterName = "cluster_name_Exists2"
val, err = ss.ClusterDiscovery().Exists(discovery)
require.NoError(t, err)
assert.False(t, val)
}
func testClusterDiscoveryGetStore(t *testing.T, ss store.Store) {
testType1 := model.NewId()
discovery1 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname1",
Type: testType1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery1))
discovery2 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname2",
Type: testType1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery2))
discovery3 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname3",
Type: testType1,
CreateAt: 1,
LastPingAt: 1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery3))
testType2 := model.NewId()
discovery4 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname1",
Type: testType2,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery4))
list, err := ss.ClusterDiscovery().GetAll(testType1, "cluster_name")
require.NoError(t, err)
assert.Len(t, list, 2)
list, err = ss.ClusterDiscovery().GetAll(testType2, "cluster_name")
require.NoError(t, err)
assert.Len(t, list, 1)
list, err = ss.ClusterDiscovery().GetAll(model.NewId(), "cluster_name")
require.NoError(t, err)
assert.Empty(t, list)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestCommandStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testCommandStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testCommandStoreGet(t, ss) })
t.Run("GetByTeam", func(t *testing.T) { testCommandStoreGetByTeam(t, ss) })
t.Run("GetByTrigger", func(t *testing.T) { testCommandStoreGetByTrigger(t, ss) })
t.Run("Delete", func(t *testing.T) { testCommandStoreDelete(t, ss) })
t.Run("DeleteByTeam", func(t *testing.T) { testCommandStoreDeleteByTeam(t, ss) })
t.Run("DeleteByUser", func(t *testing.T) { testCommandStoreDeleteByUser(t, ss) })
t.Run("Update", func(t *testing.T) { testCommandStoreUpdate(t, ss) })
t.Run("CommandCount", func(t *testing.T) { testCommandCount(t, ss) })
}
func testCommandStoreSave(t *testing.T, ss store.Store) {
o1 := model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
_, nErr := ss.Command().Save(&o1)
require.NoError(t, nErr)
_, err := ss.Command().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testCommandStoreGet(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
_, err := ss.Command().Get("123")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreGetByTeam(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().GetByTeam(o1.TeamId)
require.NoError(t, nErr)
require.NotEmpty(t, r1, "no command returned")
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned command")
result, nErr := ss.Command().GetByTeam("123")
require.NoError(t, nErr)
require.Empty(t, result, "no commands should have returned")
}
func testCommandStoreGetByTrigger(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger1"
o2 := &model.Command{}
o2.CreatorId = model.NewId()
o2.Method = model.CommandMethodPost
o2.TeamId = model.NewId()
o2.URL = "http://nowhere.com/"
o2.Trigger = "trigger1"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
_, nErr = ss.Command().Save(o2)
require.NoError(t, nErr)
var r1 *model.Command
r1, nErr = ss.Command().GetByTrigger(o1.TeamId, o1.Trigger)
require.NoError(t, nErr)
require.Equal(t, r1.Id, o1.Id, "invalid returned command")
nErr = ss.Command().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr)
_, err := ss.Command().GetByTrigger(o1.TeamId, o1.Trigger)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDelete(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDeleteByTeam(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().PermanentDeleteByTeam(o1.TeamId)
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDeleteByUser(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().PermanentDeleteByUser(o1.CreatorId)
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreUpdate(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
o1.Token = model.NewId()
_, nErr = ss.Command().Update(o1)
require.NoError(t, nErr)
o1.URL = "junk"
_, err := ss.Command().Update(o1)
require.Error(t, err)
}
func testCommandCount(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().AnalyticsCommandCount("")
require.NoError(t, nErr)
require.NotZero(t, r1, "should be at least 1 command")
r2, nErr := ss.Command().AnalyticsCommandCount(o1.TeamId)
require.NoError(t, nErr)
require.Equal(t, r2, int64(1), "should be 1 command")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestCommandWebhookStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testCommandWebhookStore(t, ss) })
}
func testCommandWebhookStore(t *testing.T, ss store.Store) {
cws := ss.CommandWebhook()
h1 := &model.CommandWebhook{}
h1.CommandId = model.NewId()
h1.UserId = model.NewId()
h1.ChannelId = model.NewId()
h1, err := cws.Save(h1)
require.NoError(t, err)
var r1 *model.CommandWebhook
r1, nErr := cws.Get(h1.Id)
require.NoError(t, nErr)
assert.Equal(t, *r1, *h1, "invalid returned webhook")
_, nErr = cws.Get("123")
var nfErr *store.ErrNotFound
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for missing id")
h2 := &model.CommandWebhook{}
h2.CreateAt = model.GetMillis() - 2*model.CommandWebhookLifetime
h2.CommandId = model.NewId()
h2.UserId = model.NewId()
h2.ChannelId = model.NewId()
h2, err = cws.Save(h2)
require.NoError(t, err)
_, nErr = cws.Get(h2.Id)
require.Error(t, nErr, "Should have set the status as not found for expired webhook")
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for expired webhook")
cws.Cleanup()
_, nErr = cws.Get(h1.Id)
require.NoError(t, nErr, "Should have no error getting unexpired webhook")
_, nErr = cws.Get(h2.Id)
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for expired webhook")
nErr = cws.TryUse(h1.Id, 1)
require.NoError(t, nErr, "Should be able to use webhook once")
nErr = cws.TryUse(h1.Id, 1)
require.Error(t, nErr, "Should be able to use webhook once")
var invErr *store.ErrInvalidInput
require.True(t, errors.As(nErr, &invErr), "Should be able to use webhook once")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func cleanupStoreState(t *testing.T, ss store.Store) {
//remove existing users
allUsers, err := ss.User().GetAll()
require.NoError(t, err, "error cleaning all test users", err)
for _, u := range allUsers {
err = ss.User().PermanentDelete(u.Id)
require.NoError(t, err, "failed cleaning up test user %s", u.Username)
//remove all posts by this user
nErr := ss.Post().PermanentDeleteByUser(u.Id)
require.NoError(t, nErr, "failed cleaning all posts of test user %s", u.Username)
}
//remove existing channels
allChannels, nErr := ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr, "error cleaning all test channels", nErr)
for _, channel := range allChannels {
nErr = ss.Channel().PermanentDelete(channel.Id)
require.NoError(t, nErr, "failed cleaning up test channel %s", channel.Id)
}
//remove existing teams
allTeams, nErr := ss.Team().GetAll()
require.NoError(t, nErr, "error cleaning all test teams", nErr)
for _, team := range allTeams {
err := ss.Team().PermanentDelete(team.Id)
require.NoError(t, err, "failed cleaning up test team %s", team.Id)
}
}
func TestComplianceStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testComplianceStore(t, ss) })
t.Run("ComplianceExport", func(t *testing.T) { testComplianceExport(t, ss) })
t.Run("ComplianceExportDirectMessages", func(t *testing.T) { testComplianceExportDirectMessages(t, ss) })
t.Run("MessageExportPublicChannel", func(t *testing.T) { testMessageExportPublicChannel(t, ss) })
t.Run("MessageExportPrivateChannel", func(t *testing.T) { testMessageExportPrivateChannel(t, ss) })
t.Run("MessageExportDirectMessageChannel", func(t *testing.T) { testMessageExportDirectMessageChannel(t, ss) })
t.Run("MessageExportGroupMessageChannel", func(t *testing.T) { testMessageExportGroupMessageChannel(t, ss) })
t.Run("MessageEditExportMessage", func(t *testing.T) { testEditExportMessage(t, ss) })
t.Run("MessageEditAfterExportMessage", func(t *testing.T) { testEditAfterExportMessage(t, ss) })
t.Run("MessageDeleteExportMessage", func(t *testing.T) { testDeleteExportMessage(t, ss) })
t.Run("MessageDeleteAfterExportMessage", func(t *testing.T) { testDeleteAfterExportMessage(t, ss) })
}
func testComplianceStore(t *testing.T, ss store.Store) {
compliance1 := &model.Compliance{Desc: "Audit for federal subpoena case #22443", UserId: model.NewId(), Status: model.ComplianceStatusFailed, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.ComplianceTypeAdhoc}
_, err := ss.Compliance().Save(compliance1)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
compliance2 := &model.Compliance{Desc: "Audit for federal subpoena case #11458", UserId: model.NewId(), Status: model.ComplianceStatusRunning, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.ComplianceTypeAdhoc}
_, err = ss.Compliance().Save(compliance2)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
compliances, _ := ss.Compliance().GetAll(0, 1000)
require.Equal(t, model.ComplianceStatusRunning, compliances[0].Status)
require.Equal(t, compliance2.Id, compliances[0].Id)
compliance2.Status = model.ComplianceStatusFailed
_, err = ss.Compliance().Update(compliance2)
require.NoError(t, err)
compliances, _ = ss.Compliance().GetAll(0, 1000)
require.Equal(t, model.ComplianceStatusFailed, compliances[0].Status)
require.Equal(t, compliance2.Id, compliances[0].Id)
compliances, _ = ss.Compliance().GetAll(0, 1)
require.Len(t, compliances, 1)
compliances, _ = ss.Compliance().GetAll(1, 1)
require.Len(t, compliances, 1)
rc2, _ := ss.Compliance().Get(compliance2.Id)
require.Equal(t, compliance2.Status, rc2.Status)
}
func testComplianceExport(t *testing.T, ss store.Store) {
time.Sleep(100 * time.Millisecond)
const (
limit = 30000
)
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.CreateAt = model.GetMillis()
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = u1.Id
o1a.CreateAt = o1.CreateAt + 10
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = u1.Id
o2.CreateAt = o1.CreateAt + 20
o2.Message = NewTestId()
_, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = u2.Id
o2a.CreateAt = o1.CreateAt + 30
o2a.Message = NewTestId()
o2a, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
cr1 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1}
cposts, _, nErr := ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[3].PostId, o2a.Id)
// Test limit
cposts, _, nErr = ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
cr2 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr2, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o2a.Id)
cr3 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr3, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[3].PostId, o2a.Id)
cr4 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr4, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o2a.Id)
cr5 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message + " " + o1.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr5, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
cr6 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email, Keywords: o2a.Message + " " + o1.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr6, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[1].PostId, o2a.Id)
t.Run("multiple batches", func(t *testing.T) {
cr7 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1}
cursor := model.ComplianceExportCursor{}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr7, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[1].PostId, o1a.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr7, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o2.Id)
assert.Equal(t, cposts[1].PostId, o2a.Id)
})
}
func testComplianceExportDirectMessages(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
time.Sleep(100 * time.Millisecond)
const (
limit = 30000
)
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
cDM, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.CreateAt = model.GetMillis()
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = u1.Id
o1a.CreateAt = o1.CreateAt + 10
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = u1.Id
o2.CreateAt = o1.CreateAt + 20
o2.Message = NewTestId()
_, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = u2.Id
o2a.CreateAt = o1.CreateAt + 30
o2a.Message = NewTestId()
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
o3 := &model.Post{}
o3.ChannelId = cDM.Id
o3.UserId = u1.Id
o3.CreateAt = o1.CreateAt + 40
o3.Message = NewTestId()
o3, nErr = ss.Post().Save(o3)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
cr1 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, _, nErr := ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o3.Id)
t.Run("mix of channel and direct messages", func(t *testing.T) {
// This will "cross the boundary" between the two queries
cursor := model.ComplianceExportCursor{}
cr2 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr2, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o1a.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr2, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o2.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o3.Id)
// This will exhaust the first query before moving to the next one
cursor = model.ComplianceExportCursor{}
cr3 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr3, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o2.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr3, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o3.Id)
})
t.Run("timestamp collision", func(t *testing.T) {
time.Sleep(100 * time.Millisecond)
nowMillis := model.GetMillis()
createPost := func(createAt int64) {
post := &model.Post{}
post.ChannelId = c1.Id
post.UserId = u1.Id
post.CreateAt = createAt
post.Message = NewTestId()
_, nErr = ss.Post().Save(post)
require.NoError(t, nErr)
}
for i := 0; i < 3; i++ {
createPost(nowMillis)
}
for i := 0; i < 2; i++ {
createPost(nowMillis + 1)
}
cursor := model.ComplianceExportCursor{}
cr4 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 2}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr4, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
cr5 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 2}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr5, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
// range should be [inclusive, exclusive)
cursor = model.ComplianceExportCursor{}
cr6 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 1}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr6, cursor, 5)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
})
}
func testMessageExportPublicChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts twice in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
post2 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime + 10,
Message: NewTestId(),
}
post2, err = ss.Post().Save(post2)
require.NoError(t, err)
// fetch the message exports for both posts that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId)
assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt)
assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt)
assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
}
func testMessageExportPrivateChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// need a private channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Private Channel",
Type: model.ChannelTypePrivate,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts twice in the private channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
post2 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime + 10,
Message: NewTestId(),
}
post2, err = ss.Post().Save(post2)
require.NoError(t, err)
// fetch the message exports for both posts that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId)
assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt)
assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, channel.Type, *messageExportMap[post1.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt)
assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, channel.Type, *messageExportMap[post2.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
}
func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// as well as a DM channel between those users
directMessageChannel, nErr := ss.Channel().CreateDirectChannel(user1, user2)
require.NoError(t, nErr)
// user1 also sends a DM to user2
post := &model.Post{
ChannelId: directMessageChannel.Id,
UserId: user1.Id,
CreateAt: startTime + 20,
Message: NewTestId(),
}
post, err = ss.Post().Save(post)
require.NoError(t, err)
// fetch the message export for the post that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post is a DM between user1 and user2
// there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
assert.Equal(t, directMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
assert.Equal(t, "Direct Message", *messageExportMap[post.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
}
func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and three users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user3.Id,
}, -1)
require.NoError(t, nErr)
// can't create a group channel directly, because importing app creates an import cycle, so we have to fake it
groupMessageChannel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
Type: model.ChannelTypeGroup,
}
groupMessageChannel, nErr = ss.Channel().Save(groupMessageChannel, -1)
require.NoError(t, nErr)
// user1 posts in the GM
post := &model.Post{
ChannelId: groupMessageChannel.Id,
UserId: user1.Id,
CreateAt: startTime + 20,
Message: NewTestId(),
}
post, err = ss.Post().Save(post)
require.NoError(t, err)
// fetch the message export for the post that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post is a DM between user1 and user2
// there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
assert.Equal(t, groupMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
assert.Equal(t, "Group Message", *messageExportMap[post.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
}
// post,edit,export
func testEditExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
//user 1 edits the previous post
post1e := post1.Clone()
post1e.Message = "edit " + post1.Message
post1e, err = ss.Post().Update(post1e, post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
if *v.PostDeleteAt > 0 {
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
} else {
// post1e was made by user1 in channel1 and team1
assert.Equal(t, post1e.Id, *v.PostId)
assert.Equal(t, post1e.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1e.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1e.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
}
}
// post, export, edit, export
func testEditAfterExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
postEditTime := post1.UpdateAt + 1
//user 1 edits the previous post
post1e := post1.Clone()
post1e.EditAt = postEditTime
post1e.Message = "edit " + post1.Message
post1e, err = ss.Post().Update(post1e, post1)
require.NoError(t, err)
// fetch the message exports after edit
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postEditTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
if *v.PostDeleteAt > 0 {
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
} else {
// post1e was made by user1 in channel1 and team1
assert.Equal(t, post1e.Id, *v.PostId)
assert.Equal(t, post1e.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1e.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1e.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
}
}
// post, delete, export
func testDeleteExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
//user 1 deletes the previous post
postDeleteTime := post1.UpdateAt + 1
err = ss.Post().Delete(post1.Id, postDeleteTime, user1.Id)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was made and deleted by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, postDeleteTime, *v.PostUpdateAt)
assert.NotNil(t, v.PostProps)
props := map[string]any{}
e := json.Unmarshal([]byte(*v.PostProps), &props)
require.NoError(t, e)
_, ok := props[model.PostPropsDeleteBy]
assert.True(t, ok)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
// post,export,delete,export
func testDeleteAfterExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was created by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
//user 1 deletes the previous post
postDeleteTime := post1.UpdateAt + 1
err = ss.Post().Delete(post1.Id, postDeleteTime, user1.Id)
require.NoError(t, err)
// fetch the message exports after delete
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postDeleteTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v = messages[0]
// post1 was created and deleted by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, postDeleteTime, *v.PostUpdateAt)
assert.NotNil(t, v.PostProps)
props := map[string]any{}
e := json.Unmarshal([]byte(*v.PostProps), &props)
require.NoError(t, e)
_, ok := props[model.PostPropsDeleteBy]
assert.True(t, ok)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestDraftStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveDraft", func(t *testing.T) { testSaveDraft(t, ss) })
t.Run("UpdateDraft", func(t *testing.T) { testUpdateDraft(t, ss) })
t.Run("DeleteDraft", func(t *testing.T) { testDeleteDraft(t, ss) })
t.Run("GetDraft", func(t *testing.T) { testGetDraft(t, ss) })
t.Run("GetDraftsForUser", func(t *testing.T) { testGetDraftsForUser(t, ss) })
}
func testSaveDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("save drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Save(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Save(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
}
func testUpdateDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("update drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Update(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Update(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
}
func testDeleteDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("delete drafts", func(t *testing.T) {
err := ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
err = ss.Draft().Delete(user.Id, channel2.Id, "")
assert.NoError(t, err)
_, err = ss.Draft().Get(user.Id, channel.Id, "", false)
require.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
_, err = ss.Draft().Get(user.Id, channel2.Id, "", false)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
})
}
func testGetDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Get(user.Id, channel2.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
t.Run("get draft including deleted", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
err = ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
_, err = ss.Draft().Get(user.Id, channel.Id, "", false)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
draftResp, err = ss.Draft().Get(user.Id, channel.Id, "", true)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
})
}
func testGetDraftsForUser(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().GetDraftsForUser(user.Id, "")
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Equal(t, draft1.Message, draftResp[1].Message)
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmojiStore(t *testing.T, ss store.Store) {
t.Run("EmojiSaveDelete", func(t *testing.T) { testEmojiSaveDelete(t, ss) })
t.Run("EmojiGet", func(t *testing.T) { testEmojiGet(t, ss) })
t.Run("EmojiGetByName", func(t *testing.T) { testEmojiGetByName(t, ss) })
t.Run("EmojiGetMultipleByName", func(t *testing.T) { testEmojiGetMultipleByName(t, ss) })
t.Run("EmojiGetList", func(t *testing.T) { testEmojiGetList(t, ss) })
t.Run("EmojiSearch", func(t *testing.T) { testEmojiSearch(t, ss) })
}
func testEmojiSaveDelete(t *testing.T, ss store.Store) {
emoji1 := &model.Emoji{
CreatorId: model.NewId(),
Name: model.NewId(),
}
_, err := ss.Emoji().Save(emoji1)
require.NoError(t, err)
assert.Len(t, emoji1.Id, 26, "should've set id for emoji")
emoji2 := model.Emoji{
CreatorId: model.NewId(),
Name: emoji1.Name,
}
_, err = ss.Emoji().Save(&emoji2)
require.Error(t, err, "shouldn't be able to save emoji with duplicate name")
err = ss.Emoji().Delete(emoji1, time.Now().Unix())
require.NoError(t, err)
_, err = ss.Emoji().Save(&emoji2)
require.NoError(t, err, "should be able to save emoji with duplicate name now that original has been deleted")
err = ss.Emoji().Delete(&emoji2, time.Now().Unix()+1)
require.NoError(t, err)
}
func testEmojiGet(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
for _, emoji := range emojis {
_, err := ss.Emoji().Get(context.Background(), emoji.Id, false)
require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id)
}
for _, emoji := range emojis {
_, err := ss.Emoji().Get(context.Background(), emoji.Id, true)
require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id)
}
}
func testEmojiGetByName(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
for _, emoji := range emojis {
_, err := ss.Emoji().GetByName(context.Background(), emoji.Name, true)
require.NoErrorf(t, err, "failed to get emoji with name %v", emoji.Name)
}
}
func testEmojiGetMultipleByName(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
t.Run("one emoji", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name})
require.NoError(t, err, "could not get emoji")
require.Len(t, received, 1, "got incorrect emoji")
require.Equal(t, *received[0], emojis[0], "got incorrect emoji")
})
t.Run("multiple emojis", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name, emojis[1].Name, emojis[2].Name})
require.NoError(t, err, "could not get emojis")
require.Len(t, received, 3, "got incorrect emojis")
})
t.Run("one nonexistent emoji", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{"ab"})
require.NoError(t, err, "could not get emoji", err)
require.Empty(t, received, "got incorrect emoji")
})
t.Run("multiple emojis with nonexistent names", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name, emojis[1].Name, emojis[2].Name, "abcd", "1234"})
require.NoError(t, err, "could not get emojis")
require.Len(t, received, 3, "got incorrect emojis")
})
}
func testEmojiGetList(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000a" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000b" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000c" + model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
result, err := ss.Emoji().GetList(0, 100, "")
require.NoError(t, err)
for _, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
require.Truef(t, found, "failed to get emoji with id %v", emoji.Id)
}
remojis, err := ss.Emoji().GetList(0, 3, model.EmojiSortByName)
assert.NoError(t, err)
assert.Equal(t, 3, len(remojis))
assert.Equal(t, emojis[0].Name, remojis[0].Name)
assert.Equal(t, emojis[1].Name, remojis[1].Name)
assert.Equal(t, emojis[2].Name, remojis[2].Name)
remojis, err = ss.Emoji().GetList(1, 2, model.EmojiSortByName)
assert.NoError(t, err)
assert.Equal(t, 2, len(remojis))
assert.Equal(t, emojis[1].Name, remojis[0].Name)
assert.Equal(t, emojis[2].Name, remojis[1].Name)
}
func testEmojiSearch(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: "blargh_" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId() + "_blargh",
},
{
CreatorId: model.NewId(),
Name: model.NewId() + "_blargh_" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
shouldFind := []bool{true, false, false, false}
result, err := ss.Emoji().Search("blargh", true, 100)
require.NoError(t, err)
for i, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
assert.Equal(t, shouldFind[i], found, emoji.Name)
}
shouldFind = []bool{true, true, true, false}
result, err = ss.Emoji().Search("blargh", false, 100)
require.NoError(t, err)
for i, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
assert.Equal(t, shouldFind[i], found, emoji.Name)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"fmt"
"sort"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFileInfoStore(t *testing.T, ss store.Store, s SqlStore) {
t.Cleanup(func() {
s.GetMasterX().Exec("TRUNCATE FileInfo")
})
t.Run("FileInfoSaveGet", func(t *testing.T) { testFileInfoSaveGet(t, ss) })
t.Run("FileInfoSaveGetByPath", func(t *testing.T) { testFileInfoSaveGetByPath(t, ss) })
t.Run("FileInfoGetForPost", func(t *testing.T) { testFileInfoGetForPost(t, ss) })
t.Run("FileInfoGetForUser", func(t *testing.T) { testFileInfoGetForUser(t, ss) })
t.Run("FileInfoGetWithOptions", func(t *testing.T) { testFileInfoGetWithOptions(t, ss) })
t.Run("FileInfoAttachToPost", func(t *testing.T) { testFileInfoAttachToPost(t, ss) })
t.Run("FileInfoDeleteForPost", func(t *testing.T) { testFileInfoDeleteForPost(t, ss) })
t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) })
t.Run("FileInfoPermanentDeleteBatch", func(t *testing.T) { testFileInfoPermanentDeleteBatch(t, ss) })
t.Run("FileInfoPermanentDeleteByUser", func(t *testing.T) { testFileInfoPermanentDeleteByUser(t, ss) })
t.Run("FileInfoUpdateMinipreview", func(t *testing.T) { testFileInfoUpdateMinipreview(t, ss) })
t.Run("GetFilesBatchForIndexing", func(t *testing.T) { testFileInfoStoreGetFilesBatchForIndexing(t, ss) })
t.Run("CountAll", func(t *testing.T) { testFileInfoStoreCountAll(t, ss) })
t.Run("GetStorageUsage", func(t *testing.T) { testFileInfoGetStorageUsage(t, ss) })
t.Run("GetUptoNSizeFileTime", func(t *testing.T) { testGetUptoNSizeFileTime(t, ss, s) })
}
func testFileInfoSaveGet(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
require.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
DeleteAt: 123,
})
require.NoError(t, err)
_, err = ss.FileInfo().Get(info2.Id)
assert.Error(t, err)
defer func() {
ss.FileInfo().PermanentDelete(info2.Id)
}()
}
func testFileInfoSaveGetByPath(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: fmt.Sprintf("%v/file.txt", model.NewId()),
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
assert.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().GetByPath(info.Path)
require.NoError(t, err)
assert.Equal(t, info.Id, rinfo.Id)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
DeleteAt: 123,
})
require.NoError(t, err)
_, err = ss.FileInfo().GetByPath(info2.Id)
assert.Error(t, err)
defer func() {
ss.FileInfo().PermanentDelete(info2.Id)
}()
}
func testFileInfoGetForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
DeleteAt: 123,
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
testCases := []struct {
Name string
PostId string
ReadFromMaster bool
IncludeDeleted bool
AllowFromCache bool
ExpectedPosts int
}{
{
Name: "Fetch from master, without deleted and without cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: false,
AllowFromCache: false,
ExpectedPosts: 2,
},
{
Name: "Fetch from master, with deleted and without cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: true,
AllowFromCache: false,
ExpectedPosts: 3,
},
{
Name: "Fetch from master, with deleted and with cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: true,
AllowFromCache: true,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, without deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: false,
AllowFromCache: false,
ExpectedPosts: 2,
},
{
Name: "Fetch from replica, with deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: true,
AllowFromCache: false,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, with deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: true,
AllowFromCache: true,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, without deleted and with cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: false,
AllowFromCache: true,
ExpectedPosts: 2,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
postInfos, err := ss.FileInfo().GetForPost(
tc.PostId,
tc.ReadFromMaster,
tc.IncludeDeleted,
tc.AllowFromCache,
)
require.NoError(t, err)
assert.Len(t, postInfos, tc.ExpectedPosts)
})
}
}
func testFileInfoGetForUser(t *testing.T, ss store.Store) {
userId := model.NewId()
userId2 := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId2,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
userPosts, err := ss.FileInfo().GetForUser(userId)
require.NoError(t, err)
assert.Len(t, userPosts, 3)
userPosts, err = ss.FileInfo().GetForUser(userId2)
require.NoError(t, err)
assert.Len(t, userPosts, 1)
}
func testFileInfoGetWithOptions(t *testing.T, ss store.Store) {
makePost := func(chId string, user string) *model.Post {
post := model.Post{}
post.ChannelId = chId
post.UserId = user
_, err := ss.Post().Save(&post)
require.NoError(t, err)
return &post
}
makeFile := func(post *model.Post, user string, createAt int64, idPrefix string) model.FileInfo {
id := model.NewId()
id = idPrefix + id[1:] // hacky way to get sortable Ids to confirm secondary Id sort works
fileInfo := model.FileInfo{
Id: id,
CreatorId: user,
Path: "file.txt",
CreateAt: createAt,
}
if post.Id != "" {
fileInfo.PostId = post.Id
}
if post.ChannelId != "" {
fileInfo.ChannelId = post.ChannelId
}
_, err := ss.FileInfo().Save(&fileInfo)
require.NoError(t, err)
return fileInfo
}
userId1 := model.NewId()
userId2 := model.NewId()
channelId1 := model.NewId()
channelId2 := model.NewId()
channelId3 := model.NewId()
post1_1 := makePost(channelId1, userId1) // post 1 by user 1
post1_2 := makePost(channelId3, userId1) // post 2 by user 1
post2_1 := makePost(channelId2, userId2)
post2_2 := makePost(channelId3, userId2)
epoch := time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)
file1_1 := makeFile(post1_1, userId1, epoch.AddDate(0, 0, 1).Unix(), "a") // file 1 by user 1
file1_2 := makeFile(post1_2, userId1, epoch.AddDate(0, 0, 2).Unix(), "b") // file 2 by user 1
file1_3 := makeFile(&model.Post{}, userId1, epoch.AddDate(0, 0, 3).Unix(), "c") // file that is not attached to a post
file2_1 := makeFile(post2_1, userId2, epoch.AddDate(0, 0, 4).Unix(), "d") // file 2 by user 1
file2_2 := makeFile(post2_2, userId2, epoch.AddDate(0, 0, 5).Unix(), "e")
// delete a file
_, err := ss.FileInfo().DeleteForPost(file2_2.PostId)
require.NoError(t, err)
testCases := []struct {
Name string
Page, PerPage int
Opt *model.GetFileInfosOptions
ExpectedFileIds []string
}{
{
Name: "Get files with nil option",
Page: 0,
PerPage: 10,
Opt: nil,
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id},
},
{
Name: "Get files including deleted",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{IncludeDeleted: true},
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id, file2_2.Id},
},
{
Name: "Get files including deleted filtered by channel",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
ChannelIds: []string{channelId3},
},
ExpectedFileIds: []string{file1_2.Id, file2_2.Id},
},
{
Name: "Get files including deleted filtered by channel and user",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
UserIds: []string{userId1},
ChannelIds: []string{channelId3},
},
ExpectedFileIds: []string{file1_2.Id},
},
{
Name: "Get files including deleted sorted by created at",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
SortBy: model.FileinfoSortByCreated,
},
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id, file2_2.Id},
},
{
Name: "Get files filtered by user ordered by created at descending",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
UserIds: []string{userId1},
SortBy: model.FileinfoSortByCreated,
SortDescending: true,
},
ExpectedFileIds: []string{file1_3.Id, file1_2.Id, file1_1.Id},
},
{
Name: "Get all files including deleted ordered by created descending 2nd page of 3 per page ",
Page: 1,
PerPage: 3,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
SortBy: model.FileinfoSortByCreated,
SortDescending: true,
},
ExpectedFileIds: []string{file1_2.Id, file1_1.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
fileInfos, err := ss.FileInfo().GetWithOptions(tc.Page, tc.PerPage, tc.Opt)
require.NoError(t, err)
require.Len(t, fileInfos, len(tc.ExpectedFileIds))
for i := range tc.ExpectedFileIds {
assert.Equal(t, tc.ExpectedFileIds[i], fileInfos[i].Id)
}
})
}
}
type byFileInfoId []*model.FileInfo
func (a byFileInfoId) Len() int { return len(a) }
func (a byFileInfoId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byFileInfoId) Less(i, j int) bool { return a[i].Id < a[j].Id }
func testFileInfoAttachToPost(t *testing.T, ss store.Store) {
t.Run("should attach files", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info1, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file2.txt",
})
require.NoError(t, err)
require.Equal(t, "", info1.PostId)
require.Equal(t, "", info2.PostId)
err = ss.FileInfo().AttachToPost(info1.Id, postId, channelId, userId)
assert.NoError(t, err)
info1.PostId = postId
info1.ChannelId = channelId
err = ss.FileInfo().AttachToPost(info2.Id, postId, channelId, userId)
assert.NoError(t, err)
info2.PostId = postId
info2.ChannelId = channelId
data, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
expected := []*model.FileInfo{info1, info2}
sort.Sort(byFileInfoId(expected))
sort.Sort(byFileInfoId(data))
assert.EqualValues(t, expected, data)
})
t.Run("should not attach files to multiple posts", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
require.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, model.NewId(), channelId, userId)
require.NoError(t, err)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, userId)
require.Error(t, err)
})
t.Run("should not attach files owned from a different user", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
})
require.NoError(t, err)
require.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, userId)
assert.Error(t, err)
})
t.Run("should attach files uploaded by nouser", func(t *testing.T) {
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: "nouser",
Path: "file.txt",
})
require.NoError(t, err)
assert.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, model.NewId())
require.NoError(t, err)
data, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
info.PostId = postId
info.ChannelId = channelId
assert.EqualValues(t, []*model.FileInfo{info}, data)
})
}
func testFileInfoDeleteForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
DeleteAt: 123,
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
_, err := ss.FileInfo().DeleteForPost(postId)
require.NoError(t, err)
infos, err = ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Empty(t, infos)
}
func testFileInfoPermanentDelete(t *testing.T, ss store.Store) {
info, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file.txt",
})
require.NoError(t, err)
err = ss.FileInfo().PermanentDelete(info.Id)
require.NoError(t, err)
}
func testFileInfoPermanentDeleteBatch(t *testing.T, ss store.Store) {
postId := model.NewId()
channelId := model.NewId()
_, err := ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 1200,
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 2000,
})
require.NoError(t, err)
postFiles, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 3)
_, err = ss.FileInfo().PermanentDeleteBatch(1500, 1000)
require.NoError(t, err)
postFiles, err = ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 1)
}
func testFileInfoPermanentDeleteByUser(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
_, err := ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().PermanentDeleteByUser(userId)
require.NoError(t, err)
}
func testFileInfoUpdateMinipreview(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: "image.png",
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
require.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
require.Nil(t, rinfo.MiniPreview)
miniPreview := []byte{0x0, 0x1, 0x2}
rinfo.MiniPreview = &miniPreview
rinfo, err = ss.FileInfo().Upsert(rinfo)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
tinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, tinfo.Id)
require.Equal(t, *tinfo.MiniPreview, miniPreview)
}
func testFileInfoStoreGetFilesBatchForIndexing(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = "zz" + model.NewId() + "b"
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = "zz" + model.NewId() + "b"
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o1, err := ss.Post().Save(o1)
require.NoError(t, err)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o1.Id,
ChannelId: o1.ChannelId,
CreatorId: model.NewId(),
Path: "file1.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f1.Id)
}()
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = c2.Id
o2.UserId = model.NewId()
o2.Message = "zz" + model.NewId() + "CCCCCCCCC"
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
f2, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o2.Id,
ChannelId: o2.ChannelId,
CreatorId: model.NewId(),
Path: "file2.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f2.Id)
}()
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = c1.Id
o3.UserId = model.NewId()
o3.RootId = o1.Id
o3.Message = "zz" + model.NewId() + "QQQQQQQQQQ"
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
f3, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o3.Id,
ChannelId: o3.ChannelId,
CreatorId: model.NewId(),
Path: "file3.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f3.Id)
}()
// Getting all
r, err := ss.FileInfo().GetFilesBatchForIndexing(f1.CreateAt-1, "", 100)
require.NoError(t, err)
require.Len(t, r, 3, "Expected 3 posts in results. Got %v", len(r))
// Testing pagination
r, err = ss.FileInfo().GetFilesBatchForIndexing(f1.CreateAt-1, "", 2)
require.NoError(t, err)
require.Len(t, r, 2, "Expected 2 posts in results. Got %v", len(r))
r, err = ss.FileInfo().GetFilesBatchForIndexing(r[1].CreateAt, r[1].Id, 2)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.FileInfo().GetFilesBatchForIndexing(r[0].CreateAt, r[0].Id, 2)
require.NoError(t, err)
require.Len(t, r, 0, "Expected 0 posts in results. Got %v", len(r))
}
func testFileInfoStoreCountAll(t *testing.T, ss store.Store) {
_, err := ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file1.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file2.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file3.txt",
})
require.NoError(t, err)
count, err := ss.FileInfo().CountAll()
require.NoError(t, err)
require.Equal(t, int64(3), count)
_, err = ss.FileInfo().DeleteForPost(f1.PostId)
require.NoError(t, err)
count, err = ss.FileInfo().CountAll()
require.NoError(t, err)
require.Equal(t, int64(2), count)
}
func testFileInfoGetStorageUsage(t *testing.T, ss store.Store) {
_, err := ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
usage, err := ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(0), usage)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file1.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file2.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file3.txt",
})
require.NoError(t, err)
usage, err = ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(30), usage)
_, err = ss.FileInfo().DeleteForPost(f1.PostId)
require.NoError(t, err)
usage, err = ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(20), usage)
usage, err = ss.FileInfo().GetStorageUsage(false, true)
require.NoError(t, err)
require.Equal(t, int64(30), usage)
}
func testGetUptoNSizeFileTime(t *testing.T, ss store.Store, s SqlStore) {
_, err := ss.FileInfo().GetUptoNSizeFileTime(0)
assert.Error(t, err)
_, err = ss.FileInfo().GetUptoNSizeFileTime(-1)
assert.Error(t, err)
_, err = ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
diff := int64(10000)
now := utils.MillisFromTime(time.Now()) + diff
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file1.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f1.Id)
now = now + diff
f2, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file2.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f2.Id)
now = now + diff
f3, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file3.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f3.Id)
now = now + diff
tmp, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file4.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(tmp.Id)
createAt, err := ss.FileInfo().GetUptoNSizeFileTime(20)
require.NoError(t, err)
assert.Equal(t, f3.CreateAt, createAt)
_, err = ss.FileInfo().GetUptoNSizeFileTime(5)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
createAt, err = ss.FileInfo().GetUptoNSizeFileTime(1000)
require.NoError(t, err)
assert.Equal(t, f1.CreateAt, createAt)
_, err = ss.FileInfo().DeleteForPost(f3.PostId)
require.NoError(t, err)
createAt, err = ss.FileInfo().GetUptoNSizeFileTime(20)
require.NoError(t, err)
assert.Equal(t, f2.CreateAt, createAt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"fmt"
"math"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestGroupStore(t *testing.T, ss store.Store) {
t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) })
t.Run("CreateWithUserIds", func(t *testing.T) { testGroupCreateWithUserIds(t, ss) })
t.Run("Get", func(t *testing.T) { testGroupStoreGet(t, ss) })
t.Run("GetByName", func(t *testing.T) { testGroupStoreGetByName(t, ss) })
t.Run("GetByIDs", func(t *testing.T) { testGroupStoreGetByIDs(t, ss) })
t.Run("GetByRemoteID", func(t *testing.T) { testGroupStoreGetByRemoteID(t, ss) })
t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) })
t.Run("GetByUser", func(t *testing.T) { testGroupStoreGetByUser(t, ss) })
t.Run("Update", func(t *testing.T) { testGroupStoreUpdate(t, ss) })
t.Run("Delete", func(t *testing.T) { testGroupStoreDelete(t, ss) })
t.Run("Restore", func(t *testing.T) { testGroupStoreRestore(t, ss) })
t.Run("GetMemberUsers", func(t *testing.T) { testGroupGetMemberUsers(t, ss) })
t.Run("GetMemberUsersPage", func(t *testing.T) { testGroupGetMemberUsersPage(t, ss) })
t.Run("GetMemberUsersSortedPage", func(t *testing.T) { testGroupGetMemberUsersSortedPage(t, ss) })
t.Run("GetMemberUsersInTeam", func(t *testing.T) { testGroupGetMemberUsersInTeam(t, ss) })
t.Run("GetMemberUsersNotInChannel", func(t *testing.T) { testGroupGetMemberUsersNotInChannel(t, ss) })
t.Run("UpsertMember", func(t *testing.T) { testUpsertMember(t, ss) })
t.Run("UpsertMembers", func(t *testing.T) { testUpsertMembers(t, ss) })
t.Run("DeleteMember", func(t *testing.T) { testGroupDeleteMember(t, ss) })
t.Run("DeleteMembers", func(t *testing.T) { testGroupDeleteMembers(t, ss) })
t.Run("PermanentDeleteMembersByUser", func(t *testing.T) { testGroupPermanentDeleteMembersByUser(t, ss) })
t.Run("CreateGroupSyncable", func(t *testing.T) { testCreateGroupSyncable(t, ss) })
t.Run("GetGroupSyncable", func(t *testing.T) { testGetGroupSyncable(t, ss) })
t.Run("GetAllGroupSyncablesByGroupId", func(t *testing.T) { testGetAllGroupSyncablesByGroup(t, ss) })
t.Run("UpdateGroupSyncable", func(t *testing.T) { testUpdateGroupSyncable(t, ss) })
t.Run("DeleteGroupSyncable", func(t *testing.T) { testDeleteGroupSyncable(t, ss) })
t.Run("TeamMembersToAdd", func(t *testing.T) { testTeamMembersToAdd(t, ss) })
t.Run("TeamMembersToAdd_SingleTeam", func(t *testing.T) { testTeamMembersToAddSingleTeam(t, ss) })
t.Run("ChannelMembersToAdd", func(t *testing.T) { testChannelMembersToAdd(t, ss) })
t.Run("ChannelMembersToAdd_SingleChannel", func(t *testing.T) { testChannelMembersToAddSingleChannel(t, ss) })
t.Run("TeamMembersToRemove", func(t *testing.T) { testTeamMembersToRemove(t, ss) })
t.Run("TeamMembersToRemove_SingleTeam", func(t *testing.T) { testTeamMembersToRemoveSingleTeam(t, ss) })
t.Run("ChannelMembersToRemove", func(t *testing.T) { testChannelMembersToRemove(t, ss) })
t.Run("ChannelMembersToRemove_SingleChannel", func(t *testing.T) { testChannelMembersToRemoveSingleChannel(t, ss) })
t.Run("GetGroupsByChannel", func(t *testing.T) { testGetGroupsByChannel(t, ss) })
t.Run("GetGroupsAssociatedToChannelsByTeam", func(t *testing.T) { testGetGroupsAssociatedToChannelsByTeam(t, ss) })
t.Run("GetGroupsByTeam", func(t *testing.T) { testGetGroupsByTeam(t, ss) })
t.Run("GetGroups", func(t *testing.T) { testGetGroups(t, ss) })
t.Run("TeamMembersMinusGroupMembers", func(t *testing.T) { testTeamMembersMinusGroupMembers(t, ss) })
t.Run("ChannelMembersMinusGroupMembers", func(t *testing.T) { testChannelMembersMinusGroupMembers(t, ss) })
t.Run("GetMemberCount", func(t *testing.T) { groupTestGetMemberCount(t, ss) })
t.Run("AdminRoleGroupsForSyncableMember_Channel", func(t *testing.T) { groupTestAdminRoleGroupsForSyncableMemberChannel(t, ss) })
t.Run("AdminRoleGroupsForSyncableMember_Team", func(t *testing.T) { groupTestAdminRoleGroupsForSyncableMemberTeam(t, ss) })
t.Run("PermittedSyncableAdmins_Team", func(t *testing.T) { groupTestPermittedSyncableAdminsTeam(t, ss) })
t.Run("PermittedSyncableAdmins_Channel", func(t *testing.T) { groupTestPermittedSyncableAdminsChannel(t, ss) })
t.Run("UpdateMembersRole_Team", func(t *testing.T) { groupTestpUpdateMembersRoleTeam(t, ss) })
t.Run("UpdateMembersRole_Channel", func(t *testing.T) { groupTestpUpdateMembersRoleChannel(t, ss) })
t.Run("GroupCount", func(t *testing.T) { groupTestGroupCount(t, ss) })
t.Run("GroupTeamCount", func(t *testing.T) { groupTestGroupTeamCount(t, ss) })
t.Run("GroupChannelCount", func(t *testing.T) { groupTestGroupChannelCount(t, ss) })
t.Run("GroupMemberCount", func(t *testing.T) { groupTestGroupMemberCount(t, ss) })
t.Run("DistinctGroupMemberCount", func(t *testing.T) { groupTestDistinctGroupMemberCount(t, ss) })
t.Run("GroupCountWithAllowReference", func(t *testing.T) { groupTestGroupCountWithAllowReference(t, ss) })
t.Run("GetMember", func(t *testing.T) { groupTestGetMember(t, ss) })
t.Run("GetNonMemberUsersPage", func(t *testing.T) { groupTestGetNonMemberUsersPage(t, ss) })
t.Run("DistinctGroupMemberCountForSource", func(t *testing.T) { groupTestDistinctGroupMemberCountForSource(t, ss) })
}
func testGroupStoreCreate(t *testing.T, ss store.Store) {
// Save a new group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Happy path
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
require.Equal(t, *g1.Name, *d1.Name)
require.Equal(t, g1.DisplayName, d1.DisplayName)
require.Equal(t, g1.Description, d1.Description)
require.Equal(t, g1.RemoteId, d1.RemoteId)
require.NotZero(t, d1.CreateAt)
require.NotZero(t, d1.UpdateAt)
require.Zero(t, d1.DeleteAt)
// Requires display name
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
data, err := ss.Group().Create(g2)
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Won't accept a duplicate name
g4 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g4)
require.NoError(t, err)
g4b := &model.Group{
Name: g4.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
data, err = ss.Group().Create(g4b)
require.Nil(t, data)
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("Group with name %s already exists", *g4b.Name))
// Fields cannot be greater than max values
g5 := &model.Group{
Name: model.NewString(strings.Repeat("x", model.GroupNameMaxLength)),
DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength),
Description: strings.Repeat("x", model.GroupDescriptionMaxLength),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
require.Nil(t, g5.IsValidForCreate())
g5.Name = model.NewString(*g5.Name + "x")
require.Equal(t, g5.IsValidForCreate().Id, "model.group.name.invalid_length.app_error")
g5.Name = model.NewString(model.NewId())
require.Nil(t, g5.IsValidForCreate())
g5.DisplayName = g5.DisplayName + "x"
require.Equal(t, g5.IsValidForCreate().Id, "model.group.display_name.app_error")
g5.DisplayName = model.NewId()
require.Nil(t, g5.IsValidForCreate())
g5.Description = g5.Description + "x"
require.Equal(t, g5.IsValidForCreate().Id, "model.group.description.app_error")
g5.Description = model.NewId()
require.Nil(t, g5.IsValidForCreate())
// Must use a valid type
g6 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSource("fake"),
RemoteId: model.NewString(model.NewId()),
}
require.Equal(t, g6.IsValidForCreate().Id, "model.group.source.app_error")
//must use valid characters
g7 := &model.Group{
Name: model.NewString("%^#@$$"),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
require.Equal(t, g7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error")
}
func testGroupCreateWithUserIds(t *testing.T, ss store.Store) {
// Create user 1
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create user 2
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Save a new group
guids1 := &model.GroupWithUserIds{
Group: *g1,
UserIds: []string{user1.Id, user2.Id},
}
// Happy path
d1, err := ss.Group().CreateWithUserIds(guids1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
require.Equal(t, *guids1.Name, *d1.Name)
require.Equal(t, guids1.DisplayName, d1.DisplayName)
require.Equal(t, guids1.Description, d1.Description)
require.Equal(t, guids1.RemoteId, d1.RemoteId)
require.NotZero(t, d1.CreateAt)
require.NotZero(t, d1.UpdateAt)
require.Zero(t, d1.DeleteAt)
require.Equal(t, *model.NewInt64(2), int64(*d1.MemberCount))
// Requires display name
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceCustom,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
guids2 := &model.GroupWithUserIds{
Group: *g2,
UserIds: []string{user1.Id, user2.Id},
}
data, err := ss.Group().CreateWithUserIds(guids2)
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Won't accept a duplicate name
g4 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids4 := &model.GroupWithUserIds{
Group: *g4,
UserIds: []string{user1.Id, user2.Id},
}
_, err = ss.Group().CreateWithUserIds(guids4)
require.NoError(t, err)
g4b := &model.Group{
Name: g4.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids4b := &model.GroupWithUserIds{
Group: *g4b,
UserIds: []string{user1.Id},
}
data, err = ss.Group().CreateWithUserIds(guids4b)
require.Nil(t, data)
require.Error(t, err)
require.Contains(t, err.Error(), "unique constraint: Name")
// Fields cannot be greater than max values
g5 := &model.Group{
Name: model.NewString(strings.Repeat("x", model.GroupNameMaxLength)),
DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength),
Description: strings.Repeat("x", model.GroupDescriptionMaxLength),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids5 := &model.GroupWithUserIds{
Group: *g5,
}
require.Nil(t, guids5.IsValidForCreate())
guids5.Name = model.NewString(*guids5.Name + "x")
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.name.invalid_length.app_error")
guids5.Name = model.NewString(model.NewId())
require.Nil(t, guids5.IsValidForCreate())
guids5.DisplayName = guids5.DisplayName + "x"
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.display_name.app_error")
guids5.DisplayName = model.NewId()
require.Nil(t, guids5.IsValidForCreate())
guids5.Description = guids5.Description + "x"
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.description.app_error")
guids5.Description = model.NewId()
require.Nil(t, guids5.IsValidForCreate())
// Must use a valid type
g6 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSource("fake"),
RemoteId: model.NewString(model.NewId()),
}
guids6 := &model.GroupWithUserIds{
Group: *g6,
}
require.Equal(t, guids6.IsValidForCreate().Id, "model.group.source.app_error")
//must use valid characters
g7 := &model.Group{
Name: model.NewString("%^#@$$"),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids7 := &model.GroupWithUserIds{
Group: *g7,
}
require.Equal(t, guids7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error")
// Invalid user ids
g8 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids8 := &model.GroupWithUserIds{
Group: *g8,
UserIds: []string{"1234uid"},
}
data, err = ss.Group().CreateWithUserIds(guids8)
require.Nil(t, data)
require.Error(t, err)
require.Equal(t, store.NewErrNotFound("User", "1234uid"), err)
}
func testGroupStoreGet(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().Get(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetByName(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g1Opts := model.GroupSearchOpts{
FilterAllowReference: false,
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().GetByName(*d1.Name, g1Opts)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().GetByName(model.NewId(), g1Opts)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetByIDs(t *testing.T, ss store.Store) {
var group1 *model.Group
var group2 *model.Group
for i := 0; i < 2; i++ {
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
switch i {
case 0:
group1 = group
case 1:
group2 = group
}
}
groups, err := ss.Group().GetByIDs([]string{group1.Id, group2.Id})
require.NoError(t, err)
require.Len(t, groups, 2)
for i := 0; i < 2; i++ {
require.True(t, (groups[i].Id == group1.Id || groups[i].Id == group2.Id))
}
require.True(t, groups[0].Id != groups[1].Id)
}
func testGroupStoreGetByRemoteID(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().GetByRemoteID(*d1.RemoteId, model.GroupSourceLdap)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().GetByRemoteID(model.NewId(), model.GroupSource("fake"))
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetAllByType(t *testing.T, ss store.Store) {
numGroups := 10
groups := []*model.Group{}
// Create groups
for i := 0; i < numGroups; i++ {
g := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
groups = append(groups, g)
_, err := ss.Group().Create(g)
require.NoError(t, err)
}
// Returns all the groups
d1, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
require.Condition(t, func() bool { return len(d1) >= numGroups }, len(d1), ">=", numGroups)
for _, expectedGroup := range groups {
present := false
for _, dbGroup := range d1 {
if dbGroup.Id == expectedGroup.Id {
present = true
break
}
}
require.True(t, present)
}
}
func testGroupStoreGetByUser(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g1, err := ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g2, err = ss.Group().Create(g2)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u2, nErr = ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
groups, err := ss.Group().GetByUser(u1.Id)
require.NoError(t, err)
assert.Equal(t, 2, len(groups))
found1 := false
found2 := false
for _, g := range groups {
if g.Id == g1.Id {
found1 = true
}
if g.Id == g2.Id {
found2 = true
}
}
assert.True(t, found1)
assert.True(t, found2)
groups, err = ss.Group().GetByUser(u2.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groups))
assert.Equal(t, g2.Id, groups[0].Id)
groups, err = ss.Group().GetByUser(model.NewId())
require.NoError(t, err)
assert.Equal(t, 0, len(groups))
}
func testGroupStoreUpdate(t *testing.T, ss store.Store) {
// Save a new group
g1 := &model.Group{
Name: model.NewString("g1-test"),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Create a group
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
// Update happy path
g1Update := &model.Group{}
*g1Update = *g1
g1Update.Name = model.NewString(model.NewId())
g1Update.DisplayName = model.NewId()
g1Update.Description = model.NewId()
g1Update.RemoteId = model.NewString(model.NewId())
ud1, err := ss.Group().Update(g1Update)
require.NoError(t, err)
// Not changed...
require.Equal(t, d1.Id, ud1.Id)
require.Equal(t, d1.CreateAt, ud1.CreateAt)
require.Equal(t, d1.Source, ud1.Source)
// Still zero...
require.Zero(t, ud1.DeleteAt)
// Updated...
require.Equal(t, *g1Update.Name, *ud1.Name)
require.Equal(t, g1Update.DisplayName, ud1.DisplayName)
require.Equal(t, g1Update.Description, ud1.Description)
require.Equal(t, g1Update.RemoteId, ud1.RemoteId)
// Requires display name
data, err := ss.Group().Update(&model.Group{
Id: d1.Id,
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Create another Group
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
d2, err := ss.Group().Create(g2)
require.NoError(t, err)
// Can't update the name to be a duplicate of an existing group's name
_, err = ss.Group().Update(&model.Group{
Id: d2.Id,
Name: g1Update.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
})
require.Error(t, err)
require.Contains(t, err.Error(), "unique constraint: Name")
// Cannot update CreateAt
someVal := model.GetMillis()
d1.CreateAt = someVal
d3, err := ss.Group().Update(d1)
require.NoError(t, err)
require.NotEqual(t, someVal, d3.CreateAt)
// Cannot update DeleteAt to non-zero
d1.DeleteAt = 1
_, err = ss.Group().Update(d1)
require.Error(t, err)
require.Contains(t, err.Error(), "DeleteAt should be 0 when updating")
//...except for 0 for DeleteAt
d1.DeleteAt = 0
d4, err := ss.Group().Update(d1)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
}
func testGroupStoreDelete(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Check the group is retrievable
_, err = ss.Group().Get(d1.Id)
require.NoError(t, err)
// Get the before count
d7, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
beforeCount := len(d7)
// Delete the group
_, err = ss.Group().Delete(d1.Id)
require.NoError(t, err)
// Check the group is deleted
d4, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.NotZero(t, d4.DeleteAt)
// Check the after count
d5, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
afterCount := len(d5)
require.Condition(t, func() bool { return beforeCount == afterCount+1 }, beforeCount, "==", afterCount+1)
// Try and delete a nonexistent group
_, err = ss.Group().Delete(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Cannot delete again
_, err = ss.Group().Delete(d1.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreRestore(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Check the group is retrievable
_, err = ss.Group().Get(d1.Id)
require.NoError(t, err)
// Delete the group
_, err = ss.Group().Delete(d1.Id)
require.NoError(t, err)
// Get the before count
d7, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
beforeCount := len(d7)
// restore the group
_, err = ss.Group().Restore(d1.Id)
require.NoError(t, err)
// Check the group is restored
d4, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
// Check the after count
d5, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
afterCount := len(d5)
require.Condition(t, func() bool { return beforeCount == afterCount-1 })
// Try and restore a nonexistent group
_, err = ss.Group().Delete(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Cannot restore again
_, err = ss.Group().Restore(d1.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupGetMemberUsers(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
// Check returns members
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
// Check madeup id
groupMembers, err = ss.Group().GetMemberUsers(model.NewId())
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// Delete a member
_, err = ss.Group().DeleteMember(group.Id, user1.Id)
require.NoError(t, err)
// Should not return deleted members
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
}
func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: "user1" + model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: "user2" + model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: "user3" + model.NewId(),
}
user3, nErr := ss.User().Save(u3)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Check returns members
groupMembers, err := ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
// Check page 1
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 2, nil)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers)
// Check page 2
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 1, 2, nil)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3}, groupMembers)
// Check madeup id
groupMembers, err = ss.Group().GetMemberUsersPage(model.NewId(), 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// Delete a member
_, err = ss.Group().DeleteMember(group.Id, user1.Id)
require.NoError(t, err)
// Should not return deleted members
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
}
func testGroupGetMemberUsersSortedPage(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// First by nickname, third by full name, second by username
u1 := &model.User{
Email: MakeEmail(),
Username: "y" + model.NewId(),
Nickname: "a" + model.NewId(),
FirstName: "z" + model.NewId(),
LastName: "z" + model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
// Second by nickname, first by full name, third by username
u2 := &model.User{
Email: MakeEmail(),
Username: "z" + model.NewId(),
FirstName: "b" + model.NewId(),
LastName: "b" + model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
// Third by nickname, second by full name, first by username
u3 := &model.User{
Email: MakeEmail(),
Username: "d" + model.NewId(),
}
user3, nErr := ss.User().Save(u3)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Check nickname ordering, paged
groupMembers, err := ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowNicknameFullName)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowNicknameFullName)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3}, groupMembers)
// Check full name ordering, paged
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowFullName)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user2, user3}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowFullName)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1}, groupMembers)
// Check username ordering
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowUsername)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3, user1}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowUsername)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user2}, groupMembers)
}
func testGroupGetMemberUsersInTeam(t *testing.T, ss store.Store) {
// Save a team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// returns no members when team does not exist
groupMembers, err := ss.Group().GetMemberUsersInTeam(group.Id, "non-existent-channel-id")
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// returns no members when group has no members in the team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
m1 := &model.TeamMember{TeamId: team.Id, UserId: user1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
// returns single member in team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
m2 := &model.TeamMember{TeamId: team.Id, UserId: user2.Id}
m3 := &model.TeamMember{TeamId: team.Id, UserId: user3.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
// returns all members when all members are in team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
}
func testGroupGetMemberUsersNotInChannel(t *testing.T, ss store.Store) {
// Save a team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Create Channel
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
// returns no members when channel does not exist
groupMembers, err := ss.Group().GetMemberUsersNotInChannel(group.Id, "non-existent-channel-id")
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// returns no members when group has no members in the team that the channel belongs to
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
m1 := &model.TeamMember{TeamId: team.Id, UserId: user1.Id}
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
// returns single member in team and not in channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
m2 := &model.TeamMember{TeamId: team.Id, UserId: user2.Id}
m3 := &model.TeamMember{TeamId: team.Id, UserId: user3.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
// returns all members when all members are in team and not in channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
cm1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user1.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(cm1)
require.NoError(t, err)
// returns both members not yet added to channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
cm2 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user2.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm3 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user3.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(cm3)
require.NoError(t, err)
// returns none when all members have been added to team and channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
}
func testUpsertMember(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Happy path
d2, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
require.Equal(t, d2.GroupId, group.Id)
require.Equal(t, d2.UserId, user.Id)
require.NotZero(t, d2.CreateAt)
require.Zero(t, d2.DeleteAt)
// Duplicate composite key (GroupId, UserId)
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Invalid GroupId
_, err = ss.Group().UpsertMember(model.NewId(), user.Id)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to get UserGroup with")
// Restores a deleted member
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
beforeRestoreCount := len(groupMembers)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
afterRestoreCount := len(groupMembers)
require.Equal(t, beforeRestoreCount+1, afterRestoreCount)
}
func testUpsertMembers(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create user
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
// Happy path
m, err := ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
require.Equal(t, 2, len(m))
// Duplicate composite key (GroupId, UserId)
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
// time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id})
require.NoError(t, err)
// Invalid GroupId
_, err = ss.Group().UpsertMembers(model.NewId(), []string{user.Id})
require.Error(t, err)
require.Contains(t, err.Error(), "failed to get UserGroup with")
// Restores a deleted member
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
_, err = ss.Group().DeleteMembers(group.Id, []string{user.Id})
require.NoError(t, err)
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
beforeRestoreCount := len(groupMembers)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
afterRestoreCount := len(groupMembers)
require.Equal(t, beforeRestoreCount+1, afterRestoreCount)
}
func testGroupDeleteMember(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create member
d1, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Happy path
d2, err := ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
require.Equal(t, d2.GroupId, group.Id)
require.Equal(t, d2.UserId, user.Id)
require.Equal(t, d2.CreateAt, d1.CreateAt)
require.NotZero(t, d2.DeleteAt)
// Delete an already deleted member
_, err = ss.Group().DeleteMember(group.Id, user.Id)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Delete with non-existent User
_, err = ss.Group().DeleteMember(group.Id, model.NewId())
require.True(t, errors.As(err, &nfErr))
// Delete non-existent Group
_, err = ss.Group().DeleteMember(model.NewId(), group.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupDeleteMembers(t *testing.T, ss store.Store) {
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
guids := &model.GroupWithUserIds{
Group: *g1,
UserIds: []string{user.Id},
}
group, err := ss.Group().CreateWithUserIds(guids)
require.NoError(t, err)
// Happy path
d2, err := ss.Group().DeleteMembers(group.Id, []string{user.Id})
require.NoError(t, err)
require.Equal(t, d2[0].GroupId, group.Id)
require.Equal(t, d2[0].UserId, user.Id)
require.NotZero(t, d2[0].DeleteAt)
// Delete an already deleted member
_, err = ss.Group().DeleteMembers(group.Id, []string{user.Id})
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Delete with non-existent User
_, err = ss.Group().DeleteMembers(group.Id, []string{model.NewId()})
require.True(t, errors.As(err, &nfErr))
// Delete non-existent Group
_, err = ss.Group().DeleteMembers(model.NewId(), []string{user.Id})
require.True(t, errors.As(err, &nfErr))
}
func testGroupPermanentDeleteMembersByUser(t *testing.T, ss store.Store) {
var g *model.Group
var groups []*model.Group
numberOfGroups := 5
for i := 0; i < numberOfGroups; i++ {
g = &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g)
groups = append(groups, group)
require.NoError(t, err)
}
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(u1)
require.NoError(t, err)
// Create members
for _, group := range groups {
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
}
// Happy path
err = ss.Group().PermanentDeleteMembersByUser(user.Id)
require.NoError(t, err)
}
func testCreateGroupSyncable(t *testing.T, ss store.Store) {
// Invalid GroupID
_, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam("x", model.NewId(), false))
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group_syncable.group_id.app_error")
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// New GroupSyncable, happy path
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
d1, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
require.Equal(t, gt1.SyncableId, d1.SyncableId)
require.Equal(t, gt1.GroupId, d1.GroupId)
require.Equal(t, gt1.AutoAdd, d1.AutoAdd)
require.NotZero(t, d1.CreateAt)
require.Zero(t, d1.DeleteAt)
}
func testGetGroupSyncable(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// Create GroupSyncable
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
groupTeam, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Get GroupSyncable
dgt, err := ss.Group().GetGroupSyncable(groupTeam.GroupId, groupTeam.SyncableId, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.Equal(t, gt1.GroupId, dgt.GroupId)
require.Equal(t, gt1.SyncableId, dgt.SyncableId)
require.Equal(t, gt1.AutoAdd, dgt.AutoAdd)
require.NotZero(t, gt1.CreateAt)
require.NotZero(t, gt1.UpdateAt)
require.Zero(t, gt1.DeleteAt)
}
func testGetAllGroupSyncablesByGroup(t *testing.T, ss store.Store) {
numGroupSyncables := 10
// Create group
g := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g)
require.NoError(t, err)
groupTeams := []*model.GroupSyncable{}
// Create groupTeams
for i := 0; i < numGroupSyncables; i++ {
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
var team *model.Team
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// create groupteam
var groupTeam *model.GroupSyncable
gt := model.NewGroupTeam(group.Id, team.Id, false)
gt.SchemeAdmin = true
groupTeam, err = ss.Group().CreateGroupSyncable(gt)
require.NoError(t, err)
groupTeams = append(groupTeams, groupTeam)
}
// Returns all the group teams
d1, err := ss.Group().GetAllGroupSyncablesByGroupId(group.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.Condition(t, func() bool { return len(d1) >= numGroupSyncables }, len(d1), ">=", numGroupSyncables)
for _, expectedGroupTeam := range groupTeams {
present := false
for _, dbGroupTeam := range d1 {
if dbGroupTeam.GroupId == expectedGroupTeam.GroupId && dbGroupTeam.SyncableId == expectedGroupTeam.SyncableId {
require.True(t, dbGroupTeam.SchemeAdmin)
present = true
break
}
}
require.True(t, present)
}
}
func testUpdateGroupSyncable(t *testing.T, ss store.Store) {
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// New GroupSyncable, happy path
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
d1, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Update existing group team
gt1.AutoAdd = true
d2, err := ss.Group().UpdateGroupSyncable(gt1)
require.NoError(t, err)
require.True(t, d2.AutoAdd)
// Non-existent Group
gt2 := model.NewGroupTeam(model.NewId(), team.Id, false)
_, err = ss.Group().UpdateGroupSyncable(gt2)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Non-existent Team
gt3 := model.NewGroupTeam(group.Id, model.NewId(), false)
_, err = ss.Group().UpdateGroupSyncable(gt3)
require.True(t, errors.As(err, &nfErr))
// Cannot update CreateAt or DeleteAt
origCreateAt := d1.CreateAt
d1.CreateAt = model.GetMillis()
d1.AutoAdd = true
d3, err := ss.Group().UpdateGroupSyncable(d1)
require.NoError(t, err)
require.Equal(t, origCreateAt, d3.CreateAt)
// Cannot update DeleteAt to arbitrary value
d1.DeleteAt = 1
_, err = ss.Group().UpdateGroupSyncable(d1)
require.Error(t, err)
require.Contains(t, err.Error(), "DeleteAt should be 0 when updating")
// Can update DeleteAt to 0
d1.DeleteAt = 0
d4, err := ss.Group().UpdateGroupSyncable(d1)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
}
func testDeleteGroupSyncable(t *testing.T, ss store.Store) {
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// Create GroupSyncable
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
groupTeam, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Non-existent Group
_, err = ss.Group().DeleteGroupSyncable(model.NewId(), groupTeam.SyncableId, model.GroupSyncableTypeTeam)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Non-existent Team
_, err = ss.Group().DeleteGroupSyncable(groupTeam.GroupId, model.NewId(), model.GroupSyncableTypeTeam)
require.True(t, errors.As(err, &nfErr))
// Happy path...
d1, err := ss.Group().DeleteGroupSyncable(groupTeam.GroupId, groupTeam.SyncableId, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.NotZero(t, d1.DeleteAt)
require.Equal(t, d1.GroupId, groupTeam.GroupId)
require.Equal(t, d1.SyncableId, groupTeam.SyncableId)
require.Equal(t, d1.AutoAdd, groupTeam.AutoAdd)
require.Equal(t, d1.CreateAt, groupTeam.CreateAt)
require.Condition(t, func() bool { return d1.UpdateAt >= groupTeam.UpdateAt }, d1.UpdateAt, ">=", groupTeam.UpdateAt)
// Record already deleted
_, err = ss.Group().DeleteGroupSyncable(d1.GroupId, d1.SyncableId, d1.Type)
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
}
func testTeamMembersToAdd(t *testing.T, ss store.Store) {
// Create Group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// Create User
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(user)
require.NoError(t, nErr)
// Create GroupMember
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Create Team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
// Create GroupTeam
syncable, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, team.Id, true))
require.NoError(t, err)
// Time before syncable was created
teamMembers, err := ss.Group().TeamMembersToAdd(syncable.CreateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, user.Id, teamMembers[0].UserID)
require.Equal(t, team.Id, teamMembers[0].TeamID)
// Time after syncable was created
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Delete and restore GroupMember should return result
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
pristineSyncable := *syncable
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
// Time before syncable was updated
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.UpdateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, user.Id, teamMembers[0].UserID)
require.Equal(t, team.Id, teamMembers[0].TeamID)
// Time after syncable was updated
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.UpdateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Only includes if auto-add
syncable.AutoAdd = false
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of syncable and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if Group deleted
_, err = ss.Group().Delete(group.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of group and verify
group.DeleteAt = 0
_, err = ss.Group().Update(group)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if Team deleted
team.DeleteAt = model.GetMillis()
team, nErr = ss.Team().Update(team)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of team and verify
team.DeleteAt = 0
team, nErr = ss.Team().Update(team)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if GroupTeam deleted
_, err = ss.Group().DeleteGroupSyncable(group.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset GroupTeam and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if GroupMember deleted
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// restore group member and verify
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// adding team membership stops returning result
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
}, 999)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Leaving Team should still not return result
_, nErr = ss.Team().UpdateMember(&model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
DeleteAt: model.GetMillis(),
})
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// If includeRemovedMembers is set to true, removed members should be added back in
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, true)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testTeamMembersToAddSingleTeam(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(user1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr = ss.User().Save(user2)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, nErr = ss.User().Save(user3)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
}
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
team1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, nErr = ss.Team().Save(team1)
require.NoError(t, nErr)
team2 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team2, nErr = ss.Team().Save(team2)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group1.Id, team1.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group2.Id, team2.Id, true))
require.NoError(t, err)
teamMembers, err := ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
teamMembers, err = ss.Group().TeamMembersToAdd(0, &team1.Id, false)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
teamMembers, err = ss.Group().TeamMembersToAdd(0, &team2.Id, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testChannelMembersToAdd(t *testing.T, ss store.Store) {
// Create Group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "ChannelMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// Create User
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(user)
require.NoError(t, nErr)
// Create GroupMember
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Create Channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, nErr = ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
// Create GroupChannel
syncable, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channel.Id, true))
require.NoError(t, err)
// Time before syncable was created
channelMembers, err := ss.Group().ChannelMembersToAdd(syncable.CreateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, user.Id, channelMembers[0].UserID)
require.Equal(t, channel.Id, channelMembers[0].ChannelID)
// Time after syncable was created
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Delete and restore GroupMember should return result
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
pristineSyncable := *syncable
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
// Time before syncable was updated
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.UpdateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, user.Id, channelMembers[0].UserID)
require.Equal(t, channel.Id, channelMembers[0].ChannelID)
// Time after syncable was updated
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.UpdateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Only includes if auto-add
syncable.AutoAdd = false
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of syncable and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if Group deleted
_, err = ss.Group().Delete(group.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of group and verify
group.DeleteAt = 0
_, err = ss.Group().Update(group)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if Channel deleted
nErr = ss.Channel().Delete(channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of channel and verify
channel.DeleteAt = 0
_, nErr = ss.Channel().Update(channel)
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if GroupChannel deleted
_, err = ss.Group().DeleteGroupSyncable(group.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset GroupChannel and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if GroupMember deleted
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// restore group member and verify
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// Adding Channel (ChannelMemberHistory) should stop returning result
nErr = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Leaving Channel (ChannelMemberHistory) should still not return result
nErr = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Purging ChannelMemberHistory re-returns the result
_, _, nErr = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis()+1, 100, model.RetentionPolicyCursor{})
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// If includeRemovedMembers is set to true, removed members should be added back in
nErr = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, true)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
func testChannelMembersToAddSingleChannel(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(user1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr = ss.User().Save(user2)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, nErr = ss.User().Save(user3)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
}
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
channel1 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
}
channel1, nErr = ss.Channel().Save(channel1, 999)
require.NoError(t, nErr)
channel2 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
}
channel2, nErr = ss.Channel().Save(channel2, 999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group1.Id, channel1.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group2.Id, channel2.Id, true))
require.NoError(t, err)
channelMembers, err := ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.GreaterOrEqual(t, len(channelMembers), 3)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, &channel1.Id, false)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, &channel2.Id, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
func testTeamMembersToRemove(t *testing.T, ss store.Store) {
data := pendingMemberRemovalsDataSetup(t, ss)
// one result when both users are in the group (for user C)
teamMembers, err := ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, data.UserC.Id, teamMembers[0].UserId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserB.Id)
require.NoError(t, err)
// user b and c should now be returned
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
var userIDs []string
for _, item := range teamMembers {
userIDs = append(userIDs, item.UserId)
}
require.Contains(t, userIDs, data.UserB.Id)
require.Contains(t, userIDs, data.UserC.Id)
require.Equal(t, data.ConstrainedTeam.Id, teamMembers[0].TeamId)
require.Equal(t, data.ConstrainedTeam.Id, teamMembers[1].TeamId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserA.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
// Make one of them a bot
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
teamMember := teamMembers[0]
bot := &model.Bot{
UserId: teamMember.UserId,
Username: "un_" + model.NewId(),
DisplayName: "dn_" + model.NewId(),
OwnerId: teamMember.UserId,
}
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// verify that bot is not returned in results
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
// delete the bot
nErr = ss.Bot().PermanentDelete(bot.UserId)
require.NoError(t, nErr)
// Should be back to 3 users
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
// add users back to groups
res := ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserA.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserB.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserC.Id)
require.NoError(t, res)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserA.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserB.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserC.Id)
require.NoError(t, nErr)
}
func testTeamMembersToRemoveSingleTeam(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
team1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team1, nErr := ss.Team().Save(team1)
require.NoError(t, nErr)
team2 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team2, nErr = ss.Team().Save(team2)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team1.Id,
UserId: user.Id,
}, 999)
require.NoError(t, nErr)
}
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team2.Id,
UserId: user3.Id,
}, 999)
require.NoError(t, nErr)
teamMembers, err := ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
teamMembers, err = ss.Group().TeamMembersToRemove(&team1.Id)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
teamMembers, err = ss.Group().TeamMembersToRemove(&team2.Id)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testChannelMembersToRemove(t *testing.T, ss store.Store) {
data := pendingMemberRemovalsDataSetup(t, ss)
// one result when both users are in the group (for user C)
channelMembers, err := ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, data.UserC.Id, channelMembers[0].UserId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserB.Id)
require.NoError(t, err)
// user b and c should now be returned
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
var userIDs []string
for _, item := range channelMembers {
userIDs = append(userIDs, item.UserId)
}
require.Contains(t, userIDs, data.UserB.Id)
require.Contains(t, userIDs, data.UserC.Id)
require.Equal(t, data.ConstrainedChannel.Id, channelMembers[0].ChannelId)
require.Equal(t, data.ConstrainedChannel.Id, channelMembers[1].ChannelId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserA.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
// Make one of them a bot
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
channelMember := channelMembers[0]
bot := &model.Bot{
UserId: channelMember.UserId,
Username: "un_" + model.NewId(),
DisplayName: "dn_" + model.NewId(),
OwnerId: channelMember.UserId,
}
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// verify that bot is not returned in results
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
// delete the bot
nErr = ss.Bot().PermanentDelete(bot.UserId)
require.NoError(t, nErr)
// Should be back to 3 users
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
// add users back to groups
res := ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserA.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserB.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserC.Id)
require.NoError(t, res)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserA.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserB.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserC.Id)
require.NoError(t, nErr)
}
func testChannelMembersToRemoveSingleChannel(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
channel1 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
GroupConstrained: model.NewBool(true),
}
channel1, nErr := ss.Channel().Save(channel1, 999)
require.NoError(t, nErr)
channel2 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
GroupConstrained: model.NewBool(true),
}
channel2, nErr = ss.Channel().Save(channel2, 999)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: user3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channelMembers, err := ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
channelMembers, err = ss.Group().ChannelMembersToRemove(&channel1.Id)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
channelMembers, err = ss.Group().ChannelMembersToRemove(&channel2.Id)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
type removalsData struct {
UserA *model.User
UserB *model.User
UserC *model.User
ConstrainedChannel *model.Channel
UnconstrainedChannel *model.Channel
ConstrainedTeam *model.Team
UnconstrainedTeam *model.Team
Group *model.Group
}
func pendingMemberRemovalsDataSetup(t *testing.T, ss store.Store) *removalsData {
// create group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "Pending[Channel|Team]MemberRemovals Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// create users
// userA will get removed from the group
userA := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userA, nErr := ss.User().Save(userA)
require.NoError(t, nErr)
// userB will not get removed from the group
userB := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userB, nErr = ss.User().Save(userB)
require.NoError(t, nErr)
// userC was never in the group
userC := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userC, nErr = ss.User().Save(userC)
require.NoError(t, nErr)
// add users to group (but not userC)
_, err = ss.Group().UpsertMember(group.Id, userA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, userB.Id)
require.NoError(t, err)
// create channels
channelConstrained := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
channelConstrained, nErr = ss.Channel().Save(channelConstrained, 9999)
require.NoError(t, nErr)
channelUnconstrained := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channelUnconstrained, nErr = ss.Channel().Save(channelUnconstrained, 9999)
require.NoError(t, nErr)
// create teams
teamConstrained := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
}
teamConstrained, nErr = ss.Team().Save(teamConstrained)
require.NoError(t, nErr)
teamUnconstrained := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid1",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
teamUnconstrained, nErr = ss.Team().Save(teamUnconstrained)
require.NoError(t, nErr)
// create groupteams
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, teamConstrained.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, teamUnconstrained.Id, true))
require.NoError(t, err)
// create groupchannels
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channelConstrained.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channelUnconstrained.Id, true))
require.NoError(t, err)
// add users to teams
userIDTeamIDs := [][]string{
{userA.Id, teamConstrained.Id},
{userB.Id, teamConstrained.Id},
{userC.Id, teamConstrained.Id},
{userA.Id, teamUnconstrained.Id},
{userB.Id, teamUnconstrained.Id},
{userC.Id, teamUnconstrained.Id},
}
for _, item := range userIDTeamIDs {
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: item[0],
TeamId: item[1],
}, 99)
require.NoError(t, nErr)
}
// add users to channels
userIDChannelIDs := [][]string{
{userA.Id, channelConstrained.Id},
{userB.Id, channelConstrained.Id},
{userC.Id, channelConstrained.Id},
{userA.Id, channelUnconstrained.Id},
{userB.Id, channelUnconstrained.Id},
{userC.Id, channelUnconstrained.Id},
}
for _, item := range userIDChannelIDs {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: item[0],
ChannelId: item[1],
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
return &removalsData{
UserA: userA,
UserB: userB,
UserC: userC,
ConstrainedChannel: channelConstrained,
UnconstrainedChannel: channelUnconstrained,
ConstrainedTeam: teamConstrained,
UnconstrainedTeam: teamUnconstrained,
Group: group,
}
}
func testGetGroupsByChannel(t *testing.T, ss store.Store) {
// Create Channel1
channel1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel1, err := ss.Channel().Save(channel1, 9999)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Channel1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Channel2
channel2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel2, nErr := ss.Channel().Save(channel2, 9999)
require.NoError(t, nErr)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
ChannelId string
Page int
PerPage int
Result []*model.GroupWithSchemeAdmin
Opts model.GroupSearchOpts
TotalCount *int64
}{
{
Name: "Get the two Groups for Channel1",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Get first Group for Channel1 with page 0 with 1 element",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
{
Name: "Get second Group for Channel1 with page 1 with 1 element",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
{
Name: "Get third Group for Channel2",
ChannelId: channel2.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group3WSA},
},
{
Name: "Get empty Groups for a fake id",
ChannelId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{},
TotalCount: model.NewInt64(0),
},
{
Name: "Get group matching name",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low change of a name collision
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching display name",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching multiple display names",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Include member counts",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 2,
Result: []*model.GroupWithSchemeAdmin{
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
{
Name: "Include allow reference",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsByChannel(tc.ChannelId, tc.Opts)
require.NoError(t, err)
require.ElementsMatch(t, tc.Result, groups)
if tc.TotalCount != nil {
var count int64
count, err = ss.Group().CountGroupsByChannel(tc.ChannelId, tc.Opts)
require.NoError(t, err)
require.Equal(t, *tc.TotalCount, count)
}
})
}
}
func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, errt := ss.Team().Save(team1)
require.NoError(t, errt)
// Create Channel1
channel1 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel1, err := ss.Channel().Save(channel1, 9999)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Channel1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Channel2
channel2 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel2, err = ss.Channel().Save(channel2, 9999)
require.NoError(t, err)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group3WithMemberCount := *group3
group3WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
TeamId string
Page int
PerPage int
Result map[string][]*model.GroupWithSchemeAdmin
Opts model.GroupSearchOpts
}{
{
Name: "Get the groups for Channel1 and Channel2",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}},
},
{
Name: "Get first Group for Channel1 with page 0 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get second Group for Channel1 with page 1 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group2WSA}},
},
{
Name: "Get empty Groups for a fake id",
TeamId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: map[string][]*model.GroupWithSchemeAdmin{},
},
{
Name: "Get group matching name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low chance of a name collision
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get group matching display name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get group matching multiple display names",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}},
},
{
Name: "Include member counts",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 10,
Result: map[string][]*model.GroupWithSchemeAdmin{
channel1.Id: {
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
channel2.Id: {
{Group: group3WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
},
{
Name: "Include allow reference",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 2,
Result: map[string][]*model.GroupWithSchemeAdmin{
channel1.Id: {
group2WSA,
},
channel2.Id: {
group3WSA,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsAssociatedToChannelsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
assert.Equal(t, tc.Result, groups)
})
}
}
func testGetGroupsByTeam(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, err := ss.Team().Save(team1)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Team1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team1.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Team2
team2 := &model.Team{
DisplayName: "Team2",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Team2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team2.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
TeamId string
Page int
PerPage int
Opts model.GroupSearchOpts
Result []*model.GroupWithSchemeAdmin
TotalCount *int64
}{
{
Name: "Get the two Groups for Team1",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Get first Group for Team1 with page 0 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
{
Name: "Get second Group for Team1 with page 1 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
{
Name: "Get third Group for Team2",
TeamId: team2.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group3WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get empty Groups for a fake id",
TeamId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{},
TotalCount: model.NewInt64(0),
},
{
Name: "Get group matching name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low change of a name collision
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching display name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching multiple display names",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Include member counts",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 2,
Result: []*model.GroupWithSchemeAdmin{
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
{
Name: "Include allow reference",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
require.ElementsMatch(t, tc.Result, groups)
if tc.TotalCount != nil {
var count int64
count, err = ss.Group().CountGroupsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
require.Equal(t, *tc.TotalCount, count)
}
})
}
}
func testGetGroups(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team1, err := ss.Team().Save(team1)
require.NoError(t, err)
startCreateTime := team1.UpdateAt - 1
// Create Channel1
channel1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel1, nErr := ss.Channel().Save(channel1, 9999)
require.NoError(t, nErr)
// Create Groups 1 and 2
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-2"),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-deleted"),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Team1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team1.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Team2
team2 := &model.Team{
DisplayName: "Team2",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
// Create Channel2
channel2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel2, nErr = ss.Channel().Save(channel2, 9999)
require.NoError(t, nErr)
// Create Channel3
channel3 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel3",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel3, nErr = ss.Channel().Save(channel3, 9999)
require.NoError(t, nErr)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-3"),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Team2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team2.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group3.Id,
})
require.NoError(t, err)
// And associate Group1 to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
})
require.NoError(t, err)
// And associate Group2 and Group3 to Channel1
for _, g := range []*model.Group{group2, group3} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id)
require.NoError(t, err)
m1 := model.ChannelMember{
ChannelId: channel1.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
user2.DeleteAt = 1
u2Update, _ := ss.User().Update(user2, true)
group2NameSubstring := "group-2"
endCreateTime := u2Update.New.UpdateAt + 1
// Create Team3
team3 := &model.Team{
DisplayName: "Team3",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team3, err = ss.Team().Save(team3)
require.NoError(t, err)
channel4 := &model.Channel{
TeamId: team3.Id,
DisplayName: "Channel4",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel4, nErr = ss.Channel().Save(channel4, 9999)
require.NoError(t, nErr)
testCases := []struct {
Name string
Page int
PerPage int
Opts model.GroupSearchOpts
Resultf func([]*model.Group) bool
Restrictions *model.ViewUsersRestrictions
}{
{
Name: "Get all the Groups",
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 3,
Resultf: func(groups []*model.Group) bool { return len(groups) == 3 },
Restrictions: nil,
},
{
Name: "Get first Group with page 0 with 1 element",
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Resultf: func(groups []*model.Group) bool { return len(groups) == 1 },
Restrictions: nil,
},
{
Name: "Get single result from page 1",
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Resultf: func(groups []*model.Group) bool { return len(groups) == 1 },
Restrictions: nil,
},
{
Name: "Get multiple results from page 1",
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 2,
Resultf: func(groups []*model.Group) bool { return len(groups) == 2 },
Restrictions: nil,
},
{
Name: "Get group matching name",
Opts: model.GroupSearchOpts{Q: group2NameSubstring},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(*g.Name, group2NameSubstring) && !strings.Contains(g.DisplayName, group2NameSubstring) {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Get group matching display name",
Opts: model.GroupSearchOpts{Q: "rouP-3"},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(strings.ToLower(g.DisplayName), "roup-3") {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Get group matching multiple display names",
Opts: model.GroupSearchOpts{Q: "groUp"},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(strings.ToLower(g.DisplayName), "group") {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include member counts",
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.MemberCount == nil {
return false
}
if (g.Id == group1.Id || g.Id == group2.Id) && *g.MemberCount != 1 {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include member counts with restrictions",
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.MemberCount == nil {
return false
}
if g.Id == group1.Id && *g.MemberCount != 1 {
return false
}
if g.Id == group2.Id && *g.MemberCount != 0 {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}},
},
{
Name: "Not associated to team",
Opts: model.GroupSearchOpts{NotAssociatedToTeam: team2.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.Id == group3.Id {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Not associated to other team",
Opts: model.GroupSearchOpts{NotAssociatedToTeam: team1.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.Id == group1.Id || g.Id == group2.Id {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include allow reference",
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if !g.AllowReference {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Use Since return all",
Opts: model.GroupSearchOpts{FilterAllowReference: true, Since: startCreateTime},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Use Since return none",
Opts: model.GroupSearchOpts{FilterAllowReference: true, Since: endCreateTime},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 0
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained teams",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 2 && groups[0].Id == group1.Id && groups[1].Id == group2.Id
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained page 0",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group1.Id
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained page 1",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 1,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group2.Id
},
Restrictions: nil,
},
{
Name: "Non-group constrained team with no associated groups still returns groups for the child channel",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel4.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) > 0
},
Restrictions: nil,
},
{
Name: "Filter by group member",
Opts: model.GroupSearchOpts{FilterHasMember: user1.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 1 && groups[0].Id == group1.Id
},
Restrictions: nil,
},
{
Name: "Filter by non-existent group member",
Opts: model.GroupSearchOpts{FilterHasMember: model.NewId()},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 0
},
Restrictions: nil,
},
{
Name: "Filter by non-member member",
Opts: model.GroupSearchOpts{FilterHasMember: user2.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 2
},
Restrictions: nil,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
groups, err := ss.Group().GetGroups(tc.Page, tc.PerPage, tc.Opts, tc.Restrictions)
require.NoError(t, err)
require.True(t, tc.Resultf(groups))
})
}
}
func testTeamMembersMinusGroupMembers(t *testing.T, ss store.Store) {
const numberOfGroups = 3
const numberOfUsers = 4
groups := []*model.Group{}
users := []*model.User{}
team := &model.Team{
DisplayName: model.NewId(),
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
for i := 0; i < numberOfUsers; i++ {
user := &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
trueOrFalse := int(math.Mod(float64(i), 2)) == 0
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id, SchemeUser: trueOrFalse, SchemeAdmin: !trueOrFalse}, 999)
require.NoError(t, nErr)
}
// Extra user outside of the group member users.
user := &model.User{
Email: MakeEmail(),
Username: "99_" + model.NewId(),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id, SchemeUser: true, SchemeAdmin: false}, 999)
require.NoError(t, nErr)
for i := 0; i < numberOfGroups; i++ {
group := &model.Group{
Name: model.NewString(fmt.Sprintf("n_%d_%s", i, model.NewId())),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
groups = append(groups, group)
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})
// Add even users to even group, and the inverse
for i := 0; i < numberOfUsers; i++ {
groupIndex := int(math.Mod(float64(i), 2))
_, err := ss.Group().UpsertMember(groups[groupIndex].Id, users[i].Id)
require.NoError(t, err)
// Add everyone to group 2
_, err = ss.Group().UpsertMember(groups[numberOfGroups-1].Id, users[i].Id)
require.NoError(t, err)
}
testCases := map[string]struct {
expectedUserIDs []string
expectedTotalCount int64
groupIDs []string
page int
perPage int
setup func()
teardown func()
}{
"No group IDs, all members": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id, users[3].Id, user.Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 100,
},
"All members, page 1": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 3,
},
"All members, page 2": {
expectedUserIDs: []string{users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 1,
perPage: 3,
},
"Group 1, even users would be removed": {
expectedUserIDs: []string{users[0].Id, users[2].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[1].Id},
page: 0,
perPage: 100,
},
"Group 0, odd users would be removed": {
expectedUserIDs: []string{users[1].Id, users[3].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[0].Id},
page: 0,
perPage: 100,
},
"All groups, no users would be removed": {
expectedUserIDs: []string{users[4].Id},
expectedTotalCount: 1,
groupIDs: []string{groups[0].Id, groups[1].Id},
page: 0,
perPage: 100,
},
}
mapUserIDs := func(users []*model.UserWithGroups) []string {
ids := []string{}
for _, user := range users {
ids = append(ids, user.Id)
}
return ids
}
for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
if tc.setup != nil {
tc.setup()
}
if tc.teardown != nil {
defer tc.teardown()
}
actual, err := ss.Group().TeamMembersMinusGroupMembers(team.Id, tc.groupIDs, tc.page, tc.perPage)
require.NoError(t, err)
require.ElementsMatch(t, tc.expectedUserIDs, mapUserIDs(actual))
actualCount, err := ss.Group().CountTeamMembersMinusGroupMembers(team.Id, tc.groupIDs)
require.NoError(t, err)
require.Equal(t, tc.expectedTotalCount, actualCount)
})
}
}
func testChannelMembersMinusGroupMembers(t *testing.T, ss store.Store) {
const numberOfGroups = 3
const numberOfUsers = 4
groups := []*model.Group{}
users := []*model.User{}
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
channel, err := ss.Channel().Save(channel, 9999)
require.NoError(t, err)
for i := 0; i < numberOfUsers; i++ {
user := &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
trueOrFalse := int(math.Mod(float64(i), 2)) == 0
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeUser: trueOrFalse,
SchemeAdmin: !trueOrFalse,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
// Extra user outside of the group member users.
user, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "99_" + model.NewId(),
})
require.NoError(t, err)
users = append(users, user)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
for i := 0; i < numberOfGroups; i++ {
group := &model.Group{
Name: model.NewString(fmt.Sprintf("n_%d_%s", i, model.NewId())),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
groups = append(groups, group)
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})
// Add even users to even group, and the inverse
for i := 0; i < numberOfUsers; i++ {
groupIndex := int(math.Mod(float64(i), 2))
_, err := ss.Group().UpsertMember(groups[groupIndex].Id, users[i].Id)
require.NoError(t, err)
// Add everyone to group 2
_, err = ss.Group().UpsertMember(groups[numberOfGroups-1].Id, users[i].Id)
require.NoError(t, err)
}
testCases := map[string]struct {
expectedUserIDs []string
expectedTotalCount int64
groupIDs []string
page int
perPage int
setup func()
teardown func()
}{
"No group IDs, all members": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id, users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 100,
},
"All members, page 1": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 3,
},
"All members, page 2": {
expectedUserIDs: []string{users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 1,
perPage: 3,
},
"Group 1, even users would be removed": {
expectedUserIDs: []string{users[0].Id, users[2].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[1].Id},
page: 0,
perPage: 100,
},
"Group 0, odd users would be removed": {
expectedUserIDs: []string{users[1].Id, users[3].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[0].Id},
page: 0,
perPage: 100,
},
"All groups, no users would be removed": {
expectedUserIDs: []string{users[4].Id},
expectedTotalCount: 1,
groupIDs: []string{groups[0].Id, groups[1].Id},
page: 0,
perPage: 100,
},
}
mapUserIDs := func(users []*model.UserWithGroups) []string {
ids := []string{}
for _, user := range users {
ids = append(ids, user.Id)
}
return ids
}
for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
if tc.setup != nil {
tc.setup()
}
if tc.teardown != nil {
defer tc.teardown()
}
actual, err := ss.Group().ChannelMembersMinusGroupMembers(channel.Id, tc.groupIDs, tc.page, tc.perPage)
require.NoError(t, err)
require.ElementsMatch(t, tc.expectedUserIDs, mapUserIDs(actual))
actualCount, err := ss.Group().CountChannelMembersMinusGroupMembers(channel.Id, tc.groupIDs)
require.NoError(t, err)
require.Equal(t, tc.expectedTotalCount, actualCount)
})
}
}
func groupTestGetMemberCount(t *testing.T, ss store.Store) {
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
var user *model.User
var nErr error
for i := 0; i < 2; i++ {
user = &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, nErr = ss.User().Save(user)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
}
count, err := ss.Group().GetMemberCount(group.Id)
require.NoError(t, err)
require.Equal(t, int64(2), count)
user.DeleteAt = 1
_, nErr = ss.User().Update(user, true)
require.NoError(t, nErr)
count, err = ss.Group().GetMemberCount(group.Id)
require.NoError(t, err)
require.Equal(t, int64(1), count)
}
func groupTestAdminRoleGroupsForSyncableMemberChannel(t *testing.T, ss store.Store) {
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user.Id)
require.NoError(t, err)
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group2.Id,
})
require.NoError(t, err)
// User is a member of both groups but only one is SchemeAdmin: true
actualGroupIDs, err := ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id}, actualGroupIDs)
// Update the second group syncable to be SchemeAdmin: true and both groups should be returned
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id, group2.Id}, actualGroupIDs)
// Deleting membership from group should stop the group from being returned
_, err = ss.Group().DeleteMember(group1.Id, user.Id)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group2.Id}, actualGroupIDs)
// Deleting group syncable should stop it being returned
_, err = ss.Group().DeleteGroupSyncable(group2.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{}, actualGroupIDs)
}
func groupTestAdminRoleGroupsForSyncableMemberTeam(t *testing.T, ss store.Store) {
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user.Id)
require.NoError(t, err)
team := &model.Team{
DisplayName: "A Name",
Name: NewTestId(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group2.Id,
})
require.NoError(t, err)
// User is a member of both groups but only one is SchemeAdmin: true
actualGroupIDs, err := ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id}, actualGroupIDs)
// Update the second group syncable to be SchemeAdmin: true and both groups should be returned
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id, group2.Id}, actualGroupIDs)
// Deleting membership from group should stop the group from being returned
_, err = ss.Group().DeleteMember(group1.Id, user.Id)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group2.Id}, actualGroupIDs)
// Deleting group syncable should stop it being returned
_, err = ss.Group().DeleteGroupSyncable(group2.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{}, actualGroupIDs)
}
func groupTestPermittedSyncableAdminsTeam(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
team := &model.Team{
DisplayName: "A Name",
Name: NewTestId(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group2.Id,
SchemeAdmin: false,
})
require.NoError(t, err)
// group 1's users are returned because groupsyncable 2 has SchemeAdmin false.
actualUserIDs, err := ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id}, actualUserIDs)
// update groupsyncable 2 to be SchemeAdmin true
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
// group 2's users are now included in return value
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id, user3.Id}, actualUserIDs)
// deleted group member should not be included
ss.Group().DeleteMember(group1.Id, user2.Id)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user3.Id}, actualUserIDs)
// deleted group syncable no longer includes group members
_, err = ss.Group().DeleteGroupSyncable(group1.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user3.Id}, actualUserIDs)
}
func groupTestPermittedSyncableAdminsChannel(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group2.Id,
SchemeAdmin: false,
})
require.NoError(t, err)
// group 1's users are returned because groupsyncable 2 has SchemeAdmin false.
actualUserIDs, err := ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id}, actualUserIDs)
// update groupsyncable 2 to be SchemeAdmin true
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
// group 2's users are now included in return value
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id, user3.Id}, actualUserIDs)
// deleted group member should not be included
_, err = ss.Group().DeleteMember(group1.Id, user2.Id)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user3.Id}, actualUserIDs)
// deleted group syncable no longer includes group members
_, err = ss.Group().DeleteGroupSyncable(group1.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user3.Id}, actualUserIDs)
}
func groupTestpUpdateMembersRoleTeam(t *testing.T, ss store.Store) {
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
user4 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user4, err = ss.User().Save(user4)
require.NoError(t, err)
for _, user := range []*model.User{user1, user2, user3} {
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id}, 9999)
require.NoError(t, nErr)
}
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user4.Id, SchemeGuest: true}, 9999)
require.NoError(t, nErr)
tests := []struct {
testName string
inUserIDs []string
targetSchemeAdminValue bool
}{
{
"Given users are admins",
[]string{user1.Id, user2.Id},
true,
},
{
"Given users are members",
[]string{user2.Id},
false,
},
{
"Non-given users are admins",
[]string{user2.Id},
false,
},
{
"Non-given users are members",
[]string{user2.Id},
false,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
err = ss.Team().UpdateMembersRole(team.Id, tt.inUserIDs)
require.NoError(t, err)
members, err := ss.Team().GetMembers(team.Id, 0, 100, nil)
require.NoError(t, err)
require.GreaterOrEqual(t, len(members), 4) // sanity check for team membership
for _, member := range members {
if utils.StringInSlice(member.UserId, tt.inUserIDs) {
require.True(t, member.SchemeAdmin)
} else {
require.False(t, member.SchemeAdmin)
}
// Ensure guest account never changes.
if member.UserId == user4.Id {
require.False(t, member.SchemeUser)
require.False(t, member.SchemeAdmin)
require.True(t, member.SchemeGuest)
}
}
})
}
}
func groupTestpUpdateMembersRoleChannel(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, err := ss.Channel().Save(channel, 9999)
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
user4 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user4, err = ss.User().Save(user4)
require.NoError(t, err)
for _, user := range []*model.User{user1, user2, user3} {
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
})
require.NoError(t, err)
tests := []struct {
testName string
inUserIDs []string
targetSchemeAdminValue bool
}{
{
"Given users are admins",
[]string{user1.Id, user2.Id},
true,
},
{
"Given users are members",
[]string{user2.Id},
false,
},
{
"Non-given users are admins",
[]string{user2.Id},
false,
},
{
"Non-given users are members",
[]string{user2.Id},
false,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
err = ss.Channel().UpdateMembersRole(channel.Id, tt.inUserIDs)
require.NoError(t, err)
members, err := ss.Channel().GetMembers(channel.Id, 0, 100)
require.NoError(t, err)
require.GreaterOrEqual(t, len(members), 4) // sanity check for channel membership
for _, member := range members {
if utils.StringInSlice(member.UserId, tt.inUserIDs) {
require.True(t, member.SchemeAdmin)
} else {
require.False(t, member.SchemeAdmin)
}
// Ensure guest account never changes.
if member.UserId == user4.Id {
require.False(t, member.SchemeUser)
require.False(t, member.SchemeAdmin)
require.True(t, member.SchemeGuest)
}
}
})
}
}
func groupTestGroupCount(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
count, err := ss.Group().GroupCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
countAfter, err := ss.Group().GroupCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupTeamCount(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: model.NewId(),
Description: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
})
require.NoError(t, err)
defer ss.Team().PermanentDelete(team.Id)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
groupSyncable1, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group1.Id, team.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable1.GroupId, groupSyncable1.SyncableId, groupSyncable1.Type)
count, err := ss.Group().GroupTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
groupSyncable2, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group2.Id, team.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable2.GroupId, groupSyncable2.SyncableId, groupSyncable2.Type)
countAfter, err := ss.Group().GroupTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupChannelCount(t *testing.T, ss store.Store) {
channel, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, 9999)
require.NoError(t, err)
defer ss.Channel().Delete(channel.Id, 0)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
groupSyncable1, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group1.Id, channel.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable1.GroupId, groupSyncable1.SyncableId, groupSyncable1.Type)
count, err := ss.Group().GroupChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
groupSyncable2, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group2.Id, channel.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable2.GroupId, groupSyncable2.SyncableId, groupSyncable2.Type)
countAfter, err := ss.Group().GroupChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupMemberCount(t *testing.T, ss store.Store) {
user := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
user2 := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group.Id)
member1, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group.Id, member1.UserId)
count, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
member2, err := ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group.Id, member2.UserId)
countAfter, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestDistinctGroupMemberCount(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
user := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
user2 := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
member1, err := ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member1.UserId)
count, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
member2, err := ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member2.UserId)
countAfter1, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter1, count+1)
member3, err := ss.Group().UpsertMember(group1.Id, member1.UserId)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member3.UserId)
countAfter2, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter2, countAfter1)
}
func groupTestGroupCountWithAllowReference(t *testing.T, ss store.Store) {
initialCount, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
count, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
require.Equal(t, count, initialCount)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
AllowReference: true,
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
countAfter, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
require.Greater(t, countAfter, count)
}
func groupTestGetMember(t *testing.T, ss store.Store) {
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
member, err := ss.Group().GetMember(g1.Id, u1.Id)
require.NoError(t, err)
require.NotNil(t, member)
member, err = ss.Group().GetMember(g1.Id, user2.Id)
require.Error(t, err)
require.Nil(t, member)
}
func groupTestGetNonMemberUsersPage(t *testing.T, ss store.Store) {
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, nErr = ss.User().Save(u2)
require.NoError(t, nErr)
users, err := ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil)
require.NoError(t, err)
originalLen := len(users)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
users, err = ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil)
require.NoError(t, err)
require.Len(t, users, originalLen-1)
users, err = ss.Group().GetNonMemberUsersPage(model.NewId(), 0, 1000, nil)
require.Error(t, err)
require.Nil(t, users)
}
func groupTestDistinctGroupMemberCountForSource(t *testing.T, ss store.Store) {
// get the before counts
customGroupCountBefore, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
require.NoError(t, err)
ldapGroupCountBefore, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
require.NoError(t, err)
// create 2 groups, 1 custom and 1 ldap
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
customGroup, err := ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
ldapGroup, err := ss.Group().Create(g2)
require.NoError(t, err)
// create a couple of users
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
// add both new users to both new groups
_, err = ss.Group().UpsertMember(customGroup.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(ldapGroup.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(customGroup.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(ldapGroup.Id, user2.Id)
require.NoError(t, err)
// remove one user from a group to ensure the 'where deleteat = 0' clause is working
_, err = ss.Group().DeleteMember(ldapGroup.Id, user1.Id)
require.NoError(t, err)
defer func() {
ss.Group().DeleteMember(ldapGroup.Id, user2.Id)
ss.Group().DeleteMember(customGroup.Id, user1.Id)
ss.Group().DeleteMember(customGroup.Id, user2.Id)
ss.Group().Delete(customGroup.Id)
ss.Group().Delete(ldapGroup.Id)
ss.User().PermanentDelete(user1.Id)
ss.User().PermanentDelete(user2.Id)
}()
customGroupCount, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
require.NoError(t, err)
require.Equal(t, customGroupCountBefore+2, customGroupCount)
ldapGroupCount, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
require.NoError(t, err)
require.Equal(t, ldapGroupCountBefore+1, ldapGroupCount)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestJobStore(t *testing.T, ss store.Store) {
t.Run("JobSaveGet", func(t *testing.T) { testJobSaveGet(t, ss) })
t.Run("JobGetAllByType", func(t *testing.T) { testJobGetAllByType(t, ss) })
t.Run("JobGetAllByTypeAndStatus", func(t *testing.T) { testJobGetAllByTypeAndStatus(t, ss) })
t.Run("JobGetAllByTypePage", func(t *testing.T) { testJobGetAllByTypePage(t, ss) })
t.Run("JobGetAllByTypesPage", func(t *testing.T) { testJobGetAllByTypesPage(t, ss) })
t.Run("JobGetAllPage", func(t *testing.T) { testJobGetAllPage(t, ss) })
t.Run("JobGetAllByStatus", func(t *testing.T) { testJobGetAllByStatus(t, ss) })
t.Run("GetNewestJobByStatusAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusAndType(t, ss) })
t.Run("GetNewestJobByStatusesAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusesAndType(t, ss) })
t.Run("GetCountByStatusAndType", func(t *testing.T) { testJobStoreGetCountByStatusAndType(t, ss) })
t.Run("JobUpdateOptimistically", func(t *testing.T) { testJobUpdateOptimistically(t, ss) })
t.Run("JobUpdateStatusUpdateStatusOptimistically", func(t *testing.T) { testJobUpdateStatusUpdateStatusOptimistically(t, ss) })
t.Run("JobDelete", func(t *testing.T) { testJobDelete(t, ss) })
t.Run("JobCleanup", func(t *testing.T) { testJobCleanup(t, ss) })
}
func testJobSaveGet(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.NewId(),
Status: model.NewId(),
Data: map[string]string{
"Processed": "0",
"Total": "12345",
"LastProcessed": "abcd",
},
}
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
received, err := ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, job.Id, received.Id, "received incorrect job after save")
require.Equal(t, "12345", received.Data["Total"])
}
func testJobGetAllByType(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
},
{
Id: model.NewId(),
Type: jobType,
},
{
Id: model.NewId(),
Type: model.NewId(),
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByType(jobType)
require.NoError(t, err)
require.Len(t, received, 2)
require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id})
}
func testJobGetAllByTypeAndStatus(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusPending,
},
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusPending,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByTypeAndStatus(jobType, model.JobStatusPending)
require.NoError(t, err)
require.Len(t, received, 2)
require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id})
}
func testJobGetAllByTypePage(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1001,
},
{
Id: model.NewId(),
Type: model.NewId(),
CreateAt: 1002,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByTypePage(jobType, 0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllByTypePage(jobType, 2, 2)
require.NoError(t, err)
require.Len(t, received, 1)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllByTypesPage(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobType2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1001,
},
{
Id: model.NewId(),
Type: model.NewId(),
CreateAt: 1002,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
// test return all
jobTypes := []string{jobType, jobType2}
received, err := ss.Job().GetAllByTypesPage(jobTypes, 0, 4)
require.NoError(t, err)
require.Len(t, received, 3)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
// test paging
jobTypes = []string{jobType, jobType2}
received, err = ss.Job().GetAllByTypesPage(jobTypes, 0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllByTypesPage(jobTypes, 2, 2)
require.NoError(t, err)
require.Len(t, received, 1)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllPage(t *testing.T, ss store.Store) {
jobType := model.NewId()
createAtTime := model.GetMillis()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime + 1,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime + 2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllPage(0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllPage(2, 2)
require.NoError(t, err)
require.NotEmpty(t, received)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllByStatus(t *testing.T, ss store.Store) {
jobType := model.NewId()
status := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
Status: status,
Data: map[string]string{
"test": "data",
},
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
Status: status,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1001,
Status: status,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1002,
Status: model.NewId(),
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByStatus(status)
require.NoError(t, err)
require.Len(t, received, 3)
require.Equal(t, received[0].Id, jobs[1].Id)
require.Equal(t, received[1].Id, jobs[0].Id)
require.Equal(t, received[2].Id, jobs[2].Id)
require.Equal(t, "data", received[1].Data["test"], "should've received job data field back as saved")
}
func testJobStoreGetNewestJobByStatusAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1003,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1004,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetNewestJobByStatusAndType(status1, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, jobs[0].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusAndType(model.NewId(), model.NewId())
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
}
func testJobStoreGetNewestJobByStatusesAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1003,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1004,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetNewestJobByStatusesAndType([]string{status1, status2}, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, jobs[3].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{model.NewId(), model.NewId()}, model.NewId())
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{status2}, jobType2)
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{status1}, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, jobs[2].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{}, jobType1)
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
}
func testJobStoreGetCountByStatusAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 999,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1002,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
count, err := ss.Job().GetCountByStatusAndType(status1, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
count, err = ss.Job().GetCountByStatusAndType(status2, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
count, err = ss.Job().GetCountByStatusAndType(status1, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, 1, count)
count, err = ss.Job().GetCountByStatusAndType(status2, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, 1, count)
}
func testJobUpdateOptimistically(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.JobTypeDataRetention,
CreateAt: model.GetMillis(),
Status: model.JobStatusPending,
}
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
job.LastActivityAt = model.GetMillis()
job.Status = model.JobStatusInProgress
job.Progress = 50
job.Data = map[string]string{
"Foo": "Bar",
}
updated, err := ss.Job().UpdateOptimistically(job, model.JobStatusSuccess)
require.False(t, err != nil && updated)
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateOptimistically(job, model.JobStatusPending)
require.NoError(t, err)
require.True(t, updated)
updatedJob, err := ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, updatedJob.Type, job.Type)
require.Equal(t, updatedJob.CreateAt, job.CreateAt)
require.Equal(t, updatedJob.Status, job.Status)
require.Greater(t, updatedJob.LastActivityAt, job.LastActivityAt)
require.Equal(t, updatedJob.Progress, job.Progress)
require.Equal(t, updatedJob.Data["Foo"], job.Data["Foo"])
}
func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.JobTypeDataRetention,
CreateAt: model.GetMillis(),
Status: model.JobStatusSuccess,
}
var lastUpdateAt int64
received, err := ss.Job().Save(job)
require.NoError(t, err)
lastUpdateAt = received.LastActivityAt
defer ss.Job().Delete(job.Id)
time.Sleep(2 * time.Millisecond)
received, err = ss.Job().UpdateStatus(job.Id, model.JobStatusPending)
require.NoError(t, err)
require.Equal(t, model.JobStatusPending, received.Status)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
lastUpdateAt = received.LastActivityAt
time.Sleep(2 * time.Millisecond)
updated, err := ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusInProgress, model.JobStatusSuccess)
require.NoError(t, err)
require.False(t, updated)
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusPending, received.Status)
require.Equal(t, received.LastActivityAt, lastUpdateAt)
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusPending, model.JobStatusInProgress)
require.NoError(t, err)
require.True(t, updated, "should have succeeded")
var startAtSet int64
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusInProgress, received.Status)
require.NotEqual(t, 0, received.StartAt)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
lastUpdateAt = received.LastActivityAt
startAtSet = received.StartAt
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusInProgress, model.JobStatusSuccess)
require.NoError(t, err)
require.True(t, updated, "should have succeeded")
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusSuccess, received.Status)
require.Equal(t, startAtSet, received.StartAt)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
}
func testJobDelete(t *testing.T, ss store.Store) {
job, err := ss.Job().Save(&model.Job{Id: model.NewId()})
require.NoError(t, err)
_, err = ss.Job().Delete(job.Id)
assert.NoError(t, err)
}
func testJobCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
ids := make([]string, 0, 10)
for i := 0; i < 10; i++ {
job, err := ss.Job().Save(&model.Job{
Id: model.NewId(),
CreateAt: now - int64(i),
Status: model.JobStatusPending,
})
require.NoError(t, err)
ids = append(ids, job.Id)
defer ss.Job().Delete(job.Id)
}
jobs, err := ss.Job().GetAllByStatus(model.JobStatusPending)
require.NoError(t, err)
assert.Len(t, jobs, 10)
err = ss.Job().Cleanup(now+1, 5)
require.NoError(t, err)
// Should not clean up pending jobs
jobs, err = ss.Job().GetAllByStatus(model.JobStatusPending)
require.NoError(t, err)
assert.Len(t, jobs, 10)
for _, id := range ids {
_, err = ss.Job().UpdateStatus(id, model.JobStatusSuccess)
require.NoError(t, err)
}
err = ss.Job().Cleanup(now+1, 5)
require.NoError(t, err)
// Should clean up now
jobs, err = ss.Job().GetAllByStatus(model.JobStatusSuccess)
require.NoError(t, err)
assert.Len(t, jobs, 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestLicenseStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testLicenseStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testLicenseStoreGet(t, ss) })
}
func testLicenseStoreSave(t *testing.T, ss store.Store) {
l1 := model.LicenseRecord{}
l1.Id = model.NewId()
l1.Bytes = "junk"
_, err := ss.License().Save(&l1)
require.NoError(t, err, "couldn't save license record")
_, err = ss.License().Save(&l1)
require.NoError(t, err, "shouldn't fail on trying to save existing license record")
l1.Id = ""
_, err = ss.License().Save(&l1)
require.Error(t, err, "should fail on invalid license")
}
func testLicenseStoreGet(t *testing.T, ss store.Store) {
l1 := model.LicenseRecord{}
l1.Id = model.NewId()
l1.Bytes = "junk"
_, err := ss.License().Save(&l1)
require.NoError(t, err)
record, err := ss.License().Get(l1.Id)
require.NoError(t, err, "couldn't get license")
require.Equal(t, record.Bytes, l1.Bytes, "license bytes didn't match")
_, err = ss.License().Get("missing")
require.Error(t, err, "should fail on get license")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"github.com/dyatlov/go-opengraph/opengraph/types/image"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// These tests are ran on the same store instance, so this provides easier unique, valid timestamps
var linkMetadataTimestamp int64 = 1546300800000
func getNextLinkMetadataTimestamp() int64 {
linkMetadataTimestamp += int64(time.Hour) / (1000 * 1000)
return linkMetadataTimestamp
}
func TestLinkMetadataStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testLinkMetadataStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testLinkMetadataStoreGet(t, ss) })
t.Run("Types", func(t *testing.T) { testLinkMetadataStoreTypes(t, ss) })
}
func testLinkMetadataStoreSave(t *testing.T, ss store.Store) {
t.Run("should save item", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should fail to save invalid item", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "",
Timestamp: 0,
Type: "garbage",
Data: nil,
}
_, err := ss.LinkMetadata().Save(metadata)
assert.Error(t, err)
})
t.Run("should save with duplicate URL and different timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
metadata.Timestamp = getNextLinkMetadataTimestamp()
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should save with duplicate timestamp and different URL", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
metadata.URL = "http://example.com/another/page"
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should save data with duplicate URL and timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, &model.PostImage{}, linkMetadata.Data)
newData := &model.PostImage{Height: 10, Width: 20}
metadata.Data = newData
linkMetadata, err = ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, newData, linkMetadata.Data)
// Should return the original result, not the duplicate one
linkMetadata, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
assert.Equal(t, newData, linkMetadata.Data)
})
}
func testLinkMetadataStoreGet(t *testing.T, ss store.Store) {
t.Run("should get value", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
linkMetadata, err := ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, metadata, linkMetadata)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should return not found with incorrect URL", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
_, err = ss.LinkMetadata().Get("http://example.com/another_page", metadata.Timestamp)
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
})
t.Run("should return not found with incorrect timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
_, err = ss.LinkMetadata().Get(metadata.URL, getNextLinkMetadataTimestamp())
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
})
}
func testLinkMetadataStoreTypes(t *testing.T, ss store.Store) {
t.Run("should save and get image metadata", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{
Width: 123,
Height: 456,
},
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
require.IsType(t, &model.PostImage{}, received.Data)
assert.Equal(t, *(metadata.Data.(*model.PostImage)), *(received.Data.(*model.PostImage)))
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, &model.PostImage{}, received.Data)
assert.Equal(t, *(metadata.Data.(*model.PostImage)), *(received.Data.(*model.PostImage)))
})
t.Run("should save and get opengraph data", func(t *testing.T) {
og := &opengraph.OpenGraph{
URL: "http://example.com",
Images: []*image.Image{
{
URL: "http://example.com/image.png",
},
},
}
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeOpengraph,
Data: og,
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
require.IsType(t, &opengraph.OpenGraph{}, received.Data)
assert.Equal(t, *(metadata.Data.(*opengraph.OpenGraph)), *(received.Data.(*opengraph.OpenGraph)))
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, &opengraph.OpenGraph{}, received.Data)
assert.Equal(t, *(metadata.Data.(*opengraph.OpenGraph)), *(received.Data.(*opengraph.OpenGraph)))
})
t.Run("should save and get nil", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeNone,
Data: nil,
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Nil(t, received.Data)
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.Nil(t, received.Data)
})
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// AuditStore is an autogenerated mock type for the AuditStore type
type AuditStore struct {
mock.Mock
}
// Get provides a mock function with given fields: user_id, offset, limit
func (_m *AuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
ret := _m.Called(user_id, offset, limit)
var r0 model.Audits
if rf, ok := ret.Get(0).(func(string, int, int) model.Audits); ok {
r0 = rf(user_id, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Audits)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(user_id, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *AuditStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: audit
func (_m *AuditStore) Save(audit *model.Audit) error {
ret := _m.Called(audit)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Audit) error); ok {
r0 = rf(audit)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// BotStore is an autogenerated mock type for the BotStore type
type BotStore struct {
mock.Mock
}
// Get provides a mock function with given fields: userID, includeDeleted
func (_m *BotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
ret := _m.Called(userID, includeDeleted)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(string, bool) *model.Bot); ok {
r0 = rf(userID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: options
func (_m *BotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
ret := _m.Called(options)
var r0 []*model.Bot
if rf, ok := ret.Get(0).(func(*model.BotGetOptions) []*model.Bot); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.BotGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: userID
func (_m *BotStore) PermanentDelete(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: bot
func (_m *BotStore) Save(bot *model.Bot) (*model.Bot, error) {
ret := _m.Called(bot)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(*model.Bot) *model.Bot); ok {
r0 = rf(bot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Bot) error); ok {
r1 = rf(bot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: bot
func (_m *BotStore) Update(bot *model.Bot) (*model.Bot, error) {
ret := _m.Called(bot)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(*model.Bot) *model.Bot); ok {
r0 = rf(bot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Bot) error); ok {
r1 = rf(bot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ChannelMemberHistoryStore is an autogenerated mock type for the ChannelMemberHistoryStore type
type ChannelMemberHistoryStore struct {
mock.Mock
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsLeftSince provides a mock function with given fields: userID, since
func (_m *ChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
ret := _m.Called(userID, since)
var r0 []string
if rf, ok := ret.Get(0).(func(string, int64) []string); ok {
r0 = rf(userID, since)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(userID, since)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersInChannelDuring provides a mock function with given fields: startTime, endTime, channelID
func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
ret := _m.Called(startTime, endTime, channelID)
var r0 []*model.ChannelMemberHistoryResult
if rf, ok := ret.Get(0).(func(int64, int64, string) []*model.ChannelMemberHistoryResult); ok {
r0 = rf(startTime, endTime, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberHistoryResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64, string) error); ok {
r1 = rf(startTime, endTime, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// LogJoinEvent provides a mock function with given fields: userID, channelID, joinTime
func (_m *ChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
ret := _m.Called(userID, channelID, joinTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, channelID, joinTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// LogLeaveEvent provides a mock function with given fields: userID, channelID, leaveTime
func (_m *ChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
ret := _m.Called(userID, channelID, leaveTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, channelID, leaveTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *ChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
time "time"
)
// ChannelStore is an autogenerated mock type for the ChannelStore type
type ChannelStore struct {
mock.Mock
}
// AnalyticsDeletedTypeCount provides a mock function with given fields: teamID, channelType
func (_m *ChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
ret := _m.Called(teamID, channelType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.ChannelType) int64); ok {
r0 = rf(teamID, channelType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.ChannelType) error); ok {
r1 = rf(teamID, channelType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsTypeCount provides a mock function with given fields: teamID, channelType
func (_m *ChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
ret := _m.Called(teamID, channelType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.ChannelType) int64); ok {
r0 = rf(teamID, channelType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.ChannelType) error); ok {
r1 = rf(teamID, channelType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Autocomplete provides a mock function with given fields: userID, term, includeDeleted, isGuest
func (_m *ChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
ret := _m.Called(userID, term, includeDeleted, isGuest)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, string, bool, bool) model.ChannelListWithTeamData); ok {
r0 = rf(userID, term, includeDeleted, isGuest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool, bool) error); ok {
r1 = rf(userID, term, includeDeleted, isGuest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteInTeam provides a mock function with given fields: teamID, userID, term, includeDeleted, isGuest
func (_m *ChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, term, includeDeleted, isGuest)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool, bool) model.ChannelList); ok {
r0 = rf(teamID, userID, term, includeDeleted, isGuest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool, bool) error); ok {
r1 = rf(teamID, userID, term, includeDeleted, isGuest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteInTeamForSearch provides a mock function with given fields: teamID, userID, term, includeDeleted
func (_m *ChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool) model.ChannelList); ok {
r0 = rf(teamID, userID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(teamID, userID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *ChannelStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *ChannelStore) ClearCaches() {
_m.Called()
}
// ClearMembersForUserCache provides a mock function with given fields:
func (_m *ChannelStore) ClearMembersForUserCache() {
_m.Called()
}
// ClearSidebarOnTeamLeave provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
ret := _m.Called(userID, teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// CountPostsAfter provides a mock function with given fields: channelID, timestamp, userID
func (_m *ChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
ret := _m.Called(channelID, timestamp, userID)
var r0 int
if rf, ok := ret.Get(0).(func(string, int64, string) int); ok {
r0 = rf(channelID, timestamp, userID)
} else {
r0 = ret.Get(0).(int)
}
var r1 int
if rf, ok := ret.Get(1).(func(string, int64, string) int); ok {
r1 = rf(channelID, timestamp, userID)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, int64, string) error); ok {
r2 = rf(channelID, timestamp, userID)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// CountUrgentPostsAfter provides a mock function with given fields: channelID, timestamp, userID
func (_m *ChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
ret := _m.Called(channelID, timestamp, userID)
var r0 int
if rf, ok := ret.Get(0).(func(string, int64, string) int); ok {
r0 = rf(channelID, timestamp, userID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, string) error); ok {
r1 = rf(channelID, timestamp, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateDirectChannel provides a mock function with given fields: userID, otherUserID, channelOptions
func (_m *ChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
_va := make([]interface{}, len(channelOptions))
for _i := range channelOptions {
_va[_i] = channelOptions[_i]
}
var _ca []interface{}
_ca = append(_ca, userID, otherUserID)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.User, *model.User, ...model.ChannelOption) *model.Channel); ok {
r0 = rf(userID, otherUserID, channelOptions...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User, *model.User, ...model.ChannelOption) error); ok {
r1 = rf(userID, otherUserID, channelOptions...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateInitialSidebarCategories provides a mock function with given fields: userID, opts
func (_m *ChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, opts)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, *store.SidebarCategorySearchOpts) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *store.SidebarCategorySearchOpts) error); ok {
r1 = rf(userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateSidebarCategory provides a mock function with given fields: userID, teamID, newCategory
func (_m *ChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(userID, teamID, newCategory)
var r0 *model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels) *model.SidebarCategoryWithChannels); ok {
r0 = rf(userID, teamID, newCategory)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SidebarCategoryWithChannels)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.SidebarCategoryWithChannels) error); ok {
r1 = rf(userID, teamID, newCategory)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: channelID, timestamp
func (_m *ChannelStore) Delete(channelID string, timestamp int64) error {
ret := _m.Called(channelID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(channelID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteSidebarCategory provides a mock function with given fields: categoryID
func (_m *ChannelStore) DeleteSidebarCategory(categoryID string) error {
ret := _m.Called(categoryID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(categoryID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteSidebarChannelsByPreferences provides a mock function with given fields: preferences
func (_m *ChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id, allowFromCache
func (_m *ChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, bool) *model.Channel); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: teamID
func (_m *ChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
ret := _m.Called(teamID)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(string) []*model.Channel); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersById provides a mock function with given fields: id
func (_m *ChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
ret := _m.Called(id)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersForUser provides a mock function with given fields: userID, allowFromCache, includeDeleted
func (_m *ChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
ret := _m.Called(userID, allowFromCache, includeDeleted)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, bool, bool) map[string]string); ok {
r0 = rf(userID, allowFromCache, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool) error); ok {
r1 = rf(userID, allowFromCache, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersNotifyPropsForChannel provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 map[string]model.StringMap
if rf, ok := ret.Get(0).(func(string, bool) map[string]model.StringMap); ok {
r0 = rf(channelID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannels provides a mock function with given fields: page, perPage, opts
func (_m *ChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
ret := _m.Called(page, perPage, opts)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(int, int, store.ChannelSearchOpts) model.ChannelListWithTeamData); ok {
r0 = rf(page, perPage, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, store.ChannelSearchOpts) error); ok {
r1 = rf(page, perPage, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelsCount provides a mock function with given fields: opts
func (_m *ChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(store.ChannelSearchOpts) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(store.ChannelSearchOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *ChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.ChannelForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.ChannelForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllDirectChannelsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *ChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.DirectChannelForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.DirectChannelForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DirectChannelForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: team_id, name, allowFromCache
func (_m *ChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(team_id, name, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string, bool) *model.Channel); ok {
r0 = rf(team_id, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(team_id, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNameIncludeDeleted provides a mock function with given fields: team_id, name, allowFromCache
func (_m *ChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(team_id, name, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string, bool) *model.Channel); ok {
r0 = rf(team_id, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(team_id, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: team_id, names, allowFromCache
func (_m *ChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
ret := _m.Called(team_id, names, allowFromCache)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(string, []string, bool) []*model.Channel); ok {
r0 = rf(team_id, names, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok {
r1 = rf(team_id, names, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelCounts provides a mock function with given fields: teamID, userID
func (_m *ChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
ret := _m.Called(teamID, userID)
var r0 *model.ChannelCounts
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelCounts); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelMembersForExport provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
ret := _m.Called(userID, teamID)
var r0 []*model.ChannelMemberForExport
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelMemberForExport); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelMembersTimezones provides a mock function with given fields: channelID
func (_m *ChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
ret := _m.Called(channelID)
var r0 []model.StringMap
if rf, ok := ret.Get(0).(func(string) []model.StringMap); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnread provides a mock function with given fields: channelID, userID
func (_m *ChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
ret := _m.Called(channelID, userID)
var r0 *model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelUnread); ok {
r0 = rf(channelID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannels provides a mock function with given fields: teamID, userID, opts
func (_m *ChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, opts)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts) model.ChannelList); ok {
r0 = rf(teamID, userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts) error); ok {
r1 = rf(teamID, userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsBatchForIndexing provides a mock function with given fields: startTime, startChannelID, limit
func (_m *ChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
ret := _m.Called(startTime, startChannelID, limit)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.Channel); ok {
r0 = rf(startTime, startChannelID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startChannelID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByIds provides a mock function with given fields: channelIds, includeDeleted
func (_m *ChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
ret := _m.Called(channelIds, includeDeleted)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func([]string, bool) []*model.Channel); ok {
r0 = rf(channelIds, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(channelIds, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByScheme provides a mock function with given fields: schemeID, offset, limit
func (_m *ChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(schemeID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(schemeID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(schemeID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByUser provides a mock function with given fields: userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID
func (_m *ChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
ret := _m.Called(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, bool, int, int, string) model.ChannelList); ok {
r0 = rf(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int, int, string) error); ok {
r1 = rf(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID
func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
ret := _m.Called(teamId, userId, opts, afterChannelID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts, string) model.ChannelList); ok {
r0 = rf(teamId, userId, opts, afterChannelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts, string) error); ok {
r1 = rf(teamId, userId, opts, afterChannelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsWithTeamDataByIds provides a mock function with given fields: channelIds, includeDeleted
func (_m *ChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
ret := _m.Called(channelIds, includeDeleted)
var r0 []*model.ChannelWithTeamData
if rf, ok := ret.Get(0).(func([]string, bool) []*model.ChannelWithTeamData); ok {
r0 = rf(channelIds, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(channelIds, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDeleted provides a mock function with given fields: team_id, offset, limit, userID
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
ret := _m.Called(team_id, offset, limit, userID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int, string) model.ChannelList); ok {
r0 = rf(team_id, offset, limit, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, string) error); ok {
r1 = rf(team_id, offset, limit, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDeletedByName provides a mock function with given fields: team_id, name
func (_m *ChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
ret := _m.Called(team_id, name)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string) *model.Channel); ok {
r0 = rf(team_id, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(team_id, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFileCount provides a mock function with given fields: channelID
func (_m *ChannelStore) GetFileCount(channelID string) (int64, error) {
ret := _m.Called(channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID
func (_m *ChannelStore) GetForPost(postID string) (*model.Channel, error) {
ret := _m.Called(postID)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string) *model.Channel); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGuestCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids, allowFromCache
func (_m *ChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
ret := _m.Called(ids, allowFromCache)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func([]string, bool) model.ChannelList); ok {
r0 = rf(ids, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(ids, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: ctx, channelID, userID
func (_m *ChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
ret := _m.Called(ctx, channelID, userID)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.ChannelMember); ok {
r0 = rf(ctx, channelID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, channelID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCountFromCache provides a mock function with given fields: channelID
func (_m *ChannelStore) GetMemberCountFromCache(channelID string) int64 {
ret := _m.Called(channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(int64)
}
return r0
}
// GetMemberCountsByGroup provides a mock function with given fields: ctx, channelID, includeTimezones
func (_m *ChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
ret := _m.Called(ctx, channelID, includeTimezones)
var r0 []*model.ChannelMemberCountByGroup
if rf, ok := ret.Get(0).(func(context.Context, string, bool) []*model.ChannelMemberCountByGroup); ok {
r0 = rf(ctx, channelID, includeTimezones)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberCountByGroup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, channelID, includeTimezones)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberForPost provides a mock function with given fields: postID, userID
func (_m *ChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
ret := _m.Called(postID, userID)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelMember); ok {
r0 = rf(postID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(postID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembers provides a mock function with given fields: channelID, offset, limit
func (_m *ChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
ret := _m.Called(channelID, offset, limit)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelMembers); ok {
r0 = rf(channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByChannelIds provides a mock function with given fields: channelIds, userID
func (_m *ChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
ret := _m.Called(channelIds, userID)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func([]string, string) model.ChannelMembers); ok {
r0 = rf(channelIds, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(channelIds, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByIds provides a mock function with given fields: channelID, userIds
func (_m *ChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
ret := _m.Called(channelID, userIds)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, []string) model.ChannelMembers); ok {
r0 = rf(channelID, userIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(channelID, userIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUser provides a mock function with given fields: teamID, userID
func (_m *ChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
ret := _m.Called(teamID, userID)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, string) model.ChannelMembers); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUserWithCursor provides a mock function with given fields: userID, teamID, opts
func (_m *ChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
ret := _m.Called(userID, teamID, opts)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, string, *store.ChannelMemberGraphQLSearchOpts) model.ChannelMembers); ok {
r0 = rf(userID, teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *store.ChannelMemberGraphQLSearchOpts) error); ok {
r1 = rf(userID, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUserWithPagination provides a mock function with given fields: userID, page, perPage
func (_m *ChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
ret := _m.Called(userID, page, perPage)
var r0 model.ChannelMembersWithTeamData
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelMembersWithTeamData); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembersWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersInfoByChannelIds provides a mock function with given fields: channelIDs
func (_m *ChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
ret := _m.Called(channelIDs)
var r0 map[string][]*model.User
if rf, ok := ret.Get(0).(func([]string) map[string][]*model.User); ok {
r0 = rf(channelIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(channelIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMoreChannels provides a mock function with given fields: teamID, userID, offset, limit
func (_m *ChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, int, int) model.ChannelList); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPinnedPostCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPinnedPosts provides a mock function with given fields: channelID
func (_m *ChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
ret := _m.Called(channelID)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string) *model.PostList); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPrivateChannelsForTeam provides a mock function with given fields: teamID, offset, limit
func (_m *ChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicChannelsByIdsForTeam provides a mock function with given fields: teamID, channelIds
func (_m *ChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
ret := _m.Called(teamID, channelIds)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, []string) model.ChannelList); ok {
r0 = rf(teamID, channelIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(teamID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicChannelsForTeam provides a mock function with given fields: teamID, offset, limit
func (_m *ChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategories provides a mock function with given fields: userID, opts
func (_m *ChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, opts)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, *store.SidebarCategorySearchOpts) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *store.SidebarCategorySearchOpts) error); ok {
r1 = rf(userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategoriesForTeamForUser provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, teamID)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, string) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategory provides a mock function with given fields: categoryID
func (_m *ChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(categoryID)
var r0 *model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string) *model.SidebarCategoryWithChannels); ok {
r0 = rf(categoryID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SidebarCategoryWithChannels)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(categoryID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategoryOrder provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
ret := _m.Called(userID, teamID)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string) []string); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamChannels provides a mock function with given fields: teamID
func (_m *ChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
ret := _m.Called(teamID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string) model.ChannelList); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamForChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
ret := _m.Called(channelID)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamMembersForChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
ret := _m.Called(channelID)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopChannelsForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupSyncedChannelCount provides a mock function with given fields:
func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IncrementMentionCount provides a mock function with given fields: channelID, userIDs, isRoot, isUrgent
func (_m *ChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
ret := _m.Called(channelID, userIDs, isRoot, isUrgent)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string, bool, bool) error); ok {
r0 = rf(channelID, userIDs, isRoot, isUrgent)
} else {
r0 = ret.Error(0)
}
return r0
}
// InvalidateAllChannelMembersForUser provides a mock function with given fields: userID
func (_m *ChannelStore) InvalidateAllChannelMembersForUser(userID string) {
_m.Called(userID)
}
// InvalidateCacheForChannelMembersNotifyProps provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
_m.Called(channelID)
}
// InvalidateChannel provides a mock function with given fields: id
func (_m *ChannelStore) InvalidateChannel(id string) {
_m.Called(id)
}
// InvalidateChannelByName provides a mock function with given fields: teamID, name
func (_m *ChannelStore) InvalidateChannelByName(teamID string, name string) {
_m.Called(teamID, name)
}
// InvalidateGuestCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateGuestCount(channelID string) {
_m.Called(channelID)
}
// InvalidateMemberCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateMemberCount(channelID string) {
_m.Called(channelID)
}
// InvalidatePinnedPostCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidatePinnedPostCount(channelID string) {
_m.Called(channelID)
}
// IsUserInChannelUseCache provides a mock function with given fields: userID, channelID
func (_m *ChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
ret := _m.Called(userID, channelID)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MigrateChannelMembers provides a mock function with given fields: fromChannelID, fromUserID
func (_m *ChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
ret := _m.Called(fromChannelID, fromUserID)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, string) map[string]string); ok {
r0 = rf(fromChannelID, fromUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fromChannelID, fromUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: channelID
func (_m *ChannelStore) PermanentDelete(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByTeam provides a mock function with given fields: teamID
func (_m *ChannelStore) PermanentDeleteByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteMembersByChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteMembersByUser provides a mock function with given fields: userID
func (_m *ChannelStore) PermanentDeleteMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PostCountsByDuration provides a mock function with given fields: channelIDs, sinceUnixMillis, userID, duration, groupingLocation
func (_m *ChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
ret := _m.Called(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
var r0 []*model.DurationPostCount
if rf, ok := ret.Get(0).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) []*model.DurationPostCount); ok {
r0 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DurationPostCount)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) error); ok {
r1 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveAllDeactivatedMembers provides a mock function with given fields: channelID
func (_m *ChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMember provides a mock function with given fields: channelID, userID
func (_m *ChannelStore) RemoveMember(channelID string, userID string) error {
ret := _m.Called(channelID, userID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(channelID, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMembers provides a mock function with given fields: channelID, userIds
func (_m *ChannelStore) RemoveMembers(channelID string, userIds []string) error {
ret := _m.Called(channelID, userIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(channelID, userIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAllChannelSchemes provides a mock function with given fields:
func (_m *ChannelStore) ResetAllChannelSchemes() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Restore provides a mock function with given fields: channelID, timestamp
func (_m *ChannelStore) Restore(channelID string, timestamp int64) error {
ret := _m.Called(channelID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(channelID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: channel, maxChannelsPerTeam
func (_m *ChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
ret := _m.Called(channel, maxChannelsPerTeam)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel, int64) *model.Channel); ok {
r0 = rf(channel, maxChannelsPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel, int64) error); ok {
r1 = rf(channel, maxChannelsPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveDirectChannel provides a mock function with given fields: channel, member1, member2
func (_m *ChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
ret := _m.Called(channel, member1, member2)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel, *model.ChannelMember, *model.ChannelMember) *model.Channel); ok {
r0 = rf(channel, member1, member2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel, *model.ChannelMember, *model.ChannelMember) error); ok {
r1 = rf(channel, member1, member2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMember provides a mock function with given fields: member
func (_m *ChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
ret := _m.Called(member)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(*model.ChannelMember) *model.ChannelMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ChannelMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultipleMembers provides a mock function with given fields: members
func (_m *ChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
ret := _m.Called(members)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func([]*model.ChannelMember) []*model.ChannelMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.ChannelMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAllChannels provides a mock function with given fields: term, opts
func (_m *ChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
ret := _m.Called(term, opts)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) model.ChannelListWithTeamData); ok {
r0 = rf(term, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(string, store.ChannelSearchOpts) int64); ok {
r1 = rf(term, opts)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, store.ChannelSearchOpts) error); ok {
r2 = rf(term, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// SearchArchivedInTeam provides a mock function with given fields: teamID, term, userID
func (_m *ChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
ret := _m.Called(teamID, term, userID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string) model.ChannelList); ok {
r0 = rf(teamID, term, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(teamID, term, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchForUserInTeam provides a mock function with given fields: userID, teamID, term, includeDeleted
func (_m *ChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(userID, teamID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool) model.ChannelList); ok {
r0 = rf(userID, teamID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(userID, teamID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchGroupChannels provides a mock function with given fields: userID, term
func (_m *ChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
ret := _m.Called(userID, term)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string) model.ChannelList); ok {
r0 = rf(userID, term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInTeam provides a mock function with given fields: teamID, term, includeDeleted
func (_m *ChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(teamID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, bool) model.ChannelList); ok {
r0 = rf(teamID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(teamID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchMore provides a mock function with given fields: userID, teamID, term
func (_m *ChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
ret := _m.Called(userID, teamID, term)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string) model.ChannelList); ok {
r0 = rf(userID, teamID, term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, teamID, term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetDeleteAt provides a mock function with given fields: channelID, deleteAt, updateAt
func (_m *ChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
ret := _m.Called(channelID, deleteAt, updateAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, int64) error); ok {
r0 = rf(channelID, deleteAt, updateAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetShared provides a mock function with given fields: channelId, shared
func (_m *ChannelStore) SetShared(channelId string, shared bool) error {
ret := _m.Called(channelId, shared)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(channelId, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: channel
func (_m *ChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
ret := _m.Called(channel)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok {
r0 = rf(channel)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel) error); ok {
r1 = rf(channel)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastViewedAt provides a mock function with given fields: channelIds, userID
func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
ret := _m.Called(channelIds, userID)
var r0 map[string]int64
if rf, ok := ret.Get(0).(func([]string, string) map[string]int64); ok {
r0 = rf(channelIds, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]int64)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(channelIds, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot
func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
var r0 *model.ChannelUnreadAt
if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, int, bool) *model.ChannelUnreadAt); ok {
r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnreadAt)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, int, bool) error); ok {
r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMember provides a mock function with given fields: member
func (_m *ChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
ret := _m.Called(member)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(*model.ChannelMember) *model.ChannelMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ChannelMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMemberNotifyProps provides a mock function with given fields: channelID, userID, props
func (_m *ChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
ret := _m.Called(channelID, userID, props)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(string, string, map[string]string) *model.ChannelMember); ok {
r0 = rf(channelID, userID, props)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, map[string]string) error); ok {
r1 = rf(channelID, userID, props)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMembersRole provides a mock function with given fields: channelID, userIDs
func (_m *ChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
ret := _m.Called(channelID, userIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(channelID, userIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMultipleMembers provides a mock function with given fields: members
func (_m *ChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
ret := _m.Called(members)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func([]*model.ChannelMember) []*model.ChannelMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.ChannelMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateSidebarCategories provides a mock function with given fields: userID, teamID, categories
func (_m *ChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(userID, teamID, categories)
var r0 []*model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok {
r0 = rf(userID, teamID, categories)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SidebarCategoryWithChannels)
}
}
var r1 []*model.SidebarCategoryWithChannels
if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok {
r1 = rf(userID, teamID, categories)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]*model.SidebarCategoryWithChannels)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string, []*model.SidebarCategoryWithChannels) error); ok {
r2 = rf(userID, teamID, categories)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// UpdateSidebarCategoryOrder provides a mock function with given fields: userID, teamID, categoryOrder
func (_m *ChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
ret := _m.Called(userID, teamID, categoryOrder)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, []string) error); ok {
r0 = rf(userID, teamID, categoryOrder)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateSidebarChannelCategoryOnMove provides a mock function with given fields: channel, newTeamID
func (_m *ChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
ret := _m.Called(channel, newTeamID)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Channel, string) error); ok {
r0 = rf(channel, newTeamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateSidebarChannelsByPreferences provides a mock function with given fields: preferences
func (_m *ChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// UserBelongsToChannels provides a mock function with given fields: userID, channelIds
func (_m *ChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
ret := _m.Called(userID, channelIds)
var r0 bool
if rf, ok := ret.Get(0).(func(string, []string) bool); ok {
r0 = rf(userID, channelIds)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ClusterDiscoveryStore is an autogenerated mock type for the ClusterDiscoveryStore type
type ClusterDiscoveryStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields:
func (_m *ClusterDiscoveryStore) Cleanup() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
ret := _m.Called(discovery)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) bool); ok {
r0 = rf(discovery)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ClusterDiscovery) error); ok {
r1 = rf(discovery)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Exists provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
ret := _m.Called(discovery)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) bool); ok {
r0 = rf(discovery)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ClusterDiscovery) error); ok {
r1 = rf(discovery)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: discoveryType, clusterName
func (_m *ClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
ret := _m.Called(discoveryType, clusterName)
var r0 []*model.ClusterDiscovery
if rf, ok := ret.Get(0).(func(string, string) []*model.ClusterDiscovery); ok {
r0 = rf(discoveryType, clusterName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ClusterDiscovery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(discoveryType, clusterName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
ret := _m.Called(discovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) error); ok {
r0 = rf(discovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetLastPingAt provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
ret := _m.Called(discovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) error); ok {
r0 = rf(discovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CommandStore is an autogenerated mock type for the CommandStore type
type CommandStore struct {
mock.Mock
}
// AnalyticsCommandCount provides a mock function with given fields: teamID
func (_m *CommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: commandID, timestamp
func (_m *CommandStore) Delete(commandID string, timestamp int64) error {
ret := _m.Called(commandID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(commandID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id
func (_m *CommandStore) Get(id string) (*model.Command, error) {
ret := _m.Called(id)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(string) *model.Command); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByTeam provides a mock function with given fields: teamID
func (_m *CommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
ret := _m.Called(teamID)
var r0 []*model.Command
if rf, ok := ret.Get(0).(func(string) []*model.Command); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByTrigger provides a mock function with given fields: teamID, trigger
func (_m *CommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
ret := _m.Called(teamID, trigger)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(string, string) *model.Command); ok {
r0 = rf(teamID, trigger)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, trigger)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByTeam provides a mock function with given fields: teamID
func (_m *CommandStore) PermanentDeleteByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *CommandStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: webhook
func (_m *CommandStore) Save(webhook *model.Command) (*model.Command, error) {
ret := _m.Called(webhook)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(*model.Command) *model.Command); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Command) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: hook
func (_m *CommandStore) Update(hook *model.Command) (*model.Command, error) {
ret := _m.Called(hook)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(*model.Command) *model.Command); ok {
r0 = rf(hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Command) error); ok {
r1 = rf(hook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CommandWebhookStore is an autogenerated mock type for the CommandWebhookStore type
type CommandWebhookStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields:
func (_m *CommandWebhookStore) Cleanup() {
_m.Called()
}
// Get provides a mock function with given fields: id
func (_m *CommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
ret := _m.Called(id)
var r0 *model.CommandWebhook
if rf, ok := ret.Get(0).(func(string) *model.CommandWebhook); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CommandWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: webhook
func (_m *CommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.CommandWebhook
if rf, ok := ret.Get(0).(func(*model.CommandWebhook) *model.CommandWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CommandWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.CommandWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TryUse provides a mock function with given fields: id, limit
func (_m *CommandWebhookStore) TryUse(id string, limit int) error {
ret := _m.Called(id, limit)
var r0 error
if rf, ok := ret.Get(0).(func(string, int) error); ok {
r0 = rf(id, limit)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ComplianceStore is an autogenerated mock type for the ComplianceStore type
type ComplianceStore struct {
mock.Mock
}
// ComplianceExport provides a mock function with given fields: compliance, cursor, limit
func (_m *ComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
ret := _m.Called(compliance, cursor, limit)
var r0 []*model.CompliancePost
if rf, ok := ret.Get(0).(func(*model.Compliance, model.ComplianceExportCursor, int) []*model.CompliancePost); ok {
r0 = rf(compliance, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.CompliancePost)
}
}
var r1 model.ComplianceExportCursor
if rf, ok := ret.Get(1).(func(*model.Compliance, model.ComplianceExportCursor, int) model.ComplianceExportCursor); ok {
r1 = rf(compliance, cursor, limit)
} else {
r1 = ret.Get(1).(model.ComplianceExportCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(*model.Compliance, model.ComplianceExportCursor, int) error); ok {
r2 = rf(compliance, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Get provides a mock function with given fields: id
func (_m *ComplianceStore) Get(id string) (*model.Compliance, error) {
ret := _m.Called(id)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(string) *model.Compliance); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *ComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
ret := _m.Called(offset, limit)
var r0 model.Compliances
if rf, ok := ret.Get(0).(func(int, int) model.Compliances); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Compliances)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MessageExport provides a mock function with given fields: ctx, cursor, limit
func (_m *ComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
ret := _m.Called(ctx, cursor, limit)
var r0 []*model.MessageExport
if rf, ok := ret.Get(0).(func(context.Context, model.MessageExportCursor, int) []*model.MessageExport); ok {
r0 = rf(ctx, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.MessageExport)
}
}
var r1 model.MessageExportCursor
if rf, ok := ret.Get(1).(func(context.Context, model.MessageExportCursor, int) model.MessageExportCursor); ok {
r1 = rf(ctx, cursor, limit)
} else {
r1 = ret.Get(1).(model.MessageExportCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, model.MessageExportCursor, int) error); ok {
r2 = rf(ctx, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Save provides a mock function with given fields: compliance
func (_m *ComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
ret := _m.Called(compliance)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(*model.Compliance) *model.Compliance); ok {
r0 = rf(compliance)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Compliance) error); ok {
r1 = rf(compliance)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: compliance
func (_m *ComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
ret := _m.Called(compliance)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(*model.Compliance) *model.Compliance); ok {
r0 = rf(compliance)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Compliance) error); ok {
r1 = rf(compliance)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// DraftStore is an autogenerated mock type for the DraftStore type
type DraftStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: userID, channelID, rootID
func (_m *DraftStore) Delete(userID string, channelID string, rootID string) error {
ret := _m.Called(userID, channelID, rootID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, channelID, rootID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: userID, channelID, rootID, includeDeleted
func (_m *DraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
ret := _m.Called(userID, channelID, rootID, includeDeleted)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(string, string, string, bool) *model.Draft); ok {
r0 = rf(userID, channelID, rootID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(userID, channelID, rootID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDraftsForUser provides a mock function with given fields: userID, teamID
func (_m *DraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
ret := _m.Called(userID, teamID)
var r0 []*model.Draft
if rf, ok := ret.Get(0).(func(string, string) []*model.Draft); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: d
func (_m *DraftStore) Save(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: d
func (_m *DraftStore) Update(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// EmojiStore is an autogenerated mock type for the EmojiStore type
type EmojiStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: emoji, timestamp
func (_m *EmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
ret := _m.Called(emoji, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Emoji, int64) error); ok {
r0 = rf(emoji, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id, allowFromCache
func (_m *EmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
ret := _m.Called(ctx, id, allowFromCache)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok {
r0 = rf(ctx, id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, name, allowFromCache
func (_m *EmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
ret := _m.Called(ctx, name, allowFromCache)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok {
r0 = rf(ctx, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetList provides a mock function with given fields: offset, limit, sort
func (_m *EmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
ret := _m.Called(offset, limit, sort)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func(int, int, string) []*model.Emoji); ok {
r0 = rf(offset, limit, sort)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, string) error); ok {
r1 = rf(offset, limit, sort)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMultipleByName provides a mock function with given fields: names
func (_m *EmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
ret := _m.Called(names)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func([]string) []*model.Emoji); ok {
r0 = rf(names)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(names)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: emoji
func (_m *EmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
ret := _m.Called(emoji)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(*model.Emoji) *model.Emoji); ok {
r0 = rf(emoji)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Emoji) error); ok {
r1 = rf(emoji)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: name, prefixOnly, limit
func (_m *EmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
ret := _m.Called(name, prefixOnly, limit)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func(string, bool, int) []*model.Emoji); ok {
r0 = rf(name, prefixOnly, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int) error); ok {
r1 = rf(name, prefixOnly, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// FileInfoStore is an autogenerated mock type for the FileInfoStore type
type FileInfoStore struct {
mock.Mock
}
// AttachToPost provides a mock function with given fields: fileID, postID, channelID, creatorID
func (_m *FileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
ret := _m.Called(fileID, postID, channelID, creatorID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(fileID, postID, channelID, creatorID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *FileInfoStore) ClearCaches() {
_m.Called()
}
// CountAll provides a mock function with given fields:
func (_m *FileInfoStore) CountAll() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteForPost provides a mock function with given fields: postID
func (_m *FileInfoStore) DeleteForPost(postID string) (string, error) {
ret := _m.Called(postID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(postID)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *FileInfoStore) Get(id string) (*model.FileInfo, error) {
ret := _m.Called(id)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIds provides a mock function with given fields: ids
func (_m *FileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
ret := _m.Called(ids)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func([]string) []*model.FileInfo); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByPath provides a mock function with given fields: path
func (_m *FileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
ret := _m.Called(path)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFilesBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
func (_m *FileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
ret := _m.Called(startTime, startFileID, limit)
var r0 []*model.FileForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.FileForIndexing); ok {
r0 = rf(startTime, startFileID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startFileID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID, readFromMaster, includeDeleted, allowFromCache
func (_m *FileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
ret := _m.Called(postID, readFromMaster, includeDeleted, allowFromCache)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(string, bool, bool, bool) []*model.FileInfo); ok {
r0 = rf(postID, readFromMaster, includeDeleted, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool, bool) error); ok {
r1 = rf(postID, readFromMaster, includeDeleted, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForUser provides a mock function with given fields: userID
func (_m *FileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
ret := _m.Called(userID)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(string) []*model.FileInfo); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFromMaster provides a mock function with given fields: id
func (_m *FileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
ret := _m.Called(id)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetStorageUsage provides a mock function with given fields: allowFromCache, includeDeleted
func (_m *FileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
ret := _m.Called(allowFromCache, includeDeleted)
var r0 int64
if rf, ok := ret.Get(0).(func(bool, bool) int64); ok {
r0 = rf(allowFromCache, includeDeleted)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool, bool) error); ok {
r1 = rf(allowFromCache, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUptoNSizeFileTime provides a mock function with given fields: n
func (_m *FileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
ret := _m.Called(n)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetWithOptions provides a mock function with given fields: page, perPage, opt
func (_m *FileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
ret := _m.Called(page, perPage, opt)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(int, int, *model.GetFileInfosOptions) []*model.FileInfo); ok {
r0 = rf(page, perPage, opt)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, *model.GetFileInfosOptions) error); ok {
r1 = rf(page, perPage, opt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateFileInfosForPostCache provides a mock function with given fields: postID, deleted
func (_m *FileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
_m.Called(postID, deleted)
}
// PermanentDelete provides a mock function with given fields: fileID
func (_m *FileInfoStore) PermanentDelete(fileID string) error {
ret := _m.Called(fileID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(fileID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *FileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *FileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: info
func (_m *FileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
ret := _m.Called(info)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(*model.FileInfo) *model.FileInfo); ok {
r0 = rf(info)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.FileInfo) error); ok {
r1 = rf(info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: paramsList, userID, teamID, page, perPage
func (_m *FileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
ret := _m.Called(paramsList, userID, teamID, page, perPage)
var r0 *model.FileInfoList
if rf, ok := ret.Get(0).(func([]*model.SearchParams, string, string, int, int) *model.FileInfoList); ok {
r0 = rf(paramsList, userID, teamID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfoList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.SearchParams, string, string, int, int) error); ok {
r1 = rf(paramsList, userID, teamID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetContent provides a mock function with given fields: fileID, content
func (_m *FileInfoStore) SetContent(fileID string, content string) error {
ret := _m.Called(fileID, content)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(fileID, content)
} else {
r0 = ret.Error(0)
}
return r0
}
// Upsert provides a mock function with given fields: info
func (_m *FileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
ret := _m.Called(info)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(*model.FileInfo) *model.FileInfo); ok {
r0 = rf(info)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.FileInfo) error); ok {
r1 = rf(info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// GroupStore is an autogenerated mock type for the GroupStore type
type GroupStore struct {
mock.Mock
}
// AdminRoleGroupsForSyncableMember provides a mock function with given fields: userID, syncableID, syncableType
func (_m *GroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
ret := _m.Called(userID, syncableID, syncableType)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) []string); ok {
r0 = rf(userID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(userID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs, page, perPage
func (_m *GroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
ret := _m.Called(channelID, groupIDs, page, perPage)
var r0 []*model.UserWithGroups
if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok {
r0 = rf(channelID, groupIDs, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserWithGroups)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, int, int) error); ok {
r1 = rf(channelID, groupIDs, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersToAdd provides a mock function with given fields: since, channelID, includeRemovedMembers
func (_m *GroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
ret := _m.Called(since, channelID, includeRemovedMembers)
var r0 []*model.UserChannelIDPair
if rf, ok := ret.Get(0).(func(int64, *string, bool) []*model.UserChannelIDPair); ok {
r0 = rf(since, channelID, includeRemovedMembers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserChannelIDPair)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, *string, bool) error); ok {
r1 = rf(since, channelID, includeRemovedMembers)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersToRemove provides a mock function with given fields: channelID
func (_m *GroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
ret := _m.Called(channelID)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func(*string) []*model.ChannelMember); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs
func (_m *GroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
ret := _m.Called(channelID, groupIDs)
var r0 int64
if rf, ok := ret.Get(0).(func(string, []string) int64); ok {
r0 = rf(channelID, groupIDs)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(channelID, groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountGroupsByChannel provides a mock function with given fields: channelID, opts
func (_m *GroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
ret := _m.Called(channelID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) int64); ok {
r0 = rf(channelID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(channelID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountGroupsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
ret := _m.Called(teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) int64); ok {
r0 = rf(teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountTeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs
func (_m *GroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
ret := _m.Called(teamID, groupIDs)
var r0 int64
if rf, ok := ret.Get(0).(func(string, []string) int64); ok {
r0 = rf(teamID, groupIDs)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(teamID, groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: group
func (_m *GroupStore) Create(group *model.Group) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Group) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *GroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
ret := _m.Called(groupSyncable)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) error); ok {
r1 = rf(groupSyncable)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateWithUserIds provides a mock function with given fields: group
func (_m *GroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.GroupWithUserIds) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupWithUserIds) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: groupID
func (_m *GroupStore) Delete(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *GroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableID, syncableType)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteMembers provides a mock function with given fields: groupID, userIDs
func (_m *GroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
ret := _m.Called(groupID, userIDs)
var r0 []*model.GroupMember
if rf, ok := ret.Get(0).(func(string, []string) []*model.GroupMember); ok {
r0 = rf(groupID, userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(groupID, userIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DistinctGroupMemberCount provides a mock function with given fields:
func (_m *GroupStore) DistinctGroupMemberCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DistinctGroupMemberCountForSource provides a mock function with given fields: source
func (_m *GroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
ret := _m.Called(source)
var r0 int64
if rf, ok := ret.Get(0).(func(model.GroupSource) int64); ok {
r0 = rf(source)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(source)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: groupID
func (_m *GroupStore) Get(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllBySource provides a mock function with given fields: groupSource
func (_m *GroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
ret := _m.Called(groupSource)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(model.GroupSource) []*model.Group); ok {
r0 = rf(groupSource)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(groupSource)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllGroupSyncablesByGroupId provides a mock function with given fields: groupID, syncableType
func (_m *GroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableType)
var r0 []*model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) []*model.GroupSyncable); ok {
r0 = rf(groupID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIDs provides a mock function with given fields: groupIDs
func (_m *GroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
ret := _m.Called(groupIDs)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func([]string) []*model.Group); ok {
r0 = rf(groupIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name, opts
func (_m *GroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
ret := _m.Called(name, opts)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) *model.Group); ok {
r0 = rf(name, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(name, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByRemoteID provides a mock function with given fields: remoteID, groupSource
func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
ret := _m.Called(remoteID, groupSource)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string, model.GroupSource) *model.Group); ok {
r0 = rf(remoteID, groupSource)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSource) error); ok {
r1 = rf(remoteID, groupSource)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUser provides a mock function with given fields: userID
func (_m *GroupStore) GetByUser(userID string) ([]*model.Group, error) {
ret := _m.Called(userID)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(string) []*model.Group); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *GroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableID, syncableType)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroups provides a mock function with given fields: page, perPage, opts, viewRestrictions
func (_m *GroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
ret := _m.Called(page, perPage, opts, viewRestrictions)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) []*model.Group); ok {
r0 = rf(page, perPage, opts, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) error); ok {
r1 = rf(page, perPage, opts, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsAssociatedToChannelsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(teamID, opts)
var r0 map[string][]*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) map[string][]*model.GroupWithSchemeAdmin); ok {
r0 = rf(teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsByChannel provides a mock function with given fields: channelID, opts
func (_m *GroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(channelID, opts)
var r0 []*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) []*model.GroupWithSchemeAdmin); ok {
r0 = rf(channelID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(channelID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(teamID, opts)
var r0 []*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) []*model.GroupWithSchemeAdmin); ok {
r0 = rf(teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCount provides a mock function with given fields: groupID
func (_m *GroupStore) GetMemberCount(groupID string) (int64, error) {
ret := _m.Called(groupID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(groupID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCountWithRestrictions provides a mock function with given fields: groupID, viewRestrictions
func (_m *GroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(groupID, viewRestrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(groupID, viewRestrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsers provides a mock function with given fields: groupID
func (_m *GroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
ret := _m.Called(groupID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersInTeam provides a mock function with given fields: groupID, teamID
func (_m *GroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
ret := _m.Called(groupID, teamID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string) []*model.User); ok {
r0 = rf(groupID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersNotInChannel provides a mock function with given fields: groupID, channelID
func (_m *GroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
ret := _m.Called(groupID, channelID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string) []*model.User); ok {
r0 = rf(groupID, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions
func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersSortedPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions, teammateNameDisplay
func (_m *GroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions, string) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions, string) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions
func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupChannelCount provides a mock function with given fields:
func (_m *GroupStore) GroupChannelCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCount provides a mock function with given fields:
func (_m *GroupStore) GroupCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCountBySource provides a mock function with given fields: source
func (_m *GroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
ret := _m.Called(source)
var r0 int64
if rf, ok := ret.Get(0).(func(model.GroupSource) int64); ok {
r0 = rf(source)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(source)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCountWithAllowReference provides a mock function with given fields:
func (_m *GroupStore) GroupCountWithAllowReference() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupMemberCount provides a mock function with given fields:
func (_m *GroupStore) GroupMemberCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupTeamCount provides a mock function with given fields:
func (_m *GroupStore) GroupTeamCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteMembersByUser provides a mock function with given fields: userID
func (_m *GroupStore) PermanentDeleteMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermittedSyncableAdmins provides a mock function with given fields: syncableID, syncableType
func (_m *GroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
ret := _m.Called(syncableID, syncableType)
var r0 []string
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) []string); ok {
r0 = rf(syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSyncableType) error); ok {
r1 = rf(syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Restore provides a mock function with given fields: groupID
func (_m *GroupStore) Restore(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs, page, perPage
func (_m *GroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
ret := _m.Called(teamID, groupIDs, page, perPage)
var r0 []*model.UserWithGroups
if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok {
r0 = rf(teamID, groupIDs, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserWithGroups)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, int, int) error); ok {
r1 = rf(teamID, groupIDs, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersToAdd provides a mock function with given fields: since, teamID, includeRemovedMembers
func (_m *GroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
ret := _m.Called(since, teamID, includeRemovedMembers)
var r0 []*model.UserTeamIDPair
if rf, ok := ret.Get(0).(func(int64, *string, bool) []*model.UserTeamIDPair); ok {
r0 = rf(since, teamID, includeRemovedMembers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserTeamIDPair)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, *string, bool) error); ok {
r1 = rf(since, teamID, includeRemovedMembers)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersToRemove provides a mock function with given fields: teamID
func (_m *GroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
ret := _m.Called(teamID)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(*string) []*model.TeamMember); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: group
func (_m *GroupStore) Update(group *model.Group) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Group) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *GroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
ret := _m.Called(groupSyncable)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) error); ok {
r1 = rf(groupSyncable)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpsertMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpsertMembers provides a mock function with given fields: groupID, userIDs
func (_m *GroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
ret := _m.Called(groupID, userIDs)
var r0 []*model.GroupMember
if rf, ok := ret.Get(0).(func(string, []string) []*model.GroupMember); ok {
r0 = rf(groupID, userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(groupID, userIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// JobStore is an autogenerated mock type for the JobStore type
type JobStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields: expiryTime, batchSize
func (_m *JobStore) Cleanup(expiryTime int64, batchSize int) error {
ret := _m.Called(expiryTime, batchSize)
var r0 error
if rf, ok := ret.Get(0).(func(int64, int) error); ok {
r0 = rf(expiryTime, batchSize)
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: id
func (_m *JobStore) Delete(id string) (string, error) {
ret := _m.Called(id)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(id)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *JobStore) Get(id string) (*model.Job, error) {
ret := _m.Called(id)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string) *model.Job); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByStatus provides a mock function with given fields: status
func (_m *JobStore) GetAllByStatus(status string) ([]*model.Job, error) {
ret := _m.Called(status)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string) []*model.Job); ok {
r0 = rf(status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByType provides a mock function with given fields: jobType
func (_m *JobStore) GetAllByType(jobType string) ([]*model.Job, error) {
ret := _m.Called(jobType)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string) []*model.Job); ok {
r0 = rf(jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypeAndStatus provides a mock function with given fields: jobType, status
func (_m *JobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
ret := _m.Called(jobType, status)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string, string) []*model.Job); ok {
r0 = rf(jobType, status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(jobType, status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypePage provides a mock function with given fields: jobType, offset, limit
func (_m *JobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(jobType, offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Job); ok {
r0 = rf(jobType, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(jobType, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypesPage provides a mock function with given fields: jobTypes, offset, limit
func (_m *JobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(jobTypes, offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func([]string, int, int) []*model.Job); ok {
r0 = rf(jobTypes, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, int, int) error); ok {
r1 = rf(jobTypes, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: offset, limit
func (_m *JobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(int, int) []*model.Job); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCountByStatusAndType provides a mock function with given fields: status, jobType
func (_m *JobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
ret := _m.Called(status, jobType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(status, jobType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(status, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewestJobByStatusAndType provides a mock function with given fields: status, jobType
func (_m *JobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
ret := _m.Called(status, jobType)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string, string) *model.Job); ok {
r0 = rf(status, jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(status, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewestJobByStatusesAndType provides a mock function with given fields: statuses, jobType
func (_m *JobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
ret := _m.Called(statuses, jobType)
var r0 *model.Job
if rf, ok := ret.Get(0).(func([]string, string) *model.Job); ok {
r0 = rf(statuses, jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(statuses, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: job
func (_m *JobStore) Save(job *model.Job) (*model.Job, error) {
ret := _m.Called(job)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(*model.Job) *model.Job); ok {
r0 = rf(job)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Job) error); ok {
r1 = rf(job)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateOptimistically provides a mock function with given fields: job, currentStatus
func (_m *JobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
ret := _m.Called(job, currentStatus)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.Job, string) bool); ok {
r0 = rf(job, currentStatus)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Job, string) error); ok {
r1 = rf(job, currentStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateStatus provides a mock function with given fields: id, status
func (_m *JobStore) UpdateStatus(id string, status string) (*model.Job, error) {
ret := _m.Called(id, status)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string, string) *model.Job); ok {
r0 = rf(id, status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(id, status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateStatusOptimistically provides a mock function with given fields: id, currentStatus, newStatus
func (_m *JobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
ret := _m.Called(id, currentStatus, newStatus)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(id, currentStatus, newStatus)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(id, currentStatus, newStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LicenseStore is an autogenerated mock type for the LicenseStore type
type LicenseStore struct {
mock.Mock
}
// Get provides a mock function with given fields: id
func (_m *LicenseStore) Get(id string) (*model.LicenseRecord, error) {
ret := _m.Called(id)
var r0 *model.LicenseRecord
if rf, ok := ret.Get(0).(func(string) *model.LicenseRecord); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *LicenseStore) GetAll() ([]*model.LicenseRecord, error) {
ret := _m.Called()
var r0 []*model.LicenseRecord
if rf, ok := ret.Get(0).(func() []*model.LicenseRecord); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: license
func (_m *LicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
ret := _m.Called(license)
var r0 *model.LicenseRecord
if rf, ok := ret.Get(0).(func(*model.LicenseRecord) *model.LicenseRecord); ok {
r0 = rf(license)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.LicenseRecord) error); ok {
r1 = rf(license)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LinkMetadataStore is an autogenerated mock type for the LinkMetadataStore type
type LinkMetadataStore struct {
mock.Mock
}
// Get provides a mock function with given fields: url, timestamp
func (_m *LinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
ret := _m.Called(url, timestamp)
var r0 *model.LinkMetadata
if rf, ok := ret.Get(0).(func(string, int64) *model.LinkMetadata); ok {
r0 = rf(url, timestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LinkMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(url, timestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: linkMetadata
func (_m *LinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
ret := _m.Called(linkMetadata)
var r0 *model.LinkMetadata
if rf, ok := ret.Get(0).(func(*model.LinkMetadata) *model.LinkMetadata); ok {
r0 = rf(linkMetadata)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LinkMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.LinkMetadata) error); ok {
r1 = rf(linkMetadata)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// NotifyAdminStore is an autogenerated mock type for the NotifyAdminStore type
type NotifyAdminStore struct {
mock.Mock
}
// DeleteBefore provides a mock function with given fields: trial, now
func (_m *NotifyAdminStore) DeleteBefore(trial bool, now int64) error {
ret := _m.Called(trial, now)
var r0 error
if rf, ok := ret.Get(0).(func(bool, int64) error); ok {
r0 = rf(trial, now)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: trial
func (_m *NotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
ret := _m.Called(trial)
var r0 []*model.NotifyAdminData
if rf, ok := ret.Get(0).(func(bool) []*model.NotifyAdminData); ok {
r0 = rf(trial)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(trial)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDataByUserIdAndFeature provides a mock function with given fields: userId, feature
func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
ret := _m.Called(userId, feature)
var r0 []*model.NotifyAdminData
if rf, ok := ret.Get(0).(func(string, model.MattermostFeature) []*model.NotifyAdminData); ok {
r0 = rf(userId, feature)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.MattermostFeature) error); ok {
r1 = rf(userId, feature)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: data
func (_m *NotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
ret := _m.Called(data)
var r0 *model.NotifyAdminData
if rf, ok := ret.Get(0).(func(*model.NotifyAdminData) *model.NotifyAdminData); ok {
r0 = rf(data)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.NotifyAdminData) error); ok {
r1 = rf(data)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: userId, requiredPlan, requiredFeature, now
func (_m *NotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
ret := _m.Called(userId, requiredPlan, requiredFeature, now)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, model.MattermostFeature, int64) error); ok {
r0 = rf(userId, requiredPlan, requiredFeature, now)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// OAuthStore is an autogenerated mock type for the OAuthStore type
type OAuthStore struct {
mock.Mock
}
// DeleteApp provides a mock function with given fields: id
func (_m *OAuthStore) DeleteApp(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetAccessData provides a mock function with given fields: token
func (_m *OAuthStore) GetAccessData(token string) (*model.AccessData, error) {
ret := _m.Called(token)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string) *model.AccessData); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAccessDataByRefreshToken provides a mock function with given fields: token
func (_m *OAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
ret := _m.Called(token)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string) *model.AccessData); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAccessDataByUserForApp provides a mock function with given fields: userID, clientId
func (_m *OAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
ret := _m.Called(userID, clientId)
var r0 []*model.AccessData
if rf, ok := ret.Get(0).(func(string, string) []*model.AccessData); ok {
r0 = rf(userID, clientId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, clientId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetApp provides a mock function with given fields: id
func (_m *OAuthStore) GetApp(id string) (*model.OAuthApp, error) {
ret := _m.Called(id)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(string) *model.OAuthApp); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAppByUser provides a mock function with given fields: userID, offset, limit
func (_m *OAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OAuthApp); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetApps provides a mock function with given fields: offset, limit
func (_m *OAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(int, int) []*model.OAuthApp); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAuthData provides a mock function with given fields: code
func (_m *OAuthStore) GetAuthData(code string) (*model.AuthData, error) {
ret := _m.Called(code)
var r0 *model.AuthData
if rf, ok := ret.Get(0).(func(string) *model.AuthData); ok {
r0 = rf(code)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AuthData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(code)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAuthorizedApps provides a mock function with given fields: userID, offset, limit
func (_m *OAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OAuthApp); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPreviousAccessData provides a mock function with given fields: userID, clientId
func (_m *OAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
ret := _m.Called(userID, clientId)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string, string) *model.AccessData); ok {
r0 = rf(userID, clientId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, clientId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAuthDataByUser provides a mock function with given fields: userID
func (_m *OAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAccessData provides a mock function with given fields: token
func (_m *OAuthStore) RemoveAccessData(token string) error {
ret := _m.Called(token)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(token)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllAccessData provides a mock function with given fields:
func (_m *OAuthStore) RemoveAllAccessData() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAuthData provides a mock function with given fields: code
func (_m *OAuthStore) RemoveAuthData(code string) error {
ret := _m.Called(code)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(code)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAuthDataByClientId provides a mock function with given fields: clientId, userId
func (_m *OAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
ret := _m.Called(clientId, userId)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(clientId, userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveAccessData provides a mock function with given fields: accessData
func (_m *OAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
ret := _m.Called(accessData)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(*model.AccessData) *model.AccessData); ok {
r0 = rf(accessData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AccessData) error); ok {
r1 = rf(accessData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveApp provides a mock function with given fields: app
func (_m *OAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
ret := _m.Called(app)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(*model.OAuthApp) *model.OAuthApp); ok {
r0 = rf(app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OAuthApp) error); ok {
r1 = rf(app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveAuthData provides a mock function with given fields: authData
func (_m *OAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
ret := _m.Called(authData)
var r0 *model.AuthData
if rf, ok := ret.Get(0).(func(*model.AuthData) *model.AuthData); ok {
r0 = rf(authData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AuthData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AuthData) error); ok {
r1 = rf(authData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAccessData provides a mock function with given fields: accessData
func (_m *OAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
ret := _m.Called(accessData)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(*model.AccessData) *model.AccessData); ok {
r0 = rf(accessData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AccessData) error); ok {
r1 = rf(accessData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateApp provides a mock function with given fields: app
func (_m *OAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
ret := _m.Called(app)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(*model.OAuthApp) *model.OAuthApp); ok {
r0 = rf(app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OAuthApp) error); ok {
r1 = rf(app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PluginStore is an autogenerated mock type for the PluginStore type
type PluginStore struct {
mock.Mock
}
// CompareAndDelete provides a mock function with given fields: keyVal, oldValue
func (_m *PluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
ret := _m.Called(keyVal, oldValue)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue, []byte) bool); ok {
r0 = rf(keyVal, oldValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue, []byte) error); ok {
r1 = rf(keyVal, oldValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CompareAndSet provides a mock function with given fields: keyVal, oldValue
func (_m *PluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
ret := _m.Called(keyVal, oldValue)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue, []byte) bool); ok {
r0 = rf(keyVal, oldValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue, []byte) error); ok {
r1 = rf(keyVal, oldValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: pluginID, key
func (_m *PluginStore) Delete(pluginID string, key string) error {
ret := _m.Called(pluginID, key)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(pluginID, key)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllExpired provides a mock function with given fields:
func (_m *PluginStore) DeleteAllExpired() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllForPlugin provides a mock function with given fields: PluginID
func (_m *PluginStore) DeleteAllForPlugin(PluginID string) error {
ret := _m.Called(PluginID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(PluginID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: pluginID, key
func (_m *PluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
ret := _m.Called(pluginID, key)
var r0 *model.PluginKeyValue
if rf, ok := ret.Get(0).(func(string, string) *model.PluginKeyValue); ok {
r0 = rf(pluginID, key)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PluginKeyValue)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(pluginID, key)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: pluginID, page, perPage
func (_m *PluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
ret := _m.Called(pluginID, page, perPage)
var r0 []string
if rf, ok := ret.Get(0).(func(string, int, int) []string); ok {
r0 = rf(pluginID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(pluginID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveOrUpdate provides a mock function with given fields: keyVal
func (_m *PluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
ret := _m.Called(keyVal)
var r0 *model.PluginKeyValue
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue) *model.PluginKeyValue); ok {
r0 = rf(keyVal)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PluginKeyValue)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue) error); ok {
r1 = rf(keyVal)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetWithOptions provides a mock function with given fields: pluginID, key, value, options
func (_m *PluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
ret := _m.Called(pluginID, key, value, options)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, []byte, model.PluginKVSetOptions) bool); ok {
r0 = rf(pluginID, key, value, options)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, []byte, model.PluginKVSetOptions) error); ok {
r1 = rf(pluginID, key, value, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PostAcknowledgementStore is an autogenerated mock type for the PostAcknowledgementStore type
type PostAcknowledgementStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: acknowledgement
func (_m *PostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
ret := _m.Called(acknowledgement)
var r0 error
if rf, ok := ret.Get(0).(func(*model.PostAcknowledgement) error); ok {
r0 = rf(acknowledgement)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: postID, userID
func (_m *PostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(postID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID
func (_m *PostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postID)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string) []*model.PostAcknowledgement); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPosts provides a mock function with given fields: postIds
func (_m *PostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postIds)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func([]string) []*model.PostAcknowledgement); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: postID, userID, acknowledgedAt
func (_m *PostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID, acknowledgedAt)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string, int64) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID, acknowledgedAt)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(postID, userID, acknowledgedAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PostPriorityStore is an autogenerated mock type for the PostPriorityStore type
type PostPriorityStore struct {
mock.Mock
}
// GetForPost provides a mock function with given fields: postId
func (_m *PostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
ret := _m.Called(postId)
var r0 *model.PostPriority
if rf, ok := ret.Get(0).(func(string) *model.PostPriority); ok {
r0 = rf(postId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostPriority)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPosts provides a mock function with given fields: ids
func (_m *PostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
ret := _m.Called(ids)
var r0 []*model.PostPriority
if rf, ok := ret.Get(0).(func([]string) []*model.PostPriority); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostPriority)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// PostStore is an autogenerated mock type for the PostStore type
type PostStore struct {
mock.Mock
}
// AnalyticsPostCount provides a mock function with given fields: options
func (_m *PostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
ret := _m.Called(options)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.PostCountOptions) int64); ok {
r0 = rf(options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PostCountOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsPostCountsByDay provides a mock function with given fields: options
func (_m *PostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
ret := _m.Called(options)
var r0 model.AnalyticsRows
if rf, ok := ret.Get(0).(func(*model.AnalyticsPostCountsOptions) model.AnalyticsRows); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.AnalyticsRows)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AnalyticsPostCountsOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsUserCountsWithPostsByDay provides a mock function with given fields: teamID
func (_m *PostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
ret := _m.Called(teamID)
var r0 model.AnalyticsRows
if rf, ok := ret.Get(0).(func(string) model.AnalyticsRows); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.AnalyticsRows)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearCaches provides a mock function with given fields:
func (_m *PostStore) ClearCaches() {
_m.Called()
}
// Delete provides a mock function with given fields: postID, timestamp, deleteByID
func (_m *PostStore) Delete(postID string, timestamp int64, deleteByID string) error {
ret := _m.Called(postID, timestamp, deleteByID)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, string) error); ok {
r0 = rf(postID, timestamp, deleteByID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PostStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id, opts, userID, sanitizeOptions
func (_m *PostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(ctx, id, opts, userID, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(context.Context, string, model.GetPostsOptions, string, map[string]bool) *model.PostList); ok {
r0 = rf(ctx, id, opts, userID, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, model.GetPostsOptions, string, map[string]bool) error); ok {
r1 = rf(ctx, id, opts, userID, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDirectPostParentsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *PostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.DirectPostForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.DirectPostForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DirectPostForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEditHistoryForPost provides a mock function with given fields: postId
func (_m *PostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
ret := _m.Called(postId)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string) []*model.Post); ok {
r0 = rf(postId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEtag provides a mock function with given fields: channelID, allowFromCache, collapsedThreads
func (_m *PostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
ret := _m.Called(channelID, allowFromCache, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, bool, bool) string); ok {
r0 = rf(channelID, allowFromCache, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetFlaggedPosts provides a mock function with given fields: userID, offset, limit
func (_m *PostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, int, int) *model.PostList); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFlaggedPostsForChannel provides a mock function with given fields: userID, channelID, offset, limit
func (_m *PostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, channelID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, int, int) *model.PostList); ok {
r0 = rf(userID, channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(userID, channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFlaggedPostsForTeam provides a mock function with given fields: userID, teamID, offset, limit
func (_m *PostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, teamID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, int, int) *model.PostList); ok {
r0 = rf(userID, teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(userID, teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMaxPostSize provides a mock function with given fields:
func (_m *PostStore) GetMaxPostSize() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// GetNthRecentPostTime provides a mock function with given fields: n
func (_m *PostStore) GetNthRecentPostTime(n int64) (int64, error) {
ret := _m.Called(n)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOldest provides a mock function with given fields:
func (_m *PostStore) GetOldest() (*model.Post, error) {
ret := _m.Called()
var r0 *model.Post
if rf, ok := ret.Get(0).(func() *model.Post); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOldestEntityCreationTime provides a mock function with given fields:
func (_m *PostStore) GetOldestEntityCreationTime() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetParentsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *PostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.PostForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.PostForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostAfterTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(string, int64, bool) *model.Post); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostIdAfterTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, int64, bool) string); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostIdBeforeTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, int64, bool) string); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostReminderMetadata provides a mock function with given fields: postID
func (_m *PostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
ret := _m.Called(postID)
var r0 *store.PostReminderMetadata
if rf, ok := ret.Get(0).(func(string) *store.PostReminderMetadata); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*store.PostReminderMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostReminders provides a mock function with given fields: now
func (_m *PostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
ret := _m.Called(now)
var r0 []*model.PostReminder
if rf, ok := ret.Get(0).(func(int64) []*model.PostReminder); ok {
r0 = rf(now)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostReminder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(now)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPosts provides a mock function with given fields: options, allowFromCache, sanitizeOptions
func (_m *PostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, allowFromCache, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, bool, map[string]bool) *model.PostList); ok {
r0 = rf(options, allowFromCache, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, bool, map[string]bool) error); ok {
r1 = rf(options, allowFromCache, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsAfter provides a mock function with given fields: options, sanitizeOptions
func (_m *PostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, map[string]bool) *model.PostList); ok {
r0 = rf(options, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, map[string]bool) error); ok {
r1 = rf(options, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsBatchForIndexing provides a mock function with given fields: startTime, startPostID, limit
func (_m *PostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
ret := _m.Called(startTime, startPostID, limit)
var r0 []*model.PostForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.PostForIndexing); ok {
r0 = rf(startTime, startPostID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startPostID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsBefore provides a mock function with given fields: options, sanitizeOptions
func (_m *PostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, map[string]bool) *model.PostList); ok {
r0 = rf(options, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, map[string]bool) error); ok {
r1 = rf(options, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsByIds provides a mock function with given fields: postIds
func (_m *PostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
ret := _m.Called(postIds)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]string) []*model.Post); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsByThread provides a mock function with given fields: threadID, since
func (_m *PostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
ret := _m.Called(threadID, since)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string, int64) []*model.Post); ok {
r0 = rf(threadID, since)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(threadID, since)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsCreatedAt provides a mock function with given fields: channelID, timestamp
func (_m *PostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
ret := _m.Called(channelID, timestamp)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string, int64) []*model.Post); ok {
r0 = rf(channelID, timestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(channelID, timestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsSince provides a mock function with given fields: options, allowFromCache, sanitizeOptions
func (_m *PostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, allowFromCache, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsSinceOptions, bool, map[string]bool) *model.PostList); ok {
r0 = rf(options, allowFromCache, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsSinceOptions, bool, map[string]bool) error); ok {
r1 = rf(options, allowFromCache, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsSinceForSync provides a mock function with given fields: options, cursor, limit
func (_m *PostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
ret := _m.Called(options, cursor, limit)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) []*model.Post); ok {
r0 = rf(options, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 model.GetPostsSinceForSyncCursor
if rf, ok := ret.Get(1).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) model.GetPostsSinceForSyncCursor); ok {
r1 = rf(options, cursor, limit)
} else {
r1 = ret.Get(1).(model.GetPostsSinceForSyncCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) error); ok {
r2 = rf(options, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetRecentSearchesForUser provides a mock function with given fields: userID
func (_m *PostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
ret := _m.Called(userID)
var r0 []*model.SearchParams
if rf, ok := ret.Get(0).(func(string) []*model.SearchParams); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SearchParams)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRepliesForExport provides a mock function with given fields: parentID
func (_m *PostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
ret := _m.Called(parentID)
var r0 []*model.ReplyForExport
if rf, ok := ret.Get(0).(func(string) []*model.ReplyForExport); ok {
r0 = rf(parentID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ReplyForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(parentID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSingle provides a mock function with given fields: id, inclDeleted
func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
ret := _m.Called(id, inclDeleted)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(string, bool) *model.Post); ok {
r0 = rf(id, inclDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, inclDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopDMsForUserSince provides a mock function with given fields: userID, since, offset, limit
func (_m *PostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
ret := _m.Called(userID, since, offset, limit)
var r0 *model.TopDMList
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.TopDMList); ok {
r0 = rf(userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopDMList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, int, int) error); ok {
r1 = rf(userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
ret := _m.Called(options, userId)
var r0 bool
if rf, ok := ret.Get(0).(func(model.GetPostsSinceOptions, string) bool); ok {
r0 = rf(options, userId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsSinceOptions, string) error); ok {
r1 = rf(options, userId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateLastPostTimeCache provides a mock function with given fields: channelID
func (_m *PostStore) InvalidateLastPostTimeCache(channelID string) {
_m.Called(channelID)
}
// LogRecentSearch provides a mock function with given fields: userID, searchQuery, createAt
func (_m *PostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
ret := _m.Called(userID, searchQuery, createAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, []byte, int64) error); ok {
r0 = rf(userID, searchQuery, createAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// Overwrite provides a mock function with given fields: post
func (_m *PostStore) Overwrite(post *model.Post) (*model.Post, error) {
ret := _m.Called(post)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post) *model.Post); ok {
r0 = rf(post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post) error); ok {
r1 = rf(post)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OverwriteMultiple provides a mock function with given fields: posts
func (_m *PostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
ret := _m.Called(posts)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok {
r0 = rf(posts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 int
if rf, ok := ret.Get(1).(func([]*model.Post) int); ok {
r1 = rf(posts)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func([]*model.Post) error); ok {
r2 = rf(posts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *PostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteByChannel provides a mock function with given fields: channelID
func (_m *PostStore) PermanentDeleteByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *PostStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: post
func (_m *PostStore) Save(post *model.Post) (*model.Post, error) {
ret := _m.Called(post)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post) *model.Post); ok {
r0 = rf(post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post) error); ok {
r1 = rf(post)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultiple provides a mock function with given fields: posts
func (_m *PostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
ret := _m.Called(posts)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok {
r0 = rf(posts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 int
if rf, ok := ret.Get(1).(func([]*model.Post) int); ok {
r1 = rf(posts)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func([]*model.Post) error); ok {
r2 = rf(posts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Search provides a mock function with given fields: teamID, userID, params
func (_m *PostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
ret := _m.Called(teamID, userID, params)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, *model.SearchParams) *model.PostList); ok {
r0 = rf(teamID, userID, params)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.SearchParams) error); ok {
r1 = rf(teamID, userID, params)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPostsForUser provides a mock function with given fields: paramsList, userID, teamID, page, perPage
func (_m *PostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
ret := _m.Called(paramsList, userID, teamID, page, perPage)
var r0 *model.PostSearchResults
if rf, ok := ret.Get(0).(func([]*model.SearchParams, string, string, int, int) *model.PostSearchResults); ok {
r0 = rf(paramsList, userID, teamID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostSearchResults)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.SearchParams, string, string, int, int) error); ok {
r1 = rf(paramsList, userID, teamID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetPostReminder provides a mock function with given fields: reminder
func (_m *PostStore) SetPostReminder(reminder *model.PostReminder) error {
ret := _m.Called(reminder)
var r0 error
if rf, ok := ret.Get(0).(func(*model.PostReminder) error); ok {
r0 = rf(reminder)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: newPost, oldPost
func (_m *PostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
ret := _m.Called(newPost, oldPost)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post, *model.Post) *model.Post); ok {
r0 = rf(newPost, oldPost)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, *model.Post) error); ok {
r1 = rf(newPost, oldPost)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PreferenceStore is an autogenerated mock type for the PreferenceStore type
type PreferenceStore struct {
mock.Mock
}
// CleanupFlagsBatch provides a mock function with given fields: limit
func (_m *PreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: userID, category, name
func (_m *PreferenceStore) Delete(userID string, category string, name string) error {
ret := _m.Called(userID, category, name)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, category, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteCategory provides a mock function with given fields: userID, category
func (_m *PreferenceStore) DeleteCategory(userID string, category string) error {
ret := _m.Called(userID, category)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, category)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteCategoryAndName provides a mock function with given fields: category, name
func (_m *PreferenceStore) DeleteCategoryAndName(category string, name string) error {
ret := _m.Called(category, name)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(category, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: userID, category, name
func (_m *PreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
ret := _m.Called(userID, category, name)
var r0 *model.Preference
if rf, ok := ret.Get(0).(func(string, string, string) *model.Preference); ok {
r0 = rf(userID, category, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Preference)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, category, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: userID
func (_m *PreferenceStore) GetAll(userID string) (model.Preferences, error) {
ret := _m.Called(userID)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string) model.Preferences); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCategory provides a mock function with given fields: userID, category
func (_m *PreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
ret := _m.Called(userID, category)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string, string) model.Preferences); ok {
r0 = rf(userID, category)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, category)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCategoryAndName provides a mock function with given fields: category, nane
func (_m *PreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
ret := _m.Called(category, nane)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string, string) model.Preferences); ok {
r0 = rf(category, nane)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(category, nane)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *PreferenceStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: preferences
func (_m *PreferenceStore) Save(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ProductNoticesStore is an autogenerated mock type for the ProductNoticesStore type
type ProductNoticesStore struct {
mock.Mock
}
// Clear provides a mock function with given fields: notices
func (_m *ProductNoticesStore) Clear(notices []string) error {
ret := _m.Called(notices)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(notices)
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearOldNotices provides a mock function with given fields: currentNotices
func (_m *ProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
ret := _m.Called(currentNotices)
var r0 error
if rf, ok := ret.Get(0).(func(model.ProductNotices) error); ok {
r0 = rf(currentNotices)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetViews provides a mock function with given fields: userID
func (_m *ProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
ret := _m.Called(userID)
var r0 []model.ProductNoticeViewState
if rf, ok := ret.Get(0).(func(string) []model.ProductNoticeViewState); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.ProductNoticeViewState)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// View provides a mock function with given fields: userID, notices
func (_m *ProductNoticesStore) View(userID string, notices []string) error {
ret := _m.Called(userID, notices)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, notices)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ReactionStore is an autogenerated mock type for the ReactionStore type
type ReactionStore struct {
mock.Mock
}
// BulkGetForPosts provides a mock function with given fields: postIds
func (_m *ReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
ret := _m.Called(postIds)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func([]string) []*model.Reaction); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: reaction
func (_m *ReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
ret := _m.Called(reaction)
var r0 *model.Reaction
if rf, ok := ret.Get(0).(func(*model.Reaction) *model.Reaction); ok {
r0 = rf(reaction)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Reaction) error); ok {
r1 = rf(reaction)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteAllWithEmojiName provides a mock function with given fields: emojiName
func (_m *ReactionStore) DeleteAllWithEmojiName(emojiName string) error {
ret := _m.Called(emojiName)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(emojiName)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID, allowFromCache
func (_m *ReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
ret := _m.Called(postID, allowFromCache)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func(string, bool) []*model.Reaction); ok {
r0 = rf(postID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(postID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPostSince provides a mock function with given fields: postId, since, excludeRemoteId, inclDeleted
func (_m *ReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
ret := _m.Called(postId, since, excludeRemoteId, inclDeleted)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func(string, int64, string, bool) []*model.Reaction); ok {
r0 = rf(postId, since, excludeRemoteId, inclDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, string, bool) error); ok {
r1 = rf(postId, since, excludeRemoteId, inclDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopReactionList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopReactionList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *ReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: reaction
func (_m *ReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
ret := _m.Called(reaction)
var r0 *model.Reaction
if rf, ok := ret.Get(0).(func(*model.Reaction) *model.Reaction); ok {
r0 = rf(reaction)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Reaction) error); ok {
r1 = rf(reaction)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RemoteClusterStore is an autogenerated mock type for the RemoteClusterStore type
type RemoteClusterStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
ret := _m.Called(remoteClusterId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(remoteClusterId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteClusterId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteClusterId)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string) *model.RemoteCluster); ok {
r0 = rf(remoteClusterId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteClusterId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: filter
func (_m *RemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
ret := _m.Called(filter)
var r0 []*model.RemoteCluster
if rf, ok := ret.Get(0).(func(model.RemoteClusterQueryFilter) []*model.RemoteCluster); ok {
r0 = rf(filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.RemoteClusterQueryFilter) error); ok {
r1 = rf(filter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: rc
func (_m *RemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
ret := _m.Called(rc)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(*model.RemoteCluster) *model.RemoteCluster); ok {
r0 = rf(rc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RemoteCluster) error); ok {
r1 = rf(rc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetLastPingAt provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
ret := _m.Called(remoteClusterId)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(remoteClusterId)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: rc
func (_m *RemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
ret := _m.Called(rc)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(*model.RemoteCluster) *model.RemoteCluster); ok {
r0 = rf(rc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RemoteCluster) error); ok {
r1 = rf(rc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTopics provides a mock function with given fields: remoteClusterId, topics
func (_m *RemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteClusterId, topics)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string, string) *model.RemoteCluster); ok {
r0 = rf(remoteClusterId, topics)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(remoteClusterId, topics)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RetentionPolicyStore is an autogenerated mock type for the RetentionPolicyStore type
type RetentionPolicyStore struct {
mock.Mock
}
// AddChannels provides a mock function with given fields: policyId, channelIds
func (_m *RetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
ret := _m.Called(policyId, channelIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, channelIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// AddTeams provides a mock function with given fields: policyId, teamIds
func (_m *RetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
ret := _m.Called(policyId, teamIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, teamIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: id
func (_m *RetentionPolicyStore) Delete(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *RetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *RetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(id)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(string) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *RetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(offset, limit)
var r0 []*model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(int, int) []*model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelPoliciesCountForUser provides a mock function with given fields: userID
func (_m *RetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *RetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.RetentionPolicyForChannel
if rf, ok := ret.Get(0).(func(string, int, int) []*model.RetentionPolicyForChannel); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyForChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannels provides a mock function with given fields: policyId, offset, limit
func (_m *RetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
ret := _m.Called(policyId, offset, limit)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelListWithTeamData); ok {
r0 = rf(policyId, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(policyId, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsCount provides a mock function with given fields: policyId
func (_m *RetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
ret := _m.Called(policyId)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(policyId)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(policyId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCount provides a mock function with given fields:
func (_m *RetentionPolicyStore) GetCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamPoliciesCountForUser provides a mock function with given fields: userID
func (_m *RetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *RetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.RetentionPolicyForTeam
if rf, ok := ret.Get(0).(func(string, int, int) []*model.RetentionPolicyForTeam); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyForTeam)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeams provides a mock function with given fields: policyId, offset, limit
func (_m *RetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
ret := _m.Called(policyId, offset, limit)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Team); ok {
r0 = rf(policyId, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(policyId, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsCount provides a mock function with given fields: policyId
func (_m *RetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
ret := _m.Called(policyId)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(policyId)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(policyId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Patch provides a mock function with given fields: patch
func (_m *RetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(patch)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(patch)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) error); ok {
r1 = rf(patch)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveChannels provides a mock function with given fields: policyId, channelIds
func (_m *RetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
ret := _m.Called(policyId, channelIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, channelIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveTeams provides a mock function with given fields: policyId, teamIds
func (_m *RetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
ret := _m.Called(policyId, teamIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, teamIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: policy
func (_m *RetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(policy)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) error); ok {
r1 = rf(policy)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RoleStore is an autogenerated mock type for the RoleStore type
type RoleStore struct {
mock.Mock
}
// AllChannelSchemeRoles provides a mock function with given fields:
func (_m *RoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
ret := _m.Called()
var r0 []*model.Role
if rf, ok := ret.Get(0).(func() []*model.Role); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelHigherScopedPermissions provides a mock function with given fields: roleNames
func (_m *RoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
ret := _m.Called(roleNames)
var r0 map[string]*model.RolePermissions
if rf, ok := ret.Get(0).(func([]string) map[string]*model.RolePermissions); ok {
r0 = rf(roleNames)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.RolePermissions)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(roleNames)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelRolesUnderTeamRole provides a mock function with given fields: roleName
func (_m *RoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
ret := _m.Called(roleName)
var r0 []*model.Role
if rf, ok := ret.Get(0).(func(string) []*model.Role); ok {
r0 = rf(roleName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: roleID
func (_m *RoleStore) Delete(roleID string) (*model.Role, error) {
ret := _m.Called(roleID)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(string) *model.Role); ok {
r0 = rf(roleID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: roleID
func (_m *RoleStore) Get(roleID string) (*model.Role, error) {
ret := _m.Called(roleID)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(string) *model.Role); ok {
r0 = rf(roleID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *RoleStore) GetAll() ([]*model.Role, error) {
ret := _m.Called()
var r0 []*model.Role
if rf, ok := ret.Get(0).(func() []*model.Role); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, name
func (_m *RoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
ret := _m.Called(ctx, name)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(context.Context, string) *model.Role); ok {
r0 = rf(ctx, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: names
func (_m *RoleStore) GetByNames(names []string) ([]*model.Role, error) {
ret := _m.Called(names)
var r0 []*model.Role
if rf, ok := ret.Get(0).(func([]string) []*model.Role); ok {
r0 = rf(names)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(names)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAll provides a mock function with given fields:
func (_m *RoleStore) PermanentDeleteAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: role
func (_m *RoleStore) Save(role *model.Role) (*model.Role, error) {
ret := _m.Called(role)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(*model.Role) *model.Role); ok {
r0 = rf(role)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Role) error); ok {
r1 = rf(role)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SchemeStore is an autogenerated mock type for the SchemeStore type
type SchemeStore struct {
mock.Mock
}
// CountByScope provides a mock function with given fields: scope
func (_m *SchemeStore) CountByScope(scope string) (int64, error) {
ret := _m.Called(scope)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(scope)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(scope)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountWithoutPermission provides a mock function with given fields: scope, permissionID, roleScope, roleType
func (_m *SchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
ret := _m.Called(scope, permissionID, roleScope, roleType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.RoleScope, model.RoleType) int64); ok {
r0 = rf(scope, permissionID, roleScope, roleType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.RoleScope, model.RoleType) error); ok {
r1 = rf(scope, permissionID, roleScope, roleType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: schemeID
func (_m *SchemeStore) Delete(schemeID string) (*model.Scheme, error) {
ret := _m.Called(schemeID)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: schemeID
func (_m *SchemeStore) Get(schemeID string) (*model.Scheme, error) {
ret := _m.Called(schemeID)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: scope, offset, limit
func (_m *SchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
ret := _m.Called(scope, offset, limit)
var r0 []*model.Scheme
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Scheme); ok {
r0 = rf(scope, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(scope, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: schemeName
func (_m *SchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
ret := _m.Called(schemeName)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAll provides a mock function with given fields:
func (_m *SchemeStore) PermanentDeleteAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: scheme
func (_m *SchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
ret := _m.Called(scheme)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(*model.Scheme) *model.Scheme); ok {
r0 = rf(scheme)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Scheme) error); ok {
r1 = rf(scheme)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SessionStore is an autogenerated mock type for the SessionStore type
type SessionStore struct {
mock.Mock
}
// AnalyticsSessionCount provides a mock function with given fields:
func (_m *SessionStore) AnalyticsSessionCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Cleanup provides a mock function with given fields: expiryTime, batchSize
func (_m *SessionStore) Cleanup(expiryTime int64, batchSize int64) error {
ret := _m.Called(expiryTime, batchSize)
var r0 error
if rf, ok := ret.Get(0).(func(int64, int64) error); ok {
r0 = rf(expiryTime, batchSize)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, sessionIDOrToken
func (_m *SessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
ret := _m.Called(ctx, sessionIDOrToken)
var r0 *model.Session
if rf, ok := ret.Get(0).(func(context.Context, string) *model.Session); ok {
r0 = rf(ctx, sessionIDOrToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, sessionIDOrToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessions provides a mock function with given fields: userID
func (_m *SessionStore) GetSessions(userID string) ([]*model.Session, error) {
ret := _m.Called(userID)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(string) []*model.Session); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessionsExpired provides a mock function with given fields: thresholdMillis, mobileOnly, unnotifiedOnly
func (_m *SessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
ret := _m.Called(thresholdMillis, mobileOnly, unnotifiedOnly)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(int64, bool, bool) []*model.Session); ok {
r0 = rf(thresholdMillis, mobileOnly, unnotifiedOnly)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, bool, bool) error); ok {
r1 = rf(thresholdMillis, mobileOnly, unnotifiedOnly)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessionsWithActiveDeviceIds provides a mock function with given fields: userID
func (_m *SessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
ret := _m.Called(userID)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(string) []*model.Session); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteSessionsByUser provides a mock function with given fields: teamID
func (_m *SessionStore) PermanentDeleteSessionsByUser(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Remove provides a mock function with given fields: sessionIDOrToken
func (_m *SessionStore) Remove(sessionIDOrToken string) error {
ret := _m.Called(sessionIDOrToken)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(sessionIDOrToken)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllSessions provides a mock function with given fields:
func (_m *SessionStore) RemoveAllSessions() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: session
func (_m *SessionStore) Save(session *model.Session) (*model.Session, error) {
ret := _m.Called(session)
var r0 *model.Session
if rf, ok := ret.Get(0).(func(*model.Session) *model.Session); ok {
r0 = rf(session)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Session) error); ok {
r1 = rf(session)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateDeviceId provides a mock function with given fields: id, deviceID, expiresAt
func (_m *SessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
ret := _m.Called(id, deviceID, expiresAt)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, int64) string); ok {
r0 = rf(id, deviceID, expiresAt)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(id, deviceID, expiresAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateExpiredNotify provides a mock function with given fields: sessionid, notified
func (_m *SessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
ret := _m.Called(sessionid, notified)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(sessionid, notified)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateExpiresAt provides a mock function with given fields: sessionID, timestamp
func (_m *SessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
ret := _m.Called(sessionID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(sessionID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateLastActivityAt provides a mock function with given fields: sessionID, timestamp
func (_m *SessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
ret := _m.Called(sessionID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(sessionID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateProps provides a mock function with given fields: session
func (_m *SessionStore) UpdateProps(session *model.Session) error {
ret := _m.Called(session)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Session) error); ok {
r0 = rf(session)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateRoles provides a mock function with given fields: userID, roles
func (_m *SessionStore) UpdateRoles(userID string, roles string) (string, error) {
ret := _m.Called(userID, roles)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(userID, roles)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, roles)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SharedChannelStore is an autogenerated mock type for the SharedChannelStore type
type SharedChannelStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: channelId
func (_m *SharedChannelStore) Delete(channelId string) (bool, error) {
ret := _m.Called(channelId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channelId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteRemote provides a mock function with given fields: remoteId
func (_m *SharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
ret := _m.Called(remoteId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(remoteId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: channelId
func (_m *SharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
ret := _m.Called(channelId)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(string) *model.SharedChannel); ok {
r0 = rf(channelId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit, opts
func (_m *SharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
ret := _m.Called(offset, limit, opts)
var r0 []*model.SharedChannel
if rf, ok := ret.Get(0).(func(int, int, model.SharedChannelFilterOpts) []*model.SharedChannel); ok {
r0 = rf(offset, limit, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, model.SharedChannelFilterOpts) error); ok {
r1 = rf(offset, limit, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllCount provides a mock function with given fields: opts
func (_m *SharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(model.SharedChannelFilterOpts) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SharedChannelFilterOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAttachment provides a mock function with given fields: fileId, remoteId
func (_m *SharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
ret := _m.Called(fileId, remoteId)
var r0 *model.SharedChannelAttachment
if rf, ok := ret.Get(0).(func(string, string) *model.SharedChannelAttachment); ok {
r0 = rf(fileId, remoteId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelAttachment)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fileId, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemote provides a mock function with given fields: id
func (_m *SharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
ret := _m.Called(id)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(string) *model.SharedChannelRemote); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemoteByIds provides a mock function with given fields: channelId, remoteId
func (_m *SharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
ret := _m.Called(channelId, remoteId)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(string, string) *model.SharedChannelRemote); ok {
r0 = rf(channelId, remoteId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelId, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemoteForUser provides a mock function with given fields: remoteId, userId
func (_m *SharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteId, userId)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string, string) *model.RemoteCluster); ok {
r0 = rf(remoteId, userId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(remoteId, userId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemotes provides a mock function with given fields: opts
func (_m *SharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
ret := _m.Called(opts)
var r0 []*model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(model.SharedChannelRemoteFilterOpts) []*model.SharedChannelRemote); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SharedChannelRemoteFilterOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemotesStatus provides a mock function with given fields: channelId
func (_m *SharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
ret := _m.Called(channelId)
var r0 []*model.SharedChannelRemoteStatus
if rf, ok := ret.Get(0).(func(string) []*model.SharedChannelRemoteStatus); ok {
r0 = rf(channelId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelRemoteStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSingleUser provides a mock function with given fields: userID, channelID, remoteID
func (_m *SharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
ret := _m.Called(userID, channelID, remoteID)
var r0 *model.SharedChannelUser
if rf, ok := ret.Get(0).(func(string, string, string) *model.SharedChannelUser); ok {
r0 = rf(userID, channelID, remoteID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, channelID, remoteID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersForSync provides a mock function with given fields: filter
func (_m *SharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
ret := _m.Called(filter)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(model.GetUsersForSyncFilter) []*model.User); ok {
r0 = rf(filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetUsersForSyncFilter) error); ok {
r1 = rf(filter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersForUser provides a mock function with given fields: userID
func (_m *SharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
ret := _m.Called(userID)
var r0 []*model.SharedChannelUser
if rf, ok := ret.Get(0).(func(string) []*model.SharedChannelUser); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasChannel provides a mock function with given fields: channelID
func (_m *SharedChannelStore) HasChannel(channelID string) (bool, error) {
ret := _m.Called(channelID)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasRemote provides a mock function with given fields: channelID, remoteId
func (_m *SharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
ret := _m.Called(channelID, remoteId)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(channelID, remoteId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelID, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: sc
func (_m *SharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
ret := _m.Called(sc)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(*model.SharedChannel) *model.SharedChannel); ok {
r0 = rf(sc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannel) error); ok {
r1 = rf(sc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveAttachment provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelAttachment
if rf, ok := ret.Get(0).(func(*model.SharedChannelAttachment) *model.SharedChannelAttachment); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelAttachment)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelAttachment) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveRemote provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(*model.SharedChannelRemote) *model.SharedChannelRemote); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelRemote) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveUser provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelUser
if rf, ok := ret.Get(0).(func(*model.SharedChannelUser) *model.SharedChannelUser); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelUser) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: sc
func (_m *SharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
ret := _m.Called(sc)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(*model.SharedChannel) *model.SharedChannel); ok {
r0 = rf(sc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannel) error); ok {
r1 = rf(sc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAttachmentLastSyncAt provides a mock function with given fields: id, syncTime
func (_m *SharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
ret := _m.Called(id, syncTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(id, syncTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateRemote provides a mock function with given fields: remote
func (_m *SharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(*model.SharedChannelRemote) *model.SharedChannelRemote); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelRemote) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateRemoteCursor provides a mock function with given fields: id, cursor
func (_m *SharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
ret := _m.Called(id, cursor)
var r0 error
if rf, ok := ret.Get(0).(func(string, model.GetPostsSinceForSyncCursor) error); ok {
r0 = rf(id, cursor)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateUserLastSyncAt provides a mock function with given fields: userID, channelID, remoteID
func (_m *SharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
ret := _m.Called(userID, channelID, remoteID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, channelID, remoteID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpsertAttachment provides a mock function with given fields: remote
func (_m *SharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
ret := _m.Called(remote)
var r0 string
if rf, ok := ret.Get(0).(func(*model.SharedChannelAttachment) string); ok {
r0 = rf(remote)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelAttachment) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// StatusStore is an autogenerated mock type for the StatusStore type
type StatusStore struct {
mock.Mock
}
// Get provides a mock function with given fields: userID
func (_m *StatusStore) Get(userID string) (*model.Status, error) {
ret := _m.Called(userID)
var r0 *model.Status
if rf, ok := ret.Get(0).(func(string) *model.Status); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIds provides a mock function with given fields: userIds
func (_m *StatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
ret := _m.Called(userIds)
var r0 []*model.Status
if rf, ok := ret.Get(0).(func([]string) []*model.Status); ok {
r0 = rf(userIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(userIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalActiveUsersCount provides a mock function with given fields:
func (_m *StatusStore) GetTotalActiveUsersCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ResetAll provides a mock function with given fields:
func (_m *StatusStore) ResetAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdate provides a mock function with given fields: status
func (_m *StatusStore) SaveOrUpdate(status *model.Status) error {
ret := _m.Called(status)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Status) error); ok {
r0 = rf(status)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateExpiredDNDStatuses provides a mock function with given fields:
func (_m *StatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
ret := _m.Called()
var r0 []*model.Status
if rf, ok := ret.Get(0).(func() []*model.Status); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastActivityAt provides a mock function with given fields: userID, lastActivityAt
func (_m *StatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
ret := _m.Called(userID, lastActivityAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(userID, lastActivityAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
sql "database/sql"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
time "time"
)
// Store is an autogenerated mock type for the Store type
type Store struct {
mock.Mock
}
// Audit provides a mock function with given fields:
func (_m *Store) Audit() store.AuditStore {
ret := _m.Called()
var r0 store.AuditStore
if rf, ok := ret.Get(0).(func() store.AuditStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.AuditStore)
}
}
return r0
}
// Bot provides a mock function with given fields:
func (_m *Store) Bot() store.BotStore {
ret := _m.Called()
var r0 store.BotStore
if rf, ok := ret.Get(0).(func() store.BotStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.BotStore)
}
}
return r0
}
// Channel provides a mock function with given fields:
func (_m *Store) Channel() store.ChannelStore {
ret := _m.Called()
var r0 store.ChannelStore
if rf, ok := ret.Get(0).(func() store.ChannelStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ChannelStore)
}
}
return r0
}
// ChannelMemberHistory provides a mock function with given fields:
func (_m *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
ret := _m.Called()
var r0 store.ChannelMemberHistoryStore
if rf, ok := ret.Get(0).(func() store.ChannelMemberHistoryStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ChannelMemberHistoryStore)
}
}
return r0
}
// CheckIntegrity provides a mock function with given fields:
func (_m *Store) CheckIntegrity() <-chan model.IntegrityCheckResult {
ret := _m.Called()
var r0 <-chan model.IntegrityCheckResult
if rf, ok := ret.Get(0).(func() <-chan model.IntegrityCheckResult); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(<-chan model.IntegrityCheckResult)
}
}
return r0
}
// Close provides a mock function with given fields:
func (_m *Store) Close() {
_m.Called()
}
// ClusterDiscovery provides a mock function with given fields:
func (_m *Store) ClusterDiscovery() store.ClusterDiscoveryStore {
ret := _m.Called()
var r0 store.ClusterDiscoveryStore
if rf, ok := ret.Get(0).(func() store.ClusterDiscoveryStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ClusterDiscoveryStore)
}
}
return r0
}
// Command provides a mock function with given fields:
func (_m *Store) Command() store.CommandStore {
ret := _m.Called()
var r0 store.CommandStore
if rf, ok := ret.Get(0).(func() store.CommandStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.CommandStore)
}
}
return r0
}
// CommandWebhook provides a mock function with given fields:
func (_m *Store) CommandWebhook() store.CommandWebhookStore {
ret := _m.Called()
var r0 store.CommandWebhookStore
if rf, ok := ret.Get(0).(func() store.CommandWebhookStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.CommandWebhookStore)
}
}
return r0
}
// Compliance provides a mock function with given fields:
func (_m *Store) Compliance() store.ComplianceStore {
ret := _m.Called()
var r0 store.ComplianceStore
if rf, ok := ret.Get(0).(func() store.ComplianceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ComplianceStore)
}
}
return r0
}
// Context provides a mock function with given fields:
func (_m *Store) Context() context.Context {
ret := _m.Called()
var r0 context.Context
if rf, ok := ret.Get(0).(func() context.Context); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(context.Context)
}
}
return r0
}
// Draft provides a mock function with given fields:
func (_m *Store) Draft() store.DraftStore {
ret := _m.Called()
var r0 store.DraftStore
if rf, ok := ret.Get(0).(func() store.DraftStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.DraftStore)
}
}
return r0
}
// DropAllTables provides a mock function with given fields:
func (_m *Store) DropAllTables() {
_m.Called()
}
// Emoji provides a mock function with given fields:
func (_m *Store) Emoji() store.EmojiStore {
ret := _m.Called()
var r0 store.EmojiStore
if rf, ok := ret.Get(0).(func() store.EmojiStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.EmojiStore)
}
}
return r0
}
// FileInfo provides a mock function with given fields:
func (_m *Store) FileInfo() store.FileInfoStore {
ret := _m.Called()
var r0 store.FileInfoStore
if rf, ok := ret.Get(0).(func() store.FileInfoStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.FileInfoStore)
}
}
return r0
}
// GetAppliedMigrations provides a mock function with given fields:
func (_m *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) {
ret := _m.Called()
var r0 []model.AppliedMigration
if rf, ok := ret.Get(0).(func() []model.AppliedMigration); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.AppliedMigration)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDBSchemaVersion provides a mock function with given fields:
func (_m *Store) GetDBSchemaVersion() (int, error) {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDbVersion provides a mock function with given fields: numerical
func (_m *Store) GetDbVersion(numerical bool) (string, error) {
ret := _m.Called(numerical)
var r0 string
if rf, ok := ret.Get(0).(func(bool) string); ok {
r0 = rf(numerical)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(numerical)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetInternalMasterDB provides a mock function with given fields:
func (_m *Store) GetInternalMasterDB() *sql.DB {
ret := _m.Called()
var r0 *sql.DB
if rf, ok := ret.Get(0).(func() *sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*sql.DB)
}
}
return r0
}
// GetInternalReplicaDB provides a mock function with given fields:
func (_m *Store) GetInternalReplicaDB() *sql.DB {
ret := _m.Called()
var r0 *sql.DB
if rf, ok := ret.Get(0).(func() *sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*sql.DB)
}
}
return r0
}
// GetInternalReplicaDBs provides a mock function with given fields:
func (_m *Store) GetInternalReplicaDBs() []*sql.DB {
ret := _m.Called()
var r0 []*sql.DB
if rf, ok := ret.Get(0).(func() []*sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*sql.DB)
}
}
return r0
}
// Group provides a mock function with given fields:
func (_m *Store) Group() store.GroupStore {
ret := _m.Called()
var r0 store.GroupStore
if rf, ok := ret.Get(0).(func() store.GroupStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.GroupStore)
}
}
return r0
}
// Job provides a mock function with given fields:
func (_m *Store) Job() store.JobStore {
ret := _m.Called()
var r0 store.JobStore
if rf, ok := ret.Get(0).(func() store.JobStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.JobStore)
}
}
return r0
}
// License provides a mock function with given fields:
func (_m *Store) License() store.LicenseStore {
ret := _m.Called()
var r0 store.LicenseStore
if rf, ok := ret.Get(0).(func() store.LicenseStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.LicenseStore)
}
}
return r0
}
// LinkMetadata provides a mock function with given fields:
func (_m *Store) LinkMetadata() store.LinkMetadataStore {
ret := _m.Called()
var r0 store.LinkMetadataStore
if rf, ok := ret.Get(0).(func() store.LinkMetadataStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.LinkMetadataStore)
}
}
return r0
}
// LockToMaster provides a mock function with given fields:
func (_m *Store) LockToMaster() {
_m.Called()
}
// MarkSystemRanUnitTests provides a mock function with given fields:
func (_m *Store) MarkSystemRanUnitTests() {
_m.Called()
}
// NotifyAdmin provides a mock function with given fields:
func (_m *Store) NotifyAdmin() store.NotifyAdminStore {
ret := _m.Called()
var r0 store.NotifyAdminStore
if rf, ok := ret.Get(0).(func() store.NotifyAdminStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.NotifyAdminStore)
}
}
return r0
}
// OAuth provides a mock function with given fields:
func (_m *Store) OAuth() store.OAuthStore {
ret := _m.Called()
var r0 store.OAuthStore
if rf, ok := ret.Get(0).(func() store.OAuthStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.OAuthStore)
}
}
return r0
}
// Plugin provides a mock function with given fields:
func (_m *Store) Plugin() store.PluginStore {
ret := _m.Called()
var r0 store.PluginStore
if rf, ok := ret.Get(0).(func() store.PluginStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PluginStore)
}
}
return r0
}
// Post provides a mock function with given fields:
func (_m *Store) Post() store.PostStore {
ret := _m.Called()
var r0 store.PostStore
if rf, ok := ret.Get(0).(func() store.PostStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostStore)
}
}
return r0
}
// PostAcknowledgement provides a mock function with given fields:
func (_m *Store) PostAcknowledgement() store.PostAcknowledgementStore {
ret := _m.Called()
var r0 store.PostAcknowledgementStore
if rf, ok := ret.Get(0).(func() store.PostAcknowledgementStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostAcknowledgementStore)
}
}
return r0
}
// PostPriority provides a mock function with given fields:
func (_m *Store) PostPriority() store.PostPriorityStore {
ret := _m.Called()
var r0 store.PostPriorityStore
if rf, ok := ret.Get(0).(func() store.PostPriorityStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostPriorityStore)
}
}
return r0
}
// Preference provides a mock function with given fields:
func (_m *Store) Preference() store.PreferenceStore {
ret := _m.Called()
var r0 store.PreferenceStore
if rf, ok := ret.Get(0).(func() store.PreferenceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PreferenceStore)
}
}
return r0
}
// ProductNotices provides a mock function with given fields:
func (_m *Store) ProductNotices() store.ProductNoticesStore {
ret := _m.Called()
var r0 store.ProductNoticesStore
if rf, ok := ret.Get(0).(func() store.ProductNoticesStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ProductNoticesStore)
}
}
return r0
}
// Reaction provides a mock function with given fields:
func (_m *Store) Reaction() store.ReactionStore {
ret := _m.Called()
var r0 store.ReactionStore
if rf, ok := ret.Get(0).(func() store.ReactionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ReactionStore)
}
}
return r0
}
// RecycleDBConnections provides a mock function with given fields: d
func (_m *Store) RecycleDBConnections(d time.Duration) {
_m.Called(d)
}
// RemoteCluster provides a mock function with given fields:
func (_m *Store) RemoteCluster() store.RemoteClusterStore {
ret := _m.Called()
var r0 store.RemoteClusterStore
if rf, ok := ret.Get(0).(func() store.RemoteClusterStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RemoteClusterStore)
}
}
return r0
}
// ReplicaLagAbs provides a mock function with given fields:
func (_m *Store) ReplicaLagAbs() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ReplicaLagTime provides a mock function with given fields:
func (_m *Store) ReplicaLagTime() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// RetentionPolicy provides a mock function with given fields:
func (_m *Store) RetentionPolicy() store.RetentionPolicyStore {
ret := _m.Called()
var r0 store.RetentionPolicyStore
if rf, ok := ret.Get(0).(func() store.RetentionPolicyStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RetentionPolicyStore)
}
}
return r0
}
// Role provides a mock function with given fields:
func (_m *Store) Role() store.RoleStore {
ret := _m.Called()
var r0 store.RoleStore
if rf, ok := ret.Get(0).(func() store.RoleStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RoleStore)
}
}
return r0
}
// Scheme provides a mock function with given fields:
func (_m *Store) Scheme() store.SchemeStore {
ret := _m.Called()
var r0 store.SchemeStore
if rf, ok := ret.Get(0).(func() store.SchemeStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SchemeStore)
}
}
return r0
}
// Session provides a mock function with given fields:
func (_m *Store) Session() store.SessionStore {
ret := _m.Called()
var r0 store.SessionStore
if rf, ok := ret.Get(0).(func() store.SessionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SessionStore)
}
}
return r0
}
// SetContext provides a mock function with given fields: _a0
func (_m *Store) SetContext(_a0 context.Context) {
_m.Called(_a0)
}
// SharedChannel provides a mock function with given fields:
func (_m *Store) SharedChannel() store.SharedChannelStore {
ret := _m.Called()
var r0 store.SharedChannelStore
if rf, ok := ret.Get(0).(func() store.SharedChannelStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SharedChannelStore)
}
}
return r0
}
// Status provides a mock function with given fields:
func (_m *Store) Status() store.StatusStore {
ret := _m.Called()
var r0 store.StatusStore
if rf, ok := ret.Get(0).(func() store.StatusStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StatusStore)
}
}
return r0
}
// System provides a mock function with given fields:
func (_m *Store) System() store.SystemStore {
ret := _m.Called()
var r0 store.SystemStore
if rf, ok := ret.Get(0).(func() store.SystemStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SystemStore)
}
}
return r0
}
// Team provides a mock function with given fields:
func (_m *Store) Team() store.TeamStore {
ret := _m.Called()
var r0 store.TeamStore
if rf, ok := ret.Get(0).(func() store.TeamStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TeamStore)
}
}
return r0
}
// TermsOfService provides a mock function with given fields:
func (_m *Store) TermsOfService() store.TermsOfServiceStore {
ret := _m.Called()
var r0 store.TermsOfServiceStore
if rf, ok := ret.Get(0).(func() store.TermsOfServiceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TermsOfServiceStore)
}
}
return r0
}
// Thread provides a mock function with given fields:
func (_m *Store) Thread() store.ThreadStore {
ret := _m.Called()
var r0 store.ThreadStore
if rf, ok := ret.Get(0).(func() store.ThreadStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ThreadStore)
}
}
return r0
}
// Token provides a mock function with given fields:
func (_m *Store) Token() store.TokenStore {
ret := _m.Called()
var r0 store.TokenStore
if rf, ok := ret.Get(0).(func() store.TokenStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TokenStore)
}
}
return r0
}
// TotalMasterDbConnections provides a mock function with given fields:
func (_m *Store) TotalMasterDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TotalReadDbConnections provides a mock function with given fields:
func (_m *Store) TotalReadDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TotalSearchDbConnections provides a mock function with given fields:
func (_m *Store) TotalSearchDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TrueUpReview provides a mock function with given fields:
func (_m *Store) TrueUpReview() store.TrueUpReviewStore {
ret := _m.Called()
var r0 store.TrueUpReviewStore
if rf, ok := ret.Get(0).(func() store.TrueUpReviewStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TrueUpReviewStore)
}
}
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *Store) UnlockFromMaster() {
_m.Called()
}
// UploadSession provides a mock function with given fields:
func (_m *Store) UploadSession() store.UploadSessionStore {
ret := _m.Called()
var r0 store.UploadSessionStore
if rf, ok := ret.Get(0).(func() store.UploadSessionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UploadSessionStore)
}
}
return r0
}
// User provides a mock function with given fields:
func (_m *Store) User() store.UserStore {
ret := _m.Called()
var r0 store.UserStore
if rf, ok := ret.Get(0).(func() store.UserStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserStore)
}
}
return r0
}
// UserAccessToken provides a mock function with given fields:
func (_m *Store) UserAccessToken() store.UserAccessTokenStore {
ret := _m.Called()
var r0 store.UserAccessTokenStore
if rf, ok := ret.Get(0).(func() store.UserAccessTokenStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserAccessTokenStore)
}
}
return r0
}
// UserTermsOfService provides a mock function with given fields:
func (_m *Store) UserTermsOfService() store.UserTermsOfServiceStore {
ret := _m.Called()
var r0 store.UserTermsOfServiceStore
if rf, ok := ret.Get(0).(func() store.UserTermsOfServiceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserTermsOfServiceStore)
}
}
return r0
}
// Webhook provides a mock function with given fields:
func (_m *Store) Webhook() store.WebhookStore {
ret := _m.Called()
var r0 store.WebhookStore
if rf, ok := ret.Get(0).(func() store.WebhookStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.WebhookStore)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SystemStore is an autogenerated mock type for the SystemStore type
type SystemStore struct {
mock.Mock
}
// Get provides a mock function with given fields:
func (_m *SystemStore) Get() (model.StringMap, error) {
ret := _m.Called()
var r0 model.StringMap
if rf, ok := ret.Get(0).(func() model.StringMap); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name
func (_m *SystemStore) GetByName(name string) (*model.System, error) {
ret := _m.Called(name)
var r0 *model.System
if rf, ok := ret.Get(0).(func(string) *model.System); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InsertIfExists provides a mock function with given fields: system
func (_m *SystemStore) InsertIfExists(system *model.System) (*model.System, error) {
ret := _m.Called(system)
var r0 *model.System
if rf, ok := ret.Get(0).(func(*model.System) *model.System); ok {
r0 = rf(system)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.System) error); ok {
r1 = rf(system)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByName provides a mock function with given fields: name
func (_m *SystemStore) PermanentDeleteByName(name string) (*model.System, error) {
ret := _m.Called(name)
var r0 *model.System
if rf, ok := ret.Get(0).(func(string) *model.System); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: system
func (_m *SystemStore) Save(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdate provides a mock function with given fields: system
func (_m *SystemStore) SaveOrUpdate(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdateWithWarnMetricHandling provides a mock function with given fields: system
func (_m *SystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: system
func (_m *SystemStore) Update(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TeamStore is an autogenerated mock type for the TeamStore type
type TeamStore struct {
mock.Mock
}
// AnalyticsGetTeamCountForScheme provides a mock function with given fields: schemeID
func (_m *TeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
ret := _m.Called(schemeID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(schemeID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsTeamCount provides a mock function with given fields: opts
func (_m *TeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.TeamSearch) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *TeamStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *TeamStore) ClearCaches() {
_m.Called()
}
// Get provides a mock function with given fields: id
func (_m *TeamStore) Get(id string) (*model.Team, error) {
ret := _m.Called(id)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetActiveMemberCount provides a mock function with given fields: teamID, restrictions
func (_m *TeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(teamID, restrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(teamID, restrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *TeamStore) GetAll() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllForExportAfter provides a mock function with given fields: limit, afterID
func (_m *TeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.TeamForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.TeamForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: offset, limit, opts
func (_m *TeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(offset, limit, opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(int, int, *model.TeamSearch) []*model.Team); ok {
r0 = rf(offset, limit, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, *model.TeamSearch) error); ok {
r1 = rf(offset, limit, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPrivateTeamListing provides a mock function with given fields:
func (_m *TeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllTeamListing provides a mock function with given fields:
func (_m *TeamStore) GetAllTeamListing() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByEmptyInviteID provides a mock function with given fields:
func (_m *TeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByInviteId provides a mock function with given fields: inviteID
func (_m *TeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
ret := _m.Called(inviteID)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(inviteID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(inviteID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name
func (_m *TeamStore) GetByName(name string) (*model.Team, error) {
ret := _m.Called(name)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: name
func (_m *TeamStore) GetByNames(name []string) ([]*model.Team, error) {
ret := _m.Called(name)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func([]string) []*model.Team); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnreadsForAllTeams provides a mock function with given fields: excludeTeamID, userID
func (_m *TeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
ret := _m.Called(excludeTeamID, userID)
var r0 []*model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelUnread); ok {
r0 = rf(excludeTeamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(excludeTeamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnreadsForTeam provides a mock function with given fields: teamID, userID
func (_m *TeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
ret := _m.Called(teamID, userID)
var r0 []*model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelUnread); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCommonTeamIDsForTwoUsers provides a mock function with given fields: userID, otherUserID
func (_m *TeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
ret := _m.Called(userID, otherUserID)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string) []string); ok {
r0 = rf(userID, otherUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, otherUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids
func (_m *TeamStore) GetMany(ids []string) ([]*model.Team, error) {
ret := _m.Called(ids)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func([]string) []*model.Team); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: ctx, teamID, userID
func (_m *TeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
ret := _m.Called(ctx, teamID, userID)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.TeamMember); ok {
r0 = rf(ctx, teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembers provides a mock function with given fields: teamID, offset, limit, teamMembersGetOptions
func (_m *TeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
ret := _m.Called(teamID, offset, limit, teamMembersGetOptions)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, int, int, *model.TeamMembersGetOptions) []*model.TeamMember); ok {
r0 = rf(teamID, offset, limit, teamMembersGetOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.TeamMembersGetOptions) error); ok {
r1 = rf(teamID, offset, limit, teamMembersGetOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByIds provides a mock function with given fields: teamID, userIds, restrictions
func (_m *TeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
ret := _m.Called(teamID, userIds, restrictions)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, []string, *model.ViewUsersRestrictions) []*model.TeamMember); ok {
r0 = rf(teamID, userIds, restrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, userIds, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewTeamMembersSince provides a mock function with given fields: teamID, since, offset, limit
func (_m *TeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
ret := _m.Called(teamID, since, offset, limit)
var r0 *model.NewTeamMembersList
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.NewTeamMembersList); ok {
r0 = rf(teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.NewTeamMembersList)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(string, int64, int, int) int64); ok {
r1 = rf(teamID, since, offset, limit)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, int64, int, int) error); ok {
r2 = rf(teamID, since, offset, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetTeamMembersForExport provides a mock function with given fields: userID
func (_m *TeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
ret := _m.Called(userID)
var r0 []*model.TeamMemberForExport
if rf, ok := ret.Get(0).(func(string) []*model.TeamMemberForExport); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMemberForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsByScheme provides a mock function with given fields: schemeID, offset, limit
func (_m *TeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
ret := _m.Called(schemeID, offset, limit)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Team); ok {
r0 = rf(schemeID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(schemeID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsByUserId provides a mock function with given fields: userID
func (_m *TeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
ret := _m.Called(userID)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string) []*model.Team); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsForUser provides a mock function with given fields: ctx, userID, excludeTeamID, includeDeleted
func (_m *TeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
ret := _m.Called(ctx, userID, excludeTeamID, includeDeleted)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) []*model.TeamMember); ok {
r0 = rf(ctx, userID, excludeTeamID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) error); ok {
r1 = rf(ctx, userID, excludeTeamID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsForUserWithPagination provides a mock function with given fields: userID, page, perPage
func (_m *TeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
ret := _m.Called(userID, page, perPage)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, int, int) []*model.TeamMember); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalMemberCount provides a mock function with given fields: teamID, restrictions
func (_m *TeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(teamID, restrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(teamID, restrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUserTeamIds provides a mock function with given fields: userID, allowFromCache
func (_m *TeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
ret := _m.Called(userID, allowFromCache)
var r0 []string
if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
r0 = rf(userID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupSyncedTeamCount provides a mock function with given fields:
func (_m *TeamStore) GroupSyncedTeamCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateAllTeamIdsForUser provides a mock function with given fields: userID
func (_m *TeamStore) InvalidateAllTeamIdsForUser(userID string) {
_m.Called(userID)
}
// MigrateTeamMembers provides a mock function with given fields: fromTeamID, fromUserID
func (_m *TeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
ret := _m.Called(fromTeamID, fromUserID)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, string) map[string]string); ok {
r0 = rf(fromTeamID, fromUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fromTeamID, fromUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: teamID
func (_m *TeamStore) PermanentDelete(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllMembersByTeam provides a mock function with given fields: teamID
func (_m *TeamStore) RemoveAllMembersByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllMembersByUser provides a mock function with given fields: userID
func (_m *TeamStore) RemoveAllMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMember provides a mock function with given fields: teamID, userID
func (_m *TeamStore) RemoveMember(teamID string, userID string) error {
ret := _m.Called(teamID, userID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(teamID, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMembers provides a mock function with given fields: teamID, userIds
func (_m *TeamStore) RemoveMembers(teamID string, userIds []string) error {
ret := _m.Called(teamID, userIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(teamID, userIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAllTeamSchemes provides a mock function with given fields:
func (_m *TeamStore) ResetAllTeamSchemes() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: team
func (_m *TeamStore) Save(team *model.Team) (*model.Team, error) {
ret := _m.Called(team)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(*model.Team) *model.Team); ok {
r0 = rf(team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Team) error); ok {
r1 = rf(team)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMember provides a mock function with given fields: member, maxUsersPerTeam
func (_m *TeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
ret := _m.Called(member, maxUsersPerTeam)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(*model.TeamMember, int) *model.TeamMember); ok {
r0 = rf(member, maxUsersPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamMember, int) error); ok {
r1 = rf(member, maxUsersPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultipleMembers provides a mock function with given fields: members, maxUsersPerTeam
func (_m *TeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
ret := _m.Called(members, maxUsersPerTeam)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func([]*model.TeamMember, int) []*model.TeamMember); ok {
r0 = rf(members, maxUsersPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.TeamMember, int) error); ok {
r1 = rf(members, maxUsersPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAll provides a mock function with given fields: opts
func (_m *TeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAllPaged provides a mock function with given fields: opts
func (_m *TeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(*model.TeamSearch) int64); ok {
r1 = rf(opts)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(*model.TeamSearch) error); ok {
r2 = rf(opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// SearchOpen provides a mock function with given fields: opts
func (_m *TeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPrivate provides a mock function with given fields: opts
func (_m *TeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: team
func (_m *TeamStore) Update(team *model.Team) (*model.Team, error) {
ret := _m.Called(team)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(*model.Team) *model.Team); ok {
r0 = rf(team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Team) error); ok {
r1 = rf(team)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastTeamIconUpdate provides a mock function with given fields: teamID, curTime
func (_m *TeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
ret := _m.Called(teamID, curTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(teamID, curTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMember provides a mock function with given fields: member
func (_m *TeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
ret := _m.Called(member)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(*model.TeamMember) *model.TeamMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMembersRole provides a mock function with given fields: teamID, userIDs
func (_m *TeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
ret := _m.Called(teamID, userIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(teamID, userIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMultipleMembers provides a mock function with given fields: members
func (_m *TeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
ret := _m.Called(members)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func([]*model.TeamMember) []*model.TeamMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.TeamMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserBelongsToTeams provides a mock function with given fields: userID, teamIds
func (_m *TeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
ret := _m.Called(userID, teamIds)
var r0 bool
if rf, ok := ret.Get(0).(func(string, []string) bool); ok {
r0 = rf(userID, teamIds)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, teamIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TermsOfServiceStore is an autogenerated mock type for the TermsOfServiceStore type
type TermsOfServiceStore struct {
mock.Mock
}
// Get provides a mock function with given fields: id, allowFromCache
func (_m *TermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(string, bool) *model.TermsOfService); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetLatest provides a mock function with given fields: allowFromCache
func (_m *TermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
ret := _m.Called(allowFromCache)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(bool) *model.TermsOfService); ok {
r0 = rf(allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: termsOfService
func (_m *TermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
ret := _m.Called(termsOfService)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(*model.TermsOfService) *model.TermsOfService); ok {
r0 = rf(termsOfService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TermsOfService) error); ok {
r1 = rf(termsOfService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
mock "github.com/stretchr/testify/mock"
)
// ThreadStore is an autogenerated mock type for the ThreadStore type
type ThreadStore struct {
mock.Mock
}
// DeleteMembershipForUser provides a mock function with given fields: userId, postID
func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) error {
ret := _m.Called(userId, postID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userId, postID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *ThreadStore) Get(id string) (*model.Thread, error) {
ret := _m.Called(id)
var r0 *model.Thread
if rf, ok := ret.Get(0).(func(string) *model.Thread); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Thread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembershipForUser provides a mock function with given fields: userId, postID
func (_m *ThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
ret := _m.Called(userId, postID)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string) *model.ThreadMembership); ok {
r0 = rf(userId, postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userId, postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembershipsForUser provides a mock function with given fields: userId, teamID
func (_m *ThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
ret := _m.Called(userId, teamID)
var r0 []*model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string) []*model.ThreadMembership); ok {
r0 = rf(userId, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userId, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsUnreadForUser provides a mock function with given fields: userID, teamIDs, includeUrgentMentionCount
func (_m *ThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
ret := _m.Called(userID, teamIDs, includeUrgentMentionCount)
var r0 map[string]*model.TeamUnread
if rf, ok := ret.Get(0).(func(string, []string, bool) map[string]*model.TeamUnread); ok {
r0 = rf(userID, teamIDs, includeUrgentMentionCount)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.TeamUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok {
r1 = rf(userID, teamIDs, includeUrgentMentionCount)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadFollowers provides a mock function with given fields: threadID, fetchOnlyActive
func (_m *ThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
ret := _m.Called(threadID, fetchOnlyActive)
var r0 []string
if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
r0 = rf(threadID, fetchOnlyActive)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(threadID, fetchOnlyActive)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadForUser provides a mock function with given fields: threadMembership, extended, postPriorityIsEnabled
func (_m *ThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
ret := _m.Called(threadMembership, extended, postPriorityIsEnabled)
var r0 *model.ThreadResponse
if rf, ok := ret.Get(0).(func(*model.ThreadMembership, bool, bool) *model.ThreadResponse); ok {
r0 = rf(threadMembership, extended, postPriorityIsEnabled)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership, bool, bool) error); ok {
r1 = rf(threadMembership, extended, postPriorityIsEnabled)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadUnreadReplyCount provides a mock function with given fields: threadMembership
func (_m *ThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
ret := _m.Called(threadMembership)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.ThreadMembership) int64); ok {
r0 = rf(threadMembership)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership) error); ok {
r1 = rf(threadMembership)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadsForUser provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
ret := _m.Called(userId, teamID, opts)
var r0 []*model.ThreadResponse
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) []*model.ThreadResponse); ok {
r0 = rf(userId, teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ThreadResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopThreadsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopThreadsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadMentions provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadUrgentMentions provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MaintainMembership provides a mock function with given fields: userID, postID, opts
func (_m *ThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
ret := _m.Called(userID, postID, opts)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string, store.ThreadMembershipOpts) *model.ThreadMembership); ok {
r0 = rf(userID, postID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, store.ThreadMembershipOpts) error); ok {
r1 = rf(userID, postID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MarkAllAsRead provides a mock function with given fields: userID, threadIds
func (_m *ThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
ret := _m.Called(userID, threadIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, threadIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAllAsReadByChannels provides a mock function with given fields: userID, channelIDs
func (_m *ThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
ret := _m.Called(userID, channelIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, channelIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAllAsReadByTeam provides a mock function with given fields: userID, teamID
func (_m *ThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
ret := _m.Called(userID, teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAsRead provides a mock function with given fields: userID, threadID, timestamp
func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
ret := _m.Called(userID, threadID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, threadID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteBatchThreadMembershipsForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// UpdateMembership provides a mock function with given fields: membership
func (_m *ThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
ret := _m.Called(membership)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(*model.ThreadMembership) *model.ThreadMembership); ok {
r0 = rf(membership)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership) error); ok {
r1 = rf(membership)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TokenStore is an autogenerated mock type for the TokenStore type
type TokenStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields: expiryTime
func (_m *TokenStore) Cleanup(expiryTime int64) {
_m.Called(expiryTime)
}
// Delete provides a mock function with given fields: token
func (_m *TokenStore) Delete(token string) error {
ret := _m.Called(token)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(token)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetAllTokensByType provides a mock function with given fields: tokenType
func (_m *TokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
ret := _m.Called(tokenType)
var r0 []*model.Token
if rf, ok := ret.Get(0).(func(string) []*model.Token); ok {
r0 = rf(tokenType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByToken provides a mock function with given fields: token
func (_m *TokenStore) GetByToken(token string) (*model.Token, error) {
ret := _m.Called(token)
var r0 *model.Token
if rf, ok := ret.Get(0).(func(string) *model.Token); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveAllTokensByType provides a mock function with given fields: tokenType
func (_m *TokenStore) RemoveAllTokensByType(tokenType string) error {
ret := _m.Called(tokenType)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenType)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: recovery
func (_m *TokenStore) Save(recovery *model.Token) error {
ret := _m.Called(recovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Token) error); ok {
r0 = rf(recovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TrueUpReviewStore is an autogenerated mock type for the TrueUpReviewStore type
type TrueUpReviewStore struct {
mock.Mock
}
// CreateTrueUpReviewStatusRecord provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTrueUpReviewStatus provides a mock function with given fields: dueDate
func (_m *TrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(dueDate)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(int64) *model.TrueUpReviewStatus); ok {
r0 = rf(dueDate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(dueDate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UploadSessionStore is an autogenerated mock type for the UploadSessionStore type
type UploadSessionStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: id
func (_m *UploadSessionStore) Delete(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *UploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
ret := _m.Called(ctx, id)
var r0 *model.UploadSession
if rf, ok := ret.Get(0).(func(context.Context, string) *model.UploadSession); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForUser provides a mock function with given fields: userID
func (_m *UploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
ret := _m.Called(userID)
var r0 []*model.UploadSession
if rf, ok := ret.Get(0).(func(string) []*model.UploadSession); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: session
func (_m *UploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
ret := _m.Called(session)
var r0 *model.UploadSession
if rf, ok := ret.Get(0).(func(*model.UploadSession) *model.UploadSession); ok {
r0 = rf(session)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UploadSession) error); ok {
r1 = rf(session)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: session
func (_m *UploadSessionStore) Update(session *model.UploadSession) error {
ret := _m.Called(session)
var r0 error
if rf, ok := ret.Get(0).(func(*model.UploadSession) error); ok {
r0 = rf(session)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UserAccessTokenStore is an autogenerated mock type for the UserAccessTokenStore type
type UserAccessTokenStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) Delete(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllForUser provides a mock function with given fields: userID
func (_m *UserAccessTokenStore) DeleteAllForUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
ret := _m.Called(tokenID)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) *model.UserAccessToken); ok {
r0 = rf(tokenID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *UserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
ret := _m.Called(offset, limit)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(int, int) []*model.UserAccessToken); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByToken provides a mock function with given fields: tokenString
func (_m *UserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
ret := _m.Called(tokenString)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) *model.UserAccessToken); ok {
r0 = rf(tokenString)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenString)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUser provides a mock function with given fields: userID, page, perPage
func (_m *UserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
ret := _m.Called(userID, page, perPage)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(string, int, int) []*model.UserAccessToken); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: token
func (_m *UserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
ret := _m.Called(token)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(*model.UserAccessToken) *model.UserAccessToken); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserAccessToken) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: term
func (_m *UserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
ret := _m.Called(term)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) []*model.UserAccessToken); ok {
r0 = rf(term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTokenDisable provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateTokenEnable provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// UserStore is an autogenerated mock type for the UserStore type
type UserStore struct {
mock.Mock
}
// AnalyticsActiveCount provides a mock function with given fields: timestamp, options
func (_m *UserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
ret := _m.Called(timestamp, options)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, model.UserCountOptions) int64); ok {
r0 = rf(timestamp, options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, model.UserCountOptions) error); ok {
r1 = rf(timestamp, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsActiveCountForPeriod provides a mock function with given fields: startTime, endTime, options
func (_m *UserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
ret := _m.Called(startTime, endTime, options)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, model.UserCountOptions) int64); ok {
r0 = rf(startTime, endTime, options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64, model.UserCountOptions) error); ok {
r1 = rf(startTime, endTime, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetExternalUsers provides a mock function with given fields: hostDomain
func (_m *UserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
ret := _m.Called(hostDomain)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(hostDomain)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(hostDomain)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetGuestCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetGuestCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetInactiveUsersCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetSystemAdminCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetSystemAdminCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteUsersInChannel provides a mock function with given fields: teamID, channelID, term, options
func (_m *UserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
ret := _m.Called(teamID, channelID, term, options)
var r0 *model.UserAutocompleteInChannel
if rf, ok := ret.Get(0).(func(string, string, string, *model.UserSearchOptions) *model.UserAutocompleteInChannel); ok {
r0 = rf(teamID, channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAutocompleteInChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *UserStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *UserStore) ClearCaches() {
_m.Called()
}
// Count provides a mock function with given fields: options
func (_m *UserStore) Count(options model.UserCountOptions) (int64, error) {
ret := _m.Called(options)
var r0 int64
if rf, ok := ret.Get(0).(func(model.UserCountOptions) int64); ok {
r0 = rf(options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.UserCountOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeactivateGuests provides a mock function with given fields:
func (_m *UserStore) DeactivateGuests() ([]string, error) {
ret := _m.Called()
var r0 []string
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DemoteUserToGuest provides a mock function with given fields: userID
func (_m *UserStore) DemoteUserToGuest(userID string) (*model.User, error) {
ret := _m.Called(userID)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id
func (_m *UserStore) Get(ctx context.Context, id string) (*model.User, error) {
ret := _m.Called(ctx, id)
var r0 *model.User
if rf, ok := ret.Get(0).(func(context.Context, string) *model.User); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *UserStore) GetAll() ([]*model.User, error) {
ret := _m.Called()
var r0 []*model.User
if rf, ok := ret.Get(0).(func() []*model.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllAfter provides a mock function with given fields: limit, afterID
func (_m *UserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(int, string) []*model.User); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllNotInAuthService provides a mock function with given fields: authServices
func (_m *UserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
ret := _m.Called(authServices)
var r0 []*model.User
if rf, ok := ret.Get(0).(func([]string) []*model.User); ok {
r0 = rf(authServices)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(authServices)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllProfiles provides a mock function with given fields: options
func (_m *UserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllProfilesInChannel provides a mock function with given fields: ctx, channelID, allowFromCache
func (_m *UserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
ret := _m.Called(ctx, channelID, allowFromCache)
var r0 map[string]*model.User
if rf, ok := ret.Get(0).(func(context.Context, string, bool) map[string]*model.User); ok {
r0 = rf(ctx, channelID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllUsingAuthService provides a mock function with given fields: authService
func (_m *UserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
ret := _m.Called(authService)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(authService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(authService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAnyUnreadPostCountForChannel provides a mock function with given fields: userID, channelID
func (_m *UserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
ret := _m.Called(userID, channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByAuth provides a mock function with given fields: authData, authService
func (_m *UserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
ret := _m.Called(authData, authService)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*string, string) *model.User); ok {
r0 = rf(authData, authService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string, string) error); ok {
r1 = rf(authData, authService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByEmail provides a mock function with given fields: email
func (_m *UserStore) GetByEmail(email string) (*model.User, error) {
ret := _m.Called(email)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(email)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(email)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUsername provides a mock function with given fields: username
func (_m *UserStore) GetByUsername(username string) (*model.User, error) {
ret := _m.Called(username)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(username)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(username)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelGroupUsers provides a mock function with given fields: channelID
func (_m *UserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
ret := _m.Called(channelID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEtagForAllProfiles provides a mock function with given fields:
func (_m *UserStore) GetEtagForAllProfiles() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetEtagForProfiles provides a mock function with given fields: teamID
func (_m *UserStore) GetEtagForProfiles(teamID string) string {
ret := _m.Called(teamID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetEtagForProfilesNotInTeam provides a mock function with given fields: teamID
func (_m *UserStore) GetEtagForProfilesNotInTeam(teamID string) string {
ret := _m.Called(teamID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetFirstSystemAdminID provides a mock function with given fields:
func (_m *UserStore) GetFirstSystemAdminID() (string, error) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForLogin provides a mock function with given fields: loginID, allowSignInWithUsername, allowSignInWithEmail
func (_m *UserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
ret := _m.Called(loginID, allowSignInWithUsername, allowSignInWithEmail)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string, bool, bool) *model.User); ok {
r0 = rf(loginID, allowSignInWithUsername, allowSignInWithEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool) error); ok {
r1 = rf(loginID, allowSignInWithUsername, allowSignInWithEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetKnownUsers provides a mock function with given fields: userID
func (_m *UserStore) GetKnownUsers(userID string) ([]string, error) {
ret := _m.Called(userID)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ctx, ids
func (_m *UserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
ret := _m.Called(ctx, ids)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(context.Context, []string) []*model.User); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions
func (_m *UserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfileByGroupChannelIdsForUser provides a mock function with given fields: userID, channelIds
func (_m *UserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
ret := _m.Called(userID, channelIds)
var r0 map[string][]*model.User
if rf, ok := ret.Get(0).(func(string, []string) map[string][]*model.User); ok {
r0 = rf(userID, channelIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfileByIds provides a mock function with given fields: ctx, userIds, options, allowFromCache
func (_m *UserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
ret := _m.Called(ctx, userIds, options, allowFromCache)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(context.Context, []string, *store.UserGetByIdsOpts, bool) []*model.User); ok {
r0 = rf(ctx, userIds, options, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []string, *store.UserGetByIdsOpts, bool) error); ok {
r1 = rf(ctx, userIds, options, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfiles provides a mock function with given fields: options
func (_m *UserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesByUsernames provides a mock function with given fields: usernames, viewRestrictions
func (_m *UserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(usernames, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func([]string, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(usernames, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(usernames, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannel provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannelByAdmin provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannelByStatus provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesNotInChannel provides a mock function with given fields: teamID, channelId, groupConstrained, offset, limit, viewRestrictions
func (_m *UserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, bool, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesNotInTeam provides a mock function with given fields: teamID, groupConstrained, offset, limit, viewRestrictions
func (_m *UserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, groupConstrained, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, bool, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, groupConstrained, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, groupConstrained, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesWithoutTeam provides a mock function with given fields: options
func (_m *UserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRecentlyActiveUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions
func (_m *UserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSystemAdminProfiles provides a mock function with given fields:
func (_m *UserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
ret := _m.Called()
var r0 map[string]*model.User
if rf, ok := ret.Get(0).(func() map[string]*model.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamGroupUsers provides a mock function with given fields: teamID
func (_m *UserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
ret := _m.Called(teamID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUnreadCount provides a mock function with given fields: userID, isCRTEnabled
func (_m *UserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
ret := _m.Called(userID, isCRTEnabled)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(userID, isCRTEnabled)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, isCRTEnabled)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUnreadCountForChannel provides a mock function with given fields: userID, channelID
func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
ret := _m.Called(userID, channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
func (_m *UserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
ret := _m.Called(startTime, startFileID, limit)
var r0 []*model.UserForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.UserForIndexing); ok {
r0 = rf(startTime, startFileID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startFileID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersWithInvalidEmails provides a mock function with given fields: page, perPage, restrictedDomains
func (_m *UserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
ret := _m.Called(page, perPage, restrictedDomains)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(int, int, string) []*model.User); ok {
r0 = rf(page, perPage, restrictedDomains)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, string) error); ok {
r1 = rf(page, perPage, restrictedDomains)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InferSystemInstallDate provides a mock function with given fields:
func (_m *UserStore) InferSystemInstallDate() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InsertUsers provides a mock function with given fields: users
func (_m *UserStore) InsertUsers(users []*model.User) error {
ret := _m.Called(users)
var r0 error
if rf, ok := ret.Get(0).(func([]*model.User) error); ok {
r0 = rf(users)
} else {
r0 = ret.Error(0)
}
return r0
}
// InvalidateProfileCacheForUser provides a mock function with given fields: userID
func (_m *UserStore) InvalidateProfileCacheForUser(userID string) {
_m.Called(userID)
}
// InvalidateProfilesInChannelCache provides a mock function with given fields: channelID
func (_m *UserStore) InvalidateProfilesInChannelCache(channelID string) {
_m.Called(channelID)
}
// InvalidateProfilesInChannelCacheByUser provides a mock function with given fields: userID
func (_m *UserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
_m.Called(userID)
}
// IsEmpty provides a mock function with given fields: excludeBots
func (_m *UserStore) IsEmpty(excludeBots bool) (bool, error) {
ret := _m.Called(excludeBots)
var r0 bool
if rf, ok := ret.Get(0).(func(bool) bool); ok {
r0 = rf(excludeBots)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(excludeBots)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: userID
func (_m *UserStore) PermanentDelete(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PromoteGuestToUser provides a mock function with given fields: userID
func (_m *UserStore) PromoteGuestToUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAuthDataToEmailForUsers provides a mock function with given fields: service, userIDs, includeDeleted, dryRun
func (_m *UserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
ret := _m.Called(service, userIDs, includeDeleted, dryRun)
var r0 int
if rf, ok := ret.Get(0).(func(string, []string, bool, bool) int); ok {
r0 = rf(service, userIDs, includeDeleted, dryRun)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool, bool) error); ok {
r1 = rf(service, userIDs, includeDeleted, dryRun)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ResetLastPictureUpdate provides a mock function with given fields: userID
func (_m *UserStore) ResetLastPictureUpdate(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: user
func (_m *UserStore) Save(user *model.User) (*model.User, error) {
ret := _m.Called(user)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok {
r0 = rf(user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User) error); ok {
r1 = rf(user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: teamID, term, options
func (_m *UserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(teamID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(teamID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInChannel provides a mock function with given fields: channelID, term, options
func (_m *UserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(channelID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInGroup provides a mock function with given fields: groupID, term, options
func (_m *UserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(groupID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(groupID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(groupID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInChannel provides a mock function with given fields: teamID, channelID, term, options
func (_m *UserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(teamID, channelID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(teamID, channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInGroup provides a mock function with given fields: groupID, term, options
func (_m *UserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(groupID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(groupID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(groupID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInTeam provides a mock function with given fields: notInTeamID, term, options
func (_m *UserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(notInTeamID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(notInTeamID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(notInTeamID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchWithoutTeam provides a mock function with given fields: term, options
func (_m *UserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.UserSearchOptions) error); ok {
r1 = rf(term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: user, allowRoleUpdate
func (_m *UserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
ret := _m.Called(user, allowRoleUpdate)
var r0 *model.UserUpdate
if rf, ok := ret.Get(0).(func(*model.User, bool) *model.UserUpdate); ok {
r0 = rf(user, allowRoleUpdate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserUpdate)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User, bool) error); ok {
r1 = rf(user, allowRoleUpdate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAuthData provides a mock function with given fields: userID, service, authData, email, resetMfa
func (_m *UserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
ret := _m.Called(userID, service, authData, email, resetMfa)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, *string, string, bool) string); ok {
r0 = rf(userID, service, authData, email, resetMfa)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *string, string, bool) error); ok {
r1 = rf(userID, service, authData, email, resetMfa)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateFailedPasswordAttempts provides a mock function with given fields: userID, attempts
func (_m *UserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
ret := _m.Called(userID, attempts)
var r0 error
if rf, ok := ret.Get(0).(func(string, int) error); ok {
r0 = rf(userID, attempts)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateLastPictureUpdate provides a mock function with given fields: userID
func (_m *UserStore) UpdateLastPictureUpdate(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMfaActive provides a mock function with given fields: userID, active
func (_m *UserStore) UpdateMfaActive(userID string, active bool) error {
ret := _m.Called(userID, active)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(userID, active)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMfaSecret provides a mock function with given fields: userID, secret
func (_m *UserStore) UpdateMfaSecret(userID string, secret string) error {
ret := _m.Called(userID, secret)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, secret)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateNotifyProps provides a mock function with given fields: userID, props
func (_m *UserStore) UpdateNotifyProps(userID string, props map[string]string) error {
ret := _m.Called(userID, props)
var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok {
r0 = rf(userID, props)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdatePassword provides a mock function with given fields: userID, newPassword
func (_m *UserStore) UpdatePassword(userID string, newPassword string) error {
ret := _m.Called(userID, newPassword)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, newPassword)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateUpdateAt provides a mock function with given fields: userID
func (_m *UserStore) UpdateUpdateAt(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// VerifyEmail provides a mock function with given fields: userID, email
func (_m *UserStore) VerifyEmail(userID string, email string) (string, error) {
ret := _m.Called(userID, email)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(userID, email)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, email)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UserTermsOfServiceStore is an autogenerated mock type for the UserTermsOfServiceStore type
type UserTermsOfServiceStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: userID, termsOfServiceId
func (_m *UserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
ret := _m.Called(userID, termsOfServiceId)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, termsOfServiceId)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetByUser provides a mock function with given fields: userID
func (_m *UserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
ret := _m.Called(userID)
var r0 *model.UserTermsOfService
if rf, ok := ret.Get(0).(func(string) *model.UserTermsOfService); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserTermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: userTermsOfService
func (_m *UserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
ret := _m.Called(userTermsOfService)
var r0 *model.UserTermsOfService
if rf, ok := ret.Get(0).(func(*model.UserTermsOfService) *model.UserTermsOfService); ok {
r0 = rf(userTermsOfService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserTermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserTermsOfService) error); ok {
r1 = rf(userTermsOfService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// WebhookStore is an autogenerated mock type for the WebhookStore type
type WebhookStore struct {
mock.Mock
}
// AnalyticsIncomingCount provides a mock function with given fields: teamID
func (_m *WebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsOutgoingCount provides a mock function with given fields: teamID
func (_m *WebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearCaches provides a mock function with given fields:
func (_m *WebhookStore) ClearCaches() {
_m.Called()
}
// DeleteIncoming provides a mock function with given fields: webhookID, timestamp
func (_m *WebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
ret := _m.Called(webhookID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(webhookID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOutgoing provides a mock function with given fields: webhookID, timestamp
func (_m *WebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
ret := _m.Called(webhookID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(webhookID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetIncoming provides a mock function with given fields: id, allowFromCache
func (_m *WebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, bool) *model.IncomingWebhook); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
ret := _m.Called(channelID)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string) []*model.IncomingWebhook); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByTeam provides a mock function with given fields: teamID, offset, limit
func (_m *WebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(teamID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByTeamByUser provides a mock function with given fields: teamID, userID, offset, limit
func (_m *WebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingList provides a mock function with given fields: offset, limit
func (_m *WebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(int, int) []*model.IncomingWebhook); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingListByUser provides a mock function with given fields: userID, offset, limit
func (_m *WebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoing provides a mock function with given fields: id
func (_m *WebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
ret := _m.Called(id)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string) *model.OutgoingWebhook); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByChannel provides a mock function with given fields: channelID, offset, limit
func (_m *WebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(channelID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByChannelByUser provides a mock function with given fields: channelID, userID, offset, limit
func (_m *WebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(channelID, userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(channelID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(channelID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByTeam provides a mock function with given fields: teamID, offset, limit
func (_m *WebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(teamID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByTeamByUser provides a mock function with given fields: teamID, userID, offset, limit
func (_m *WebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingList provides a mock function with given fields: offset, limit
func (_m *WebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(int, int) []*model.OutgoingWebhook); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingListByUser provides a mock function with given fields: userID, offset, limit
func (_m *WebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateWebhookCache provides a mock function with given fields: webhook
func (_m *WebhookStore) InvalidateWebhookCache(webhook string) {
_m.Called(webhook)
}
// PermanentDeleteIncomingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteIncomingByUser provides a mock function with given fields: userID
func (_m *WebhookStore) PermanentDeleteIncomingByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteOutgoingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteOutgoingByUser provides a mock function with given fields: userID
func (_m *WebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveIncoming provides a mock function with given fields: webhook
func (_m *WebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(*model.IncomingWebhook) *model.IncomingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.IncomingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveOutgoing provides a mock function with given fields: webhook
func (_m *WebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(*model.OutgoingWebhook) *model.OutgoingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OutgoingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateIncoming provides a mock function with given fields: webhook
func (_m *WebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(*model.IncomingWebhook) *model.IncomingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.IncomingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateOutgoing provides a mock function with given fields: hook
func (_m *WebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
ret := _m.Called(hook)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(*model.OutgoingWebhook) *model.OutgoingWebhook); ok {
r0 = rf(hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OutgoingWebhook) error); ok {
r1 = rf(hook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const PluginIdJenkins = "jenkins"
func TestNotifyAdminStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testNotifyAdminStoreSave(t, ss) })
t.Run("testGetDataByUserIdAndFeature", func(t *testing.T) { testGetDataByUserIdAndFeature(t, ss) })
t.Run("testGet", func(t *testing.T) { testGet(t, ss) })
t.Run("testDeleteBefore", func(t *testing.T) { testDeleteBefore(t, ss) })
t.Run("testUpdate", func(t *testing.T) { testUpdate(t, ss) })
}
func tearDown(t *testing.T, ss store.Store) {
err := ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis())
require.NoError(t, err)
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis())
require.NoError(t, err)
}
func testNotifyAdminStoreSave(t *testing.T, ss store.Store) {
d1 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
// unknow plan error
d2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: "Unknown feature",
}
_, err = ss.NotifyAdmin().Save(d2)
require.Error(t, err)
// unknown feature error
d3 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: "Unknown plan",
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d3)
require.Error(t, err)
// same user requesting same feature error
singleUserId := model.NewId()
d5 := &model.NotifyAdminData{
UserId: singleUserId,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d5)
require.NoError(t, err)
d6 := &model.NotifyAdminData{
UserId: singleUserId,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d6)
require.Error(t, err)
tearDown(t, ss)
}
func testGet(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
d1Trial := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuEnterprise,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial)
require.NoError(t, err)
d1Trial2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuEnterprise,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial2)
require.NoError(t, err)
upgradeRequests, err := ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(upgradeRequests), 1)
trialRequests, err := ss.NotifyAdmin().Get(true)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 2)
tearDown(t, ss)
}
func testGetDataByUserIdAndFeature(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
userId2 := model.NewId()
d2 := &model.NotifyAdminData{
UserId: userId2,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureCustomUsergroups,
}
_, err = ss.NotifyAdmin().Save(d2)
require.NoError(t, err)
user1Request, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(userId1, model.PaidFeatureAllProfessionalfeatures)
require.NoError(t, err)
require.Equal(t, len(user1Request), 1)
require.Equal(t, user1Request[0].RequiredFeature, model.PaidFeatureAllProfessionalfeatures)
tearDown(t, ss)
}
func testUpdate(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: PluginIdJenkins,
RequiredFeature: model.PluginFeature,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
err = ss.NotifyAdmin().Update(d1.UserId, d1.RequiredPlan, d1.RequiredFeature, 100)
require.NoError(t, err)
userRequest, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(d1.UserId, d1.RequiredFeature)
require.NoError(t, err)
require.Equal(t, len(userRequest), 1)
require.Equal(t, userRequest[0].SentAt, sql.NullInt64{Int64: 100, Valid: true})
tearDown(t, ss)
}
func testDeleteBefore(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
d1Trial := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial)
require.NoError(t, err)
d1Trial2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial2)
require.NoError(t, err)
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis()) // delete all upgrade requests
require.NoError(t, err)
upgradeRequests, err := ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(upgradeRequests), 0)
trialRequests, err := ss.NotifyAdmin().Get(true)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 2) // trial requests should still exist
err = ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis()) // delete all trial requests
require.NoError(t, err)
trialRequests, err = ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestOAuthStore(t *testing.T, ss store.Store) {
t.Run("SaveApp", func(t *testing.T) { testOAuthStoreSaveApp(t, ss) })
t.Run("GetApp", func(t *testing.T) { testOAuthStoreGetApp(t, ss) })
t.Run("UpdateApp", func(t *testing.T) { testOAuthStoreUpdateApp(t, ss) })
t.Run("SaveAccessData", func(t *testing.T) { testOAuthStoreSaveAccessData(t, ss) })
t.Run("OAuthUpdateAccessData", func(t *testing.T) { testOAuthUpdateAccessData(t, ss) })
t.Run("GetAccessData", func(t *testing.T) { testOAuthStoreGetAccessData(t, ss) })
t.Run("RemoveAccessData", func(t *testing.T) { testOAuthStoreRemoveAccessData(t, ss) })
t.Run("RemoveAllAccessData", func(t *testing.T) { testOAuthStoreRemoveAllAccessData(t, ss) })
t.Run("SaveAuthData", func(t *testing.T) { testOAuthStoreSaveAuthData(t, ss) })
t.Run("GetAuthData", func(t *testing.T) { testOAuthStoreGetAuthData(t, ss) })
t.Run("RemoveAuthData", func(t *testing.T) { testOAuthStoreRemoveAuthData(t, ss) })
t.Run("RemoveAuthDataByUser", func(t *testing.T) { testOAuthStoreRemoveAuthDataByUser(t, ss) })
t.Run("OAuthGetAuthorizedApps", func(t *testing.T) { testOAuthGetAuthorizedApps(t, ss) })
t.Run("OAuthGetAccessDataByUserForApp", func(t *testing.T) { testOAuthGetAccessDataByUserForApp(t, ss) })
t.Run("DeleteApp", func(t *testing.T) { testOAuthStoreDeleteApp(t, ss) })
}
func testOAuthStoreSaveApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
// Try to save an app that already has an Id
a1.Id = model.NewId()
_, err := ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, cannot add an OAuth app cannot be save with an Id, it has to be updated")
// Try to save an Invalid App
a1.Id = ""
_, err = ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, app should be invalid cause it doesn' have a name set")
a1.Name = "TestApp" + model.NewId() // Valid name
a1.MattermostAppID = "a very, very, very, very, very, very, very long id"
_, err = ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, app should be invalid cause the MattermostAppID is to long")
// Save the app
a1.Id = ""
a1.MattermostAppID = "some small id" // Valid id
_, err = ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
}
func testOAuthStoreGetApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// Lets try to get and app that does not exists
_, err = ss.OAuth().GetApp("fake0123456789abcderfgret1")
require.Error(t, err, "Should have failed. App does not exists")
_, err = ss.OAuth().GetApp(a1.Id)
require.NoError(t, err)
// Lets try and get the app from a user that hasn't created any apps
apps, err := ss.OAuth().GetAppByUser("fake0123456789abcderfgret1", 0, 1000)
require.NoError(t, err)
assert.Empty(t, apps, "Should have failed. Fake user hasn't created any apps")
_, err = ss.OAuth().GetAppByUser(a1.CreatorId, 0, 1000)
require.NoError(t, err)
_, err = ss.OAuth().GetApps(0, 1000)
require.NoError(t, err)
}
func testOAuthStoreUpdateApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// temporarily save the created app id
id := a1.Id
a1.CreateAt = 1
a1.ClientSecret = "pwd"
a1.CreatorId = "12345678901234567890123456"
// Lets update the app by removing the name
a1.Name = ""
_, err = ss.OAuth().UpdateApp(&a1)
require.Error(t, err, "Should have failed. App name is not set")
// Lets not find the app that we are trying to update
a1.Id = "fake0123456789abcderfgret1"
a1.Name = "NewName"
_, err = ss.OAuth().UpdateApp(&a1)
require.Error(t, err, "Should have failed. Not able to find the app")
a1.Id = id
ua, err := ss.OAuth().UpdateApp(&a1)
require.NoError(t, err)
require.Equal(t, ua.Name, "NewName", "name did not update")
require.NotEqual(t, ua.CreateAt, 1, "create at should not have updated")
require.NotEqual(t, ua.CreatorId, "12345678901234567890123456", "creator id should not have updated")
}
func testOAuthStoreSaveAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
// Lets try and save an incomplete access data
_, err := ss.OAuth().SaveAccessData(&a1)
require.Error(t, err, "Should have failed. Access data needs the token")
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
}
func testOAuthUpdateAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.ExpiresAt = model.GetMillis()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
//Try to update to invalid Refresh Token
refreshToken := a1.RefreshToken
a1.RefreshToken = model.NewId() + "123"
_, err = ss.OAuth().UpdateAccessData(&a1)
require.Error(t, err, "Should have failed with invalid token")
//Try to update to invalid RedirectUri
a1.RefreshToken = model.NewId()
a1.RedirectUri = ""
_, err = ss.OAuth().UpdateAccessData(&a1)
require.Error(t, err, "Should have failed with invalid Redirect URI")
// Should update fine
a1.RedirectUri = "http://example.com"
ra1, err := ss.OAuth().UpdateAccessData(&a1)
require.NoError(t, err)
require.NotEqual(t, ra1.RefreshToken, refreshToken, "refresh tokens didn't match")
}
func testOAuthStoreGetAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.ExpiresAt = model.GetMillis()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
_, err = ss.OAuth().GetAccessData("invalidToken")
require.Error(t, err, "Should have failed. There is no data with an invalid token")
ra1, err := ss.OAuth().GetAccessData(a1.Token)
require.NoError(t, err)
assert.Equal(t, a1.Token, ra1.Token, "tokens didn't match")
_, err = ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.NoError(t, err)
_, err = ss.OAuth().GetPreviousAccessData("user", "junk")
require.NoError(t, err)
// Try to get the Access data using an invalid refresh token
_, err = ss.OAuth().GetAccessDataByRefreshToken(a1.Token)
require.Error(t, err, "Should have failed. There is no data with an invalid token")
// Get the Access Data using the refresh token
ra1, err = ss.OAuth().GetAccessDataByRefreshToken(a1.RefreshToken)
require.NoError(t, err)
assert.Equal(t, a1.RefreshToken, ra1.RefreshToken, "tokens didn't match")
}
func testOAuthStoreRemoveAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAccessData(a1.Token)
require.NoError(t, err)
result, _ := ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.Nil(t, result, "did not delete access token")
}
func testOAuthStoreRemoveAllAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAllAccessData()
require.NoError(t, err)
result, _ := ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.Nil(t, result, "did not delete access token")
}
func testOAuthStoreSaveAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
}
func testOAuthStoreGetAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
_, err = ss.OAuth().GetAuthData(a1.Code)
require.NoError(t, err)
}
func testOAuthStoreRemoveAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAuthData(a1.Code)
require.NoError(t, err)
_, err = ss.OAuth().GetAuthData(a1.Code)
require.Error(t, err, "should have errored - auth code removed")
}
func testOAuthStoreRemoveAuthDataByUser(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
err = ss.OAuth().PermanentDeleteAuthDataByUser(a1.UserId)
require.NoError(t, err)
}
func testOAuthGetAuthorizedApps(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// Lets try and get an Authorized app for a user who hasn't authorized it
apps, err := ss.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1", 0, 1000)
require.NoError(t, err)
assert.Empty(t, apps, "Should have failed. Fake user hasn't authorized the app")
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PreferenceCategoryAuthorizedOAuthApp
p.Name = a1.Id
p.Value = "true"
nErr := ss.Preference().Save(model.Preferences{p})
require.NoError(t, nErr)
apps, err = ss.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000)
require.NoError(t, err)
assert.NotEqual(t, len(apps), 0, "It should have return apps")
}
func testOAuthGetAccessDataByUserForApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PreferenceCategoryAuthorizedOAuthApp
p.Name = a1.Id
p.Value = "true"
nErr := ss.Preference().Save(model.Preferences{p})
require.NoError(t, nErr)
apps, err := ss.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000)
require.NoError(t, err)
assert.NotEqual(t, len(apps), 0, "It should have return apps")
// save the token
ad1 := model.AccessData{}
ad1.ClientId = a1.Id
ad1.UserId = a1.CreatorId
ad1.Token = model.NewId()
ad1.RefreshToken = model.NewId()
ad1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&ad1)
require.NoError(t, err)
accessData, err := ss.OAuth().GetAccessDataByUserForApp(a1.CreatorId, a1.Id)
require.NoError(t, err)
assert.NotEqual(t, len(accessData), 0, "It should have return access data")
}
func testOAuthStoreDeleteApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// delete a non-existent app
err = ss.OAuth().DeleteApp("fakeclientId")
require.NoError(t, err)
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.Token = model.NewId()
s1.IsOAuth = true
s1, nErr := ss.Session().Save(s1)
require.NoError(t, nErr)
ad1 := model.AccessData{}
ad1.ClientId = a1.Id
ad1.UserId = a1.CreatorId
ad1.Token = s1.Token
ad1.RefreshToken = model.NewId()
ad1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&ad1)
require.NoError(t, err)
err = ss.OAuth().DeleteApp(a1.Id)
require.NoError(t, err)
_, nErr = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, nErr, "should error - session should be deleted")
_, err = ss.OAuth().GetAccessData(s1.Token)
require.Error(t, err, "should error - access data should be deleted")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPluginStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveOrUpdate", func(t *testing.T) { testPluginSaveOrUpdate(t, ss) })
t.Run("CompareAndSet", func(t *testing.T) { testPluginCompareAndSet(t, ss) })
t.Run("CompareAndDelete", func(t *testing.T) { testPluginCompareAndDelete(t, ss) })
t.Run("SetWithOptions", func(t *testing.T) { testPluginSetWithOptions(t, ss) })
t.Run("Get", func(t *testing.T) { testPluginGet(t, ss) })
t.Run("Delete", func(t *testing.T) { testPluginDelete(t, ss) })
t.Run("DeleteAllForPlugin", func(t *testing.T) { testPluginDeleteAllForPlugin(t, ss) })
t.Run("DeleteAllExpired", func(t *testing.T) { testPluginDeleteAllExpired(t, ss) })
t.Run("List", func(t *testing.T) { testPluginList(t, ss) })
}
func setupKVs(t *testing.T, ss store.Store) (string, func()) {
pluginId := model.NewId()
otherPluginId := model.NewId()
// otherKV is another key value for the current plugin, and used to verify other keys
// aren't modified unintentionally.
otherKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherKV)
require.NoError(t, err)
// otherPluginKV is a key value for another plugin, and used to verify other plugins' keys
// aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginId, func() {
actualOtherKV, err := ss.Plugin().Get(otherKV.PluginId, otherKV.Key)
require.NoError(t, err, "failed to find other key value for same plugin")
assert.Equal(t, otherKV, actualOtherKV)
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
func doTestPluginSaveOrUpdate(t *testing.T, ss store.Store, doer func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
kv, err := doer(kv)
require.Error(t, err)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
assert.Nil(t, kv)
})
t.Run("new key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(value), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for new key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
var value []byte
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: value,
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
t.Run("existing key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
newValue := model.NewId()
kv.Value = []byte(newValue)
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(newValue), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for existing key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
kv.Value = nil
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
}
func testPluginSaveOrUpdate(t *testing.T, ss store.Store) {
doTestPluginSaveOrUpdate(t, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
return ss.Plugin().SaveOrUpdate(kv)
})
}
// doTestPluginCompareAndSet exercises the CompareAndSet functionality, but abstracts the actual
// call to same to allow reuse with SetWithOptions
func doTestPluginCompareAndSet(t *testing.T, ss store.Store, compareAndSet func(kv *model.PluginKeyValue, oldValue []byte) (bool, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := compareAndSet(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
// assertChanged verifies that CompareAndSet successfully changes to the given value.
assertChanged := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
require.NoError(t, nErr)
// When tested with KVSetWithOptions, a strict comparison can fail because that
// function accepts a relative time and makes its own call to model.GetMillis(),
// leading to off-by-one issues. All these tests are written with 15+ second
// differences, so allow for an off-by-1000ms in either direction.
require.NotNil(t, actualKV)
expiryDelta := actualKV.ExpireAt - kv.ExpireAt
if expiryDelta > -1000 && expiryDelta < 1000 {
actualKV.ExpireAt = kv.ExpireAt
}
assert.Equal(t, kv, actualKV)
}
// assertUnchanged verifies that CompareAndSet fails, leaving the existing value.
assertUnchanged := func(t *testing.T, kv, existingKV *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.False(t, ok, "should have failed to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
if existingKV == nil {
require.Error(t, nErr)
_, ok := nErr.(*store.ErrNotFound)
assert.True(t, ok)
assert.Nil(t, actualKV)
} else {
require.NoError(t, nErr)
assert.Equal(t, existingKV, actualKV)
}
}
// assertRemoved verifies that CompareAndSet successfully removes the given value.
assertRemoved := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok = nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
}
// Non-existent keys and expired keys should behave identically.
for description, setup := range map[string]func(t *testing.T) (*model.PluginKeyValue, func()){
"non-existent key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
return kv, tearDown
},
"expired key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
expiredKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 1,
}
_, err := ss.Plugin().SaveOrUpdate(expiredKV)
require.NoError(t, err)
return expiredKV, tearDown
},
} {
t.Run(description, func(t *testing.T) {
t.Run("setting a nil value should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.Value = nil
assertUnchanged(t, kv, nil, oldValue)
})
}
})
t.Run("setting a non-nil value", func(t *testing.T) {
t.Run("should succeed given non-expiring, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 0
assertChanged(t, kv, []byte(nil))
})
t.Run("should succeed given not-yet-expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, []byte(nil))
})
t.Run("should fail given expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 1
assertRemoved(t, kv, []byte(nil))
})
t.Run("should fail given 'different' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, []byte(model.NewId()))
})
t.Run("should fail given 'same' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, kv.Value)
})
})
})
}
t.Run("existing key", func(t *testing.T) {
setup := func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
existingKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(existingKV)
require.NoError(t, err)
return existingKV, tearDown
}
testCases := map[string]bool{
// CompareAndSet should succeed even if the value isn't changing.
"setting the same value": true,
"setting a different value": false,
}
for description, setToSameValue := range testCases {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
ExpireAt: existingKV.ExpireAt,
}
if setToSameValue {
kv.Value = existingKV.Value
} else {
kv.Value = []byte(model.NewId())
}
return kv
}
t.Run(description, func(t *testing.T) {
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertChanged(t, kv, existingKV.Value)
})
t.Run("and future expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, existingKV.Value)
})
t.Run("and past expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() - 15*1000
assertRemoved(t, kv, existingKV.Value)
})
})
}
t.Run("setting a nil value", func(t *testing.T) {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
Value: existingKV.Value,
ExpireAt: existingKV.ExpireAt,
}
kv.Value = nil
return kv
}
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed, deleting, given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertRemoved(t, kv, existingKV.Value)
})
})
})
}
func testPluginCompareAndSet(t *testing.T, ss store.Store) {
doTestPluginCompareAndSet(t, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
return ss.Plugin().CompareAndSet(kv, oldValue)
})
}
func testPluginCompareAndDelete(t *testing.T, ss store.Store) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := ss.Plugin().CompareAndDelete(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("non-existent key should fail", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("expired key should fail", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
"given same old value": []byte(value),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("existing key should fail given different old value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(model.NewId())
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
t.Run("existing key should succeed given same old value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(value)
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.True(t, ok)
})
}
func testPluginSetWithOptions(t *testing.T, ss store.Store) {
t.Run("invalid options", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
pluginId := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{
Atomic: false,
OldValue: []byte("not-nil"),
}
ok, err := ss.Plugin().SetWithOptions(pluginId, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_kvset_options.is_valid.old_value.app_error", appErr.Id)
})
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
pluginId := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{}
ok, err := ss.Plugin().SetWithOptions(pluginId, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("atomic", func(t *testing.T) {
doTestPluginCompareAndSet(t, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: true,
OldValue: oldValue,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
return ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
})
})
t.Run("non-atomic", func(t *testing.T) {
doTestPluginSaveOrUpdate(t, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: false,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
ok, err := ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
if !ok {
return nil, err
}
return kv, err
})
})
}
func testPluginGet(t *testing.T, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
kv, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for plugin id", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(model.NewId(), key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for key", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("old expired key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("recently expired key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() - 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("matching key value, non-expiring", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginId, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
t.Run("matching key value, not yet expired", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() + 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginId, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
}
func testPluginDelete(t *testing.T, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
err := ss.Plugin().Delete(pluginId, key)
require.NoError(t, err)
kv, err := ss.Plugin().Get(pluginId, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
testCases := []struct {
description string
expireAt int64
}{
{
"expired key value",
model.GetMillis() - 15*1000,
},
{
"never expiring value",
0,
},
{
"not yet expired value",
model.GetMillis() + 15*1000,
},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := testCase.expireAt
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
err = ss.Plugin().Delete(pluginId, key)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
}
}
func testPluginDeleteAllForPlugin(t *testing.T, ss store.Store) {
setupKVsForDeleteAll := func(t *testing.T) (string, func()) {
pluginId := model.NewId()
otherPluginId := model.NewId()
// otherPluginKV is another key value for another plugin, and used to verify other
// keys aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginId, func() {
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
t.Run("no keys to delete", func(t *testing.T) {
pluginId, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
err := ss.Plugin().DeleteAllForPlugin(pluginId)
require.NoError(t, err)
})
t.Run("multiple keys to delete", func(t *testing.T) {
pluginId, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv2 := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kv2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllForPlugin(pluginId)
require.NoError(t, err)
_, err = ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
_, err = ss.Plugin().Get(kv.PluginId, kv2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
})
}
func testPluginDeleteAllExpired(t *testing.T, ss store.Store) {
t.Run("no keys", func(t *testing.T) {
err := ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
})
t.Run("no expiring keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("no expired keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("some expired keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
expiredKVA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
expiredKVB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, expiredKVA2.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, expiredKVB2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVB2)
})
}
func testPluginList(t *testing.T, ss store.Store) {
t.Run("no key values", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
keys, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
assert.Empty(t, keys)
})
t.Run("single key", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys, 1)
assert.Equal(t, kv.Key, keys[0])
})
t.Run("multiple keys", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
for i := 0; i < 150; i++ {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginId, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 50)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("multiple keys, some expiring", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
now := model.GetMillis()
for i := 0; i < 150; i++ {
key := model.NewId()
var expireAt int64
if i%10 == 0 {
// Expire keys 0, 10, 20, ...
expireAt = 1
} else if (i+5)%10 == 0 {
// Mark for future expiry keys 5, 15, 25, ...
expireAt = now + 5*60*1000
}
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
if expireAt == 0 || expireAt > now {
keys = append(keys, key)
}
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginId, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 35)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("offsets and limits", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
for i := 0; i < 150; i++ {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
t.Run("default limit", func(t *testing.T) {
keys1, err := ss.Plugin().List(pluginId, 0, 0)
require.NoError(t, err)
require.Len(t, keys1, 10)
})
t.Run("offset 0, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginId, 0, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
t.Run("offset 1, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginId, 1, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPostAcknowledgementsStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testPostAcknowledgementsStoreSave(t, ss) })
t.Run("GetForPost", func(t *testing.T) { testPostAcknowledgementsStoreGetForPost(t, ss) })
t.Run("GetForPosts", func(t *testing.T) { testPostAcknowledgementsStoreGetForPosts(t, ss) })
}
func testPostAcknowledgementsStoreSave(t *testing.T, ss store.Store) {
userId1 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
post, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("consecutive saves should just update the acknowledged at", func(t *testing.T) {
_, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
_, err = ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
ack1, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(post.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1})
})
t.Run("saving should update the update at of the post", func(t *testing.T) {
oldUpdateAt := post.UpdateAt
_, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
post, err = ss.Post().GetSingle(post.Id, false)
require.NoError(t, err)
require.Greater(t, post.UpdateAt, oldUpdateAt)
})
}
func testPostAcknowledgementsStoreGetForPost(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p1.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}
func testPostAcknowledgementsStoreGetForPosts(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(""),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p2.Id, userId2, 0)
require.NoError(t, err)
ack4, err := ss.PostAcknowledgement().Save(p2.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPosts([]string{p1.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack4})
err = ss.PostAcknowledgement().Delete(ack4)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPostPriorityStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetForPost", func(t *testing.T) { testPostPriorityStoreGetForPost(t, ss) })
}
func testPostPriorityStoreGetForPost(t *testing.T, ss store.Store) {
t.Run("Save post priority when in post's metadata", func(t *testing.T) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p3 := model.Post{}
p3.ChannelId = model.NewId()
p3.UserId = model.NewId()
p3.Message = NewTestId()
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
pp1, err := ss.PostPriority().GetForPost(p1.Id)
require.NoError(t, err)
assert.Equal(t, "important", *pp1.Priority)
assert.Equal(t, true, *pp1.RequestedAck)
assert.Equal(t, false, *pp1.PersistentNotifications)
pp2, err := ss.PostPriority().GetForPost(p2.Id)
require.NoError(t, err)
assert.Equal(t, model.PostPriorityUrgent, *pp2.Priority)
assert.Equal(t, false, *pp2.RequestedAck)
assert.Equal(t, true, *pp2.PersistentNotifications)
_, err = ss.PostPriority().GetForPost(p3.Id)
assert.True(t, errors.Is(err, sql.ErrNoRows))
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestPostStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveMultiple", func(t *testing.T) { testPostStoreSaveMultiple(t, ss) })
t.Run("Save", func(t *testing.T) { testPostStoreSave(t, ss) })
t.Run("SaveAndUpdateChannelMsgCounts", func(t *testing.T) { testPostStoreSaveChannelMsgCounts(t, ss) })
t.Run("Get", func(t *testing.T) { testPostStoreGet(t, ss) })
t.Run("GetSingle", func(t *testing.T) { testPostStoreGetSingle(t, ss) })
t.Run("Update", func(t *testing.T) { testPostStoreUpdate(t, ss) })
t.Run("Delete", func(t *testing.T) { testPostStoreDelete(t, ss) })
t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, ss) })
t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, ss) })
t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, ss) })
t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, ss) })
t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) })
t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) })
t.Run("GetPosts", func(t *testing.T) { testPostStoreGetPosts(t, ss) })
t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) })
t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testPostCountsByDay(t, ss) })
t.Run("PostCounts", func(t *testing.T) { testPostCounts(t, ss) })
t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) })
t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, ss) })
t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, ss) })
t.Run("GetPostsCreatedAt", func(t *testing.T) { testPostStoreGetPostsCreatedAt(t, ss) })
t.Run("Overwrite", func(t *testing.T) { testPostStoreOverwrite(t, ss) })
t.Run("OverwriteMultiple", func(t *testing.T) { testPostStoreOverwriteMultiple(t, ss) })
t.Run("GetPostsByIds", func(t *testing.T) { testPostStoreGetPostsByIds(t, ss) })
t.Run("GetPostsBatchForIndexing", func(t *testing.T) { testPostStoreGetPostsBatchForIndexing(t, ss) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testPostStorePermanentDeleteBatch(t, ss) })
t.Run("GetOldest", func(t *testing.T) { testPostStoreGetOldest(t, ss) })
t.Run("TestGetMaxPostSize", func(t *testing.T) { testGetMaxPostSize(t, ss) })
t.Run("GetParentsForExportAfter", func(t *testing.T) { testPostStoreGetParentsForExportAfter(t, ss) })
t.Run("GetRepliesForExport", func(t *testing.T) { testPostStoreGetRepliesForExport(t, ss) })
t.Run("GetDirectPostParentsForExportAfter", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfter(t, ss, s) })
t.Run("GetDirectPostParentsForExportAfterDeleted", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterDeleted(t, ss, s) })
t.Run("GetDirectPostParentsForExportAfterBatched", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterBatched(t, ss, s) })
t.Run("GetForThread", func(t *testing.T) { testPostStoreGetForThread(t, ss) })
t.Run("HasAutoResponsePostByUserSince", func(t *testing.T) { testHasAutoResponsePostByUserSince(t, ss) })
t.Run("GetPostsSinceForSync", func(t *testing.T) { testGetPostsSinceForSync(t, ss, s) })
t.Run("SetPostReminder", func(t *testing.T) { testSetPostReminder(t, ss, s) })
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, ss, s) })
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, ss, s) })
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, ss) })
t.Run("GetTopDMsForUserSince", func(t *testing.T) { testGetTopDMsForUserSince(t, ss, s) })
t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, ss) })
}
func testPostStoreSave(t *testing.T, ss store.Store) {
t.Run("Save post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
p, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
})
t.Run("Save replies", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestId()
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestId()
p1, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p1.ReplyCount)
p2, err := ss.Post().Save(&o2)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(2), p2.ReplyCount)
p3, err := ss.Post().Save(&o3)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p3.ReplyCount)
})
t.Run("Try to save existing post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
_, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Post().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
_, err = ss.Post().Save(&rootPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.Id
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&replyPost)
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost.UpdateAt, rootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestId()
channel.DisplayName = NewTestId()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(&channel, 100)
require.NoError(t, err)
post := model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(1), rchannel.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
post.CreateAt = 5
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel2, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Equal(t, rchannel.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(2), rchannel2.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel3, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel3.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(3), rchannel3.TotalMsgCount)
})
t.Run("Save post with priority metadata set", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
pp, err := ss.PostPriority().GetForPost(p.Id)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, "important", *pp.Priority)
assert.Equal(t, true, *pp.RequestedAck)
assert.Equal(t, false, *pp.PersistentNotifications)
})
}
func testPostStoreSaveMultiple(t *testing.T, ss store.Store) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p3 := model.Post{}
p3.ChannelId = model.NewId()
p3.UserId = model.NewId()
p3.Message = NewTestId()
p4 := model.Post{}
p4.ChannelId = model.NewId()
p4.UserId = model.NewId()
p4.Message = NewTestId()
t.Run("Save correctly a new set of posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
for _, post := range newPosts {
storedPost, err := ss.Post().GetSingle(post.Id, false)
assert.NoError(t, err)
assert.Equal(t, post.ChannelId, storedPost.ChannelId)
assert.Equal(t, post.Message, storedPost.Message)
assert.Equal(t, post.UserId, storedPost.UserId)
}
})
t.Run("Save replies", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestId()
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestId()
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestId()
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&o1, &o2, &o3, &o4})
require.NoError(t, err, "couldn't save item")
require.Equal(t, -1, errIdx)
assert.Len(t, newPosts, 4)
assert.Equal(t, int64(2), newPosts[0].ReplyCount)
assert.Equal(t, int64(2), newPosts[1].ReplyCount)
assert.Equal(t, int64(1), newPosts[2].ReplyCount)
assert.Equal(t, int64(0), newPosts[3].ReplyCount)
})
t.Run("Try to save mixed, already saved and not saved posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p4, &p3})
require.Error(t, err)
require.Equal(t, 1, errIdx)
require.Nil(t, newPosts)
storedPost, err := ss.Post().GetSingle(p3.Id, false)
assert.NoError(t, err)
assert.Equal(t, p3.ChannelId, storedPost.ChannelId)
assert.Equal(t, p3.Message, storedPost.Message)
assert.Equal(t, p3.UserId, storedPost.UserId)
storedPost, err = ss.Post().GetSingle(p4.Id, false)
assert.Error(t, err)
assert.Nil(t, storedPost)
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost})
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
replyPost2 := model.Post{}
replyPost2.ChannelId = rootPost.ChannelId
replyPost2.UserId = model.NewId()
replyPost2.Message = NewTestId()
replyPost2.RootId = rootPost.Id
replyPost3 := model.Post{}
replyPost3.ChannelId = rootPost.ChannelId
replyPost3.UserId = model.NewId()
replyPost3.Message = NewTestId()
replyPost3.RootId = rootPost.Id
// Ensure update does not occur in the same timestamp as creation
time.Sleep(time.Millisecond)
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err)
rrootPost2, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestId()
channel.DisplayName = NewTestId()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(&channel, 100)
require.NoError(t, err)
post1 := model.Post{}
post1.ChannelId = channel.Id
post1.UserId = model.NewId()
post1.Message = NewTestId()
post2 := model.Post{}
post2.ChannelId = channel.Id
post2.UserId = model.NewId()
post2.Message = NewTestId()
post2.CreateAt = 5
post3 := model.Post{}
post3.ChannelId = channel.Id
post3.UserId = model.NewId()
post3.Message = NewTestId()
_, _, err = ss.Post().SaveMultiple([]*model.Post{&post1, &post2, &post3})
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(3), rchannel.TotalMsgCount)
})
t.Run("Thread participants", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = "jessica hyde" + model.NewId() + "b"
root, err := ss.Post().Save(&o1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel5, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName5",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = root.Id
o2.Message = "zz" + model.NewId() + "b"
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = root.Id
o3.Message = "zz" + model.NewId() + "b"
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = o2.UserId
o4.RootId = root.Id
o4.Message = "zz" + model.NewId() + "b"
o5 := model.Post{}
o5.ChannelId = channel5.Id
o5.UserId = o1.UserId
o5.RootId = root.Id
o5.Message = "zz" + model.NewId() + "b"
_, err = ss.Post().Save(&o2)
require.NoError(t, err)
thread, errT := ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(1), thread.ReplyCount)
assert.Equal(t, int(1), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId}, thread.Participants)
_, err = ss.Post().Save(&o3)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(2), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId, o3.UserId}, thread.Participants)
_, err = ss.Post().Save(&o4)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(3), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId}, thread.Participants)
_, err = ss.Post().Save(&o5)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(4), thread.ReplyCount)
assert.Equal(t, int(3), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId, o1.UserId}, thread.Participants)
})
}
func testPostStoreSaveChannelMsgCounts(t *testing.T, ss store.Store) {
c1 := &model.Channel{Name: model.NewId(), DisplayName: "posttestchannel", Type: model.ChannelTypeOpen, TeamId: model.NewId()}
_, err := ss.Channel().Save(c1, 1000000)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should update by 1")
o1.Id = ""
o1.Type = model.PostTypeAddToTeam
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
o1.Id = ""
o1.Type = model.PostTypeRemoveFromTeam
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should not update for team add/removed message")
oldLastPostAt := c1.LastPostAt
o2 := model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = int64(7)
_, err = ss.Post().Save(&o2)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, oldLastPostAt, c1.LastPostAt, "LastPostAt should not update for old message save")
}
func testPostStoreGet(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
etag1 := ss.Post().GetEtag(o1.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
etag2 := ss.Post().GetEtag(o1.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag2, fmt.Sprintf("%v.%v", model.CurrentVersion, o1.UpdateAt)), "Invalid Etag")
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
_, err = ss.Post().Get(context.Background(), "123", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Missing id should have failed")
_, err = ss.Post().Get(context.Background(), "", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "should fail for blank post ids")
}
func testPostStoreGetForThread(t *testing.T, ss store.Store) {
t.Run("Post thread is followed", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.True(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post thread is explicitly not followed", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.False(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post threadmembership does not exist", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.Nil(t, r1.Posts[o1.Id].IsFollowing)
})
t.Run("Pagination", func(t *testing.T) {
t.Skip("MM-46134")
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1, err := ss.Post().Save(&model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
m1, err := ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.True(t, r1.HasNext)
lastPostID := r1.Order[len(r1.Order)-1]
lastPostCreateAt := r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt := r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "up",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, r1.HasNext)
// Non-CRT mode
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "down",
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.True(t, r1.HasNext)
lastPostID = r1.Order[len(r1.Order)-1]
lastPostCreateAt = r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 3,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 4) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt = r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "down",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, r1.HasNext)
})
}
func testPostStoreGetSingle(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = o1.UserId
o2.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = o1.UserId
o3.Message = model.NewRandomString(10)
o3.RootId = o1.Id
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = o1.UserId
o4.Message = model.NewRandomString(10)
o4.RootId = o1.Id
_, err = ss.Post().Save(o3)
require.NoError(t, err)
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
err = ss.Post().Delete(o2.Id, model.GetMillis(), o2.UserId)
require.NoError(t, err)
err = ss.Post().Delete(o4.Id, model.GetMillis(), o4.UserId)
require.NoError(t, err)
post, err := ss.Post().GetSingle(o1.Id, false)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o1.CreateAt, "invalid returned post")
require.Equal(t, int64(1), post.ReplyCount, "wrong replyCount computed")
_, err = ss.Post().GetSingle(o2.Id, false)
require.Error(t, err, "should not return deleted post")
post, err = ss.Post().GetSingle(o2.Id, true)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o2.CreateAt, "invalid returned post")
require.NotZero(t, post.DeleteAt, "DeleteAt should be non-zero")
require.Zero(t, post.ReplyCount, "Post without replies should return zero ReplyCount")
_, err = ss.Post().GetSingle("123", false)
require.Error(t, err, "Missing id should have failed")
}
func testPostStoreUpdate(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Update(o1a, ro1)
require.NoError(t, err)
r1, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
require.Equal(t, ro1a.Message, o1a.Message, "Failed to update/get")
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Update(o2a, ro2)
require.NoError(t, err)
r2, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
require.Equal(t, ro2a.Message, o2a.Message, "Failed to update/get")
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Update(o3a, ro3)
require.NoError(t, err)
r3, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
if ro3a.Message != o3a.Message {
require.Equal(t, ro3a.Hashtags, o3a.Hashtags, "Failed to update/get")
}
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Update(o4a, ro4)
require.NoError(t, err)
r4, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
}
func testPostStoreDelete(t *testing.T, ss store.Store) {
t.Run("single post, no replies", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a post
rootPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: model.NewRandomString(10),
})
require.NoError(t, err)
// Verify etag generation for the channel containing the post.
etag1 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
// Verify the created post.
r1, err := ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.NotNil(t, r1.Posts[rootPost.Id])
require.Equal(t, rootPost, r1.Posts[rootPost.Id])
// Mark the post as deleted by the user identified with deleteByID.
deleteByID := model.NewId()
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), deleteByID)
require.NoError(t, err)
// Ensure the appropriate posts prop reflects the user deleting the post.
posts, err := ss.Post().GetPostsCreatedAt(rootPost.ChannelId, rootPost.CreateAt)
require.NoError(t, err)
require.NotEmpty(t, posts)
assert.Equal(t, deleteByID, posts[0].GetProp(model.PostPropsDeleteBy), "unexpected Props[model.PostPropsDeleteBy]")
// Verify that the post is no longer fetched by default.
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "fetching deleted post should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify etag generation for the channel containing the now deleted post.
etag2 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag")
})
t.Run("thread with one reply", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost, err := ss.Post().Save(&model.Post{
ChannelId: rootPost.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost.Id,
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post deleted
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify the reply post deleted
_, err = ss.Post().Get(context.Background(), replyPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
})
t.Run("thread with multiple replies", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create another root post in a separate channel
rootPost2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rootPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post and replies deleted
_, err = ss.Post().Get(context.Background(), rootPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), replyPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), replyPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
// Verify other root posts remain undeleted.
_, err = ss.Post().Get(context.Background(), rootPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
})
t.Run("thread with multiple replies, update thread last reply at", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a third time
replyPost3, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
thread, err := ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the reply previous to last
err = ss.Post().Delete(replyPost2.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be unchanged
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(replyPost3.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should have changed
require.Equal(t, replyPost1.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(replyPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be 0
require.Equal(t, int64(0), thread.LastReplyAt)
})
}
func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel3.Id
o4.RootId = o1.Id
o4.UserId = o2.UserId
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = o3.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o3.ChannelId
o6.RootId = o5.Id
o6.UserId = model.NewId()
o6.Message = NewTestId()
o6, err = ss.Post().Save(o6)
require.NoError(t, err)
var thread *model.Thread
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 2, thread.ReplyCount)
require.EqualValues(t, model.StringArray{o2.UserId}, thread.Participants)
err2 := ss.Post().PermanentDeleteByUser(o2.UserId)
require.NoError(t, err2)
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 0, thread.ReplyCount)
require.EqualValues(t, model.StringArray{}, thread.Participants)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id shouldn't have failed")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.NotEmpty(t, thread)
err = ss.Post().PermanentDeleteByChannel(o3.ChannelId)
require.NoError(t, err)
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.Nil(t, thread)
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o6.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
}
func testPostStorePermDelete1Level2(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
err2 := ss.Post().PermanentDeleteByUser(o1.UserId)
require.NoError(t, err2)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id should have failed")
}
func testPostStoreGetWithChildren(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.RootId = o1.Id
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
pl, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 3, "invalid returned post")
dErr := ss.Post().Delete(o3.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 2, "invalid returned post")
dErr = ss.Post().Delete(o2.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 1, "invalid returned post")
}
func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
_, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = o1.ChannelId
o2a.UserId = model.NewId()
o2a.Message = NewTestId()
o2a.RootId = o1.Id
o2a, err = ss.Post().Save(o2a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.RootId = o1.Id
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o5 := &model.Post{}
o5.ChannelId = o1.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5.RootId = o4.Id
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
r1, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Order[0], o5.Id, "invalid order")
require.Equal(t, r1.Order[1], o4.Id, "invalid order")
require.Equal(t, r1.Order[2], o3.Id, "invalid order")
require.Equal(t, r1.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r1.Posts, 6, "wrong size")
require.Equal(t, r1.Posts[o1.Id].Message, o1.Message, "Missing parent")
r2, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r2.Order[0], o5.Id, "invalid order")
require.Equal(t, r2.Order[1], o4.Id, "invalid order")
require.Equal(t, r2.Order[2], o3.Id, "invalid order")
require.Equal(t, r2.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r2.Posts, 6, "wrong size")
require.Equal(t, r2.Posts[o1.Id].Message, o1.Message, "Missing parent")
// Run once to fill cache
_, err = ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o1.ChannelId
o6.UserId = model.NewId()
o6.Message = NewTestId()
_, err = ss.Post().Save(o6)
require.NoError(t, err)
r3, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, 7, len(r3.Order))
}
func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) {
t.Run("without threads", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
var posts []*model.Post
for i := 0; i < 10; i++ {
post, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
posts = append(posts, post)
time.Sleep(time.Millisecond)
}
t.Run("should return error if negative Page/PerPage options are passed", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: 0, PerPage: -1}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
postList, err = ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: -1, PerPage: 10}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
})
t.Run("should not return anything before the first post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id, posts[2].Id, posts[1].Id, posts[0].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[0].Id: posts[0],
posts[1].Id: posts[1],
posts[2].Id: posts[2],
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should limit posts before", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should not return anything after the last post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[len(posts)-1].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[9].Id, posts[8].Id, posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
posts[8].Id: posts[8],
posts[9].Id: posts[9],
}, postList.Posts)
})
t.Run("should limit posts after", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
}, postList.Posts)
})
})
t.Run("with threads", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
post1.ReplyCount = 1
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "message",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "message",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "message",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
post4.Id: post4,
post6.Id: post6,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post4.Id: post4,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (skipFetchThreads)", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and thread before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 1, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (collapsedThreads)", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each root post before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id, post1.Id}, postList.Order)
})
t.Run("should return each root post before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 1, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id}, postList.Order)
})
t.Run("should return each root after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post5.Id}, postList.Order)
})
})
}
func testPostStoreGetPostsSince(t *testing.T, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post3.CreateAt}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 5)
assert.NotNil(t, postList.Posts[post1.Id], "should return the parent post")
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return empty list when nothing has changed", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Empty(t, postList.Posts)
})
t.Run("should not cache a timestamp of 0 when nothing has changed", func(t *testing.T) {
ss.Post().ClearCaches()
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
// Make a request that returns no results
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt}, true, map[string]bool{})
require.NoError(t, err)
require.Equal(t, model.NewPostList(), postList)
// And then ensure that it doesn't cause future requests to also return no results
postList, err = ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt - 1}, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post1.Id}, postList.Order)
assert.Len(t, postList.Posts, 1)
assert.NotNil(t, postList.Posts[post1.Id])
})
}
func testPostStoreGetPosts(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
t.Run("should return the last posts created in a channel", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return the last posts created in a channel and the threads and the reply count must be 0", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
require.NotNil(t, postList.Posts[post1.Id])
require.NotNil(t, postList.Posts[post3.Id])
require.NotNil(t, postList.Posts[post5.Id])
require.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(0), postList.Posts[post1.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post3.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the last posts created in a channel without the threads and the reply count must be correct", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return all posts in a channel included deleted posts", func(t *testing.T) {
err := ss.Post().Delete(post1.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: false, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return all posts in a channel included deleted posts without threads", func(t *testing.T) {
err := ss.Post().Delete(post5.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the lasts posts created in channel without include deleted posts", func(t *testing.T) {
err := ss.Post().Delete(post6.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: false}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post4.Id,
post3.Id,
post2.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 3)
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
})
}
func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
o0 := &model.Post{}
o0.ChannelId = channelId
o0.UserId = model.NewId()
o0.Message = NewTestId()
_, err = ss.Post().Save(o0)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o1 := &model.Post{}
o1.ChannelId = channelId
o1.Type = model.PostTypeJoinChannel
o1.UserId = model.NewId()
o1.Message = "system_join_channel message"
_, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0a := &model.Post{}
o0a.ChannelId = channelId
o0a.UserId = model.NewId()
o0a.Message = NewTestId()
o0a.RootId = o1.Id
_, err = ss.Post().Save(o0a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0b := &model.Post{}
o0b.ChannelId = channelId
o0b.UserId = model.NewId()
o0b.Message = "deleted message"
o0b.RootId = o1.Id
o0b.DeleteAt = 1
_, err = ss.Post().Save(o0b)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
otherChannelPost := &model.Post{}
otherChannelPost.ChannelId = channel2.Id
otherChannelPost.UserId = model.NewId()
otherChannelPost.Message = NewTestId()
_, err = ss.Post().Save(otherChannelPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = channelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
_, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = channelId
o2a.UserId = model.NewId()
o2a.Message = NewTestId()
o2a.RootId = o2.Id
_, err = ss.Post().Save(o2a)
require.NoError(t, err)
rPostId1, err := ss.Post().GetPostIdBeforeTime(channelId, o0a.CreateAt, false)
require.Equal(t, rPostId1, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostId1, err = ss.Post().GetPostIdAfterTime(channelId, o0b.CreateAt, false)
require.Equal(t, rPostId1, o2.Id, "should return before post o2")
require.NoError(t, err)
rPost1, err := ss.Post().GetPostAfterTime(channelId, o0b.CreateAt, false)
require.Equal(t, rPost1.Id, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostId2, err := ss.Post().GetPostIdBeforeTime(channelId, o0.CreateAt, false)
require.Empty(t, rPostId2, "should return no post")
require.NoError(t, err)
rPostId2, err = ss.Post().GetPostIdAfterTime(channelId, o0.CreateAt, false)
require.Equal(t, rPostId2, o1.Id, "should return before post o1")
require.NoError(t, err)
rPost2, err := ss.Post().GetPostAfterTime(channelId, o0.CreateAt, false)
require.Equal(t, rPost2.Id, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostId3, err := ss.Post().GetPostIdBeforeTime(channelId, o2a.CreateAt, false)
require.Equal(t, rPostId3, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostId3, err = ss.Post().GetPostIdAfterTime(channelId, o2a.CreateAt, false)
require.Empty(t, rPostId3, "should return no post")
require.NoError(t, err)
rPost3, err := ss.Post().GetPostAfterTime(channelId, o2a.CreateAt, false)
require.Empty(t, rPost3.Id, "should return no post")
require.NoError(t, err)
}
func testUserCountsWithPostsByDay(t *testing.T, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2.Message = NewTestId()
o2, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2a.Message = NewTestId()
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
r1, err := ss.Post().AnalyticsUserCountsWithPostsByDay(t1.Id)
require.NoError(t, err)
row1 := r1[0]
require.Equal(t, float64(2), row1.Value, "wrong value")
row2 := r1[1]
require.Equal(t, float64(1), row2.Value, "wrong value")
}
func testPostCountsByDay(t *testing.T, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestId()
o1.Hashtags = "hashtag"
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestId()
o1a.FileIds = []string{"fileId1"}
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2.Message = NewTestId()
o2.Filenames = []string{"filename1"}
o2, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2a.Message = NewTestId()
o2a.Hashtags = "hashtag"
o2a.FileIds = []string{"fileId2"}
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
b1 := &model.Post{}
b1.Message = "bot message one"
b1.ChannelId = c1.Id
b1.UserId = bot1.UserId
b1.CreateAt = utils.MillisFromTime(utils.Yesterday())
_, nErr = ss.Post().Save(b1)
require.NoError(t, nErr)
b1a := &model.Post{}
b1a.Message = "bot message two"
b1a.ChannelId = c1.Id
b1a.UserId = bot1.UserId
b1a.CreateAt = utils.MillisFromTime(utils.Yesterday()) - (1000 * 60 * 60 * 24 * 2)
_, nErr = ss.Post().Save(b1a)
require.NoError(t, nErr)
time.Sleep(1 * time.Second)
// summary of posts
// yesterday - 2 non-bot user posts, 1 bot user post
// 3 days ago - 2 non-bot user posts, 1 bot user post
// last 31 days, all users (including bots)
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: false}
r1, err := ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
assert.Equal(t, float64(3), r1[1].Value)
// last 31 days, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: false}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
assert.Equal(t, float64(1), r1[1].Value)
// yesterday only, all users (including bots)
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
// yesterday only, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
}
func testPostCounts(t *testing.T, ss store.Store) {
now := time.Now()
twentyMinAgo := now.Add(-20 * time.Minute).UnixMilli()
fifteenMinAgo := now.Add(-15 * time.Minute).UnixMilli()
tenMinAgo := now.Add(-10 * time.Minute).UnixMilli()
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
// system post
p1 := &model.Post{}
p1.Type = "system_add_to_channel"
p1.ChannelId = c1.Id
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = twentyMinAgo
p1.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Hashtags = "hashtag"
p2.CreateAt = twentyMinAgo
p2.UpdateAt = twentyMinAgo
p2, nErr = ss.Post().Save(p2)
require.NoError(t, nErr)
p3 := &model.Post{}
p3.ChannelId = c1.Id
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.FileIds = []string{"fileId1"}
p3.CreateAt = twentyMinAgo
p3.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p3)
require.NoError(t, nErr)
p4 := &model.Post{}
p4.ChannelId = c1.Id
p4.UserId = model.NewId()
p4.Message = NewTestId()
p4.Filenames = []string{"filename1"}
p4.CreateAt = tenMinAgo
p4.UpdateAt = tenMinAgo
p4, nErr = ss.Post().Save(p4)
require.NoError(t, nErr)
p5 := &model.Post{}
p5.ChannelId = c1.Id
p5.UserId = p4.UserId
p5.Message = NewTestId()
p5.Hashtags = "hashtag"
p5.FileIds = []string{"fileId2"}
p5.CreateAt = tenMinAgo
p5.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(p5)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
p6 := &model.Post{}
p6.Message = "bot message one"
p6.ChannelId = c1.Id
p6.UserId = bot1.UserId
p6.CreateAt = twentyMinAgo
p6.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p6)
require.NoError(t, nErr)
p7 := &model.Post{}
p7.Message = "bot message two"
p7.ChannelId = c1.Id
p7.UserId = bot1.UserId
p7.CreateAt = tenMinAgo
p7.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(p7)
require.NoError(t, nErr)
// total across all teams
c, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
assert.GreaterOrEqual(t, c, int64(7))
// total for single team
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id})
require.NoError(t, err)
assert.Equal(t, int64(7), c)
// with files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// with hashtags
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// with hashtags and files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(1), c)
// excluding system posts
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeSystemPosts: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// before update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: fifteenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// equal to update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// since update_at and since post id
tenMinAgoIDs := []string{p4.Id, p5.Id, p7.Id}
sort.Strings(tenMinAgoIDs)
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo, SincePostID: tenMinAgoIDs[0]})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// delete 1 post
err = ss.Post().Delete(p2.Id, 1, p2.UserId)
require.NoError(t, err)
// total for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// total users only posts for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
}
func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStore) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = c1.Id
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m0)
require.NoError(t, err)
teamId := model.NewId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
c2 := &model.Channel{}
c2.DisplayName = "DMChannel1"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeDirect
m1 := &model.ChannelMember{}
m1.ChannelId = c2.Id
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := &model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
c2, err = ss.Channel().SaveDirectChannel(c2, m1, m2)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = c2.Id
o5.UserId = m2.UserId
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = channel3.Id
o6.UserId = m2.UserId
o6.Message = NewTestId()
o6, err = ss.Post().Save(o6)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
r1, err := ss.Post().GetFlaggedPosts(o1.ChannelId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r2, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r3, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, model.NewId(), 0, 2)
require.NoError(t, err)
require.Empty(t, r4.Order, "should have 0 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o5.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o6.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetFlaggedPosts(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
teamId := model.NewId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = o1.ChannelId
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m0)
require.NoError(t, err)
r1, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
nErr := ss.Preference().Save(preferences)
require.NoError(t, nErr)
r2, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r3, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
}
func testPostStoreGetFlaggedPostsForChannel(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, err = ss.Channel().Save(c2, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// deleted post
teamId := model.NewId()
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = o1.ChannelId
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = c2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = channel4.Id
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m1 := &model.ChannelMember{}
m1.ChannelId = o1.ChannelId
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m1)
require.NoError(t, err)
m2 := &model.ChannelMember{}
m2.ChannelId = o4.ChannelId
m2.UserId = o1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
r, err := ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
preference := model.Preference{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
}
nErr := ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
preference.Name = o2.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
preference.Name = o3.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 2, "should have 2 posts")
preference.Name = o4.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o4.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 posts")
preference.Name = o5.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o5.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 0, "should have 0 posts")
}
func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
createTime := model.GetMillis() + 1
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestId()
o0.CreateAt = createTime
o0, err = ss.Post().Save(o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.ChannelId
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = createTime
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2.CreateAt = createTime + 1
_, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.CreateAt = createTime
_, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, _ := ss.Post().GetPostsCreatedAt(o1.ChannelId, createTime)
assert.Equal(t, 2, len(r1))
}
func testPostStoreOverwriteMultiple(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5, err := ss.Post().Save(&model.Post{
ChannelId: channel3.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test2", "test3"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
r5, err := ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro5 := r5.Posts[o5.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
require.Equal(t, ro4.Filenames, o4.Filenames, "Failed to save/get")
require.Equal(t, ro5.Message, o5.Message, "Failed to save/get")
require.Equal(t, ro5.Filenames, o5.Filenames, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o1a, o2a, o3a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r1, nErr := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro1a := r1.Posts[o1.Id]
r2, nErr = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro2a := r2.Posts[o2.Id]
r3, nErr = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
o5a := ro5.Clone()
o5a.Filenames = []string{}
o5a.FileIds = []string{}
_, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o4a, o5a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r4, nErr := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro4a := r4.Posts[o4.Id]
r5, nErr = ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro5a := r5.Posts[o5.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
require.Empty(t, ro5a.Filenames, "Failed to clear Filenames")
require.Empty(t, ro5a.FileIds, "Failed to set FileIds")
})
}
func testPostStoreOverwrite(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Overwrite(o1a)
require.NoError(t, err)
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Overwrite(o2a)
require.NoError(t, err)
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Overwrite(o3a)
require.NoError(t, err)
r1, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
r2, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
r3, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Overwrite(o4a)
require.NoError(t, err)
r4, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
})
}
func testPostStoreGetPostsByIds(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
postIds := []string{
ro1.Id,
ro2.Id,
ro3.Id,
}
posts, err := ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
err = ss.Post().Delete(ro1.Id, model.GetMillis(), "")
require.NoError(t, err)
posts, err = ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
}
func testPostStoreGetPostsBatchForIndexing(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err := ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = c2.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
_, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = c1.Id
o3.UserId = model.NewId()
o3.RootId = o1.Id
o3.Message = NewTestId()
_, err = ss.Post().Save(o3)
require.NoError(t, err)
// Getting all
r, err := ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 100)
require.NoError(t, err)
require.Len(t, r, 3, "Expected 3 posts in results. Got %v", len(r))
// Testing pagination
r, err = ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 0, "Expected 0 post in results. Got %v", len(r))
}
func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = 1000
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = channel.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = 1000
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.CreateAt = 100000
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Should have found post 3 after purge")
t.Run("with pagination", func(t *testing.T) {
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err)
}
cursor := model.RetentionPolicyCursor{}
deleted, cursor, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(2), deleted)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(1), deleted)
})
t.Run("with data retention policies", func(t *testing.T) {
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err2)
post := &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
}
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "global policy should have been ignored due to granular policy")
nowMillis := post.CreateAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by channel policy")
// Create a team policy which is stricter than the channel policy
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
post.Id = ""
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
nowMillis = post.CreateAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err2 = ss.RetentionPolicy().RemoveChannels(channelPolicy.ID, []string{channel.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by team policy")
err2 = ss.RetentionPolicy().RemoveTeams(teamPolicy.ID, []string{team.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err2)
})
t.Run("with channel, team and global policies", func(t *testing.T) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(channelPolicy.ID)
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(teamPolicy.ID)
// This one should be deleted by the channel policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c1.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the team policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the global policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c2.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
nowMillis := int64(1 + 30*model.DayInMilliseconds + 1)
deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
require.Equal(t, int64(3), deleted)
})
}
func testPostStoreGetOldest(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestId()
o0.CreateAt = 3
o0, err = ss.Post().Save(o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = 2
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = 1
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
r1, err := ss.Post().GetOldest()
require.NoError(t, err)
assert.EqualValues(t, o2.Id, r1.Id)
}
func testGetMaxPostSize(t *testing.T, ss store.Store) {
assert.Equal(t, model.PostMessageMaxRunesV2, ss.Post().GetMaxPostSize())
assert.Equal(t, model.PostMessageMaxRunesV2, ss.Post().GetMaxPostSize())
}
func testPostStoreGetParentsForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, p := range posts {
if p.Id == p1.Id {
found = true
assert.Equal(t, p.Id, p1.Id)
assert.Equal(t, p.Message, p1.Message)
assert.Equal(t, p.Username, u1.Username)
assert.Equal(t, p.TeamName, t1.Name)
assert.Equal(t, p.ChannelName, c1.Name)
}
}
assert.True(t, found)
}
func testPostStoreGetRepliesForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = u1.Id
p2.Message = NewTestId()
p2.CreateAt = 1001
p2.RootId = p1.Id
p2, nErr = ss.Post().Save(p2)
require.NoError(t, nErr)
r1, err := ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
reply1 := r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
// Checking whether replies by deleted user are exported
u1.DeleteAt = 1002
_, err = ss.User().Update(&u1, false)
require.NoError(t, err)
r1, err = ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
reply1 = r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
}
func testPostStoreGetDirectPostParentsForExportAfter(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, p1.Message, r1[0].Message)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterDeleted(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.DeleteAt = 1
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.DeleteAt = 1
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
o1.DeleteAt = 1
nErr = ss.Channel().SetDeleteAt(o1.Id, 1, 1)
assert.NoError(t, nErr)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
o1a := p1.Clone()
o1a.DeleteAt = 1
o1a.Message = p1.Message + "BBBBBBBBBB"
_, nErr = ss.Post().Update(o1a, p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, 0, len(r1))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterBatched(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
var postIds []string
for i := 0; i < 150; i++ {
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
postIds = append(postIds, p1.Id)
}
sort.Slice(postIds, func(i, j int) bool { return postIds[i] < postIds[j] })
// Get all posts
r1, err := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
assert.Equal(t, len(postIds), len(r1))
var exportedPostIds []string
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] })
assert.ElementsMatch(t, postIds, exportedPostIds)
// Get 100
r1, err = ss.Post().GetDirectPostParentsForExportAfter(100, strings.Repeat("0", 26))
assert.NoError(t, err)
assert.Equal(t, 100, len(r1))
exportedPostIds = []string{}
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] })
assert.ElementsMatch(t, postIds[:100], exportedPostIds)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testHasAutoResponsePostByUserSince(t *testing.T, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
_, err = ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
// We need to sleep because SendAutoResponseIfNecessary
// runs in a goroutine.
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "auto response message",
Type: model.PostTypeAutoResponder,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
exists, err := ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post2.CreateAt}, userId)
require.NoError(t, err)
assert.True(t, exists)
err = ss.Post().Delete(post3.Id, time.Now().Unix(), userId)
require.NoError(t, err)
exists, err = ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post2.CreateAt}, userId)
require.NoError(t, err)
assert.False(t, exists)
})
}
func testGetPostsSinceForSync(t *testing.T, ss store.Store, s SqlStore) {
// create some posts.
channelID := model.NewId()
remoteID := model.NewString(model.NewId())
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 0"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 1"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 2"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 3", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 4", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 5", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 6", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 7"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 8", DeleteAt: model.GetMillis()},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 9", DeleteAt: model.GetMillis()},
}
for i, p := range data {
p.UpdateAt = first + (int64(i) * 300000)
if p.RemoteId == nil {
p.RemoteId = model.NewString(model.NewId())
}
_, err := ss.Post().Save(p)
require.NoError(t, err, "couldn't save post")
}
t.Run("Invalid channel id", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: model.NewId(),
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, cursorOut, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Empty(t, posts, "should return zero posts")
require.Equal(t, cursor, cursorOut)
})
t.Run("Get by channel, exclude remotes, exclude deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeRemoteId: *remoteID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data[0:3], data[7]), getPostIds(posts))
})
t.Run("Include deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
IncludeDeleted: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data), getPostIds(posts))
})
t.Run("Limit and cursor", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
t.Run("UpdateAt collisions", func(t *testing.T) {
// this test requires all the UpdateAt timestamps to be the same.
result, err := s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ?", model.GetMillis())
require.NoError(t, err)
rows, err := result.RowsAffected()
require.NoError(t, err)
require.Greater(t, rows, int64(0))
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
}
func testSetPostReminder(t *testing.T, ss store.Store, s SqlStore) {
// Basic
userID := NewTestId()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestId(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: 1234,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
out := model.PostReminder{}
require.NoError(t, s.GetMasterX().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
reminder.PostId = "notfound"
err = ss.Post().SetPostReminder(reminder)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Upsert
reminder = &model.PostReminder{
TargetTime: 12345,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
require.NoError(t, s.GetMasterX().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
}
func testGetPostReminders(t *testing.T, ss store.Store, s SqlStore) {
times := []int64{100, 101, 102}
for _, tt := range times {
userID := NewTestId()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestId(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: tt,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
}
reminders, err := ss.Post().GetPostReminders(102)
require.NoError(t, err)
require.Len(t, reminders, 2)
// assert one reminder is left
reminders, err = ss.Post().GetPostReminders(103)
require.NoError(t, err)
require.Len(t, reminders, 1)
// assert everything is deleted.
reminders, err = ss.Post().GetPostReminders(103)
require.NoError(t, err)
require.Len(t, reminders, 0)
}
func testGetPostReminderMetadata(t *testing.T, ss store.Store, s SqlStore) {
team := &model.Team{
Name: "teamname",
DisplayName: "display",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
ch := &model.Channel{
TeamId: team.Id,
DisplayName: "channeldisplay",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
ch, err = ss.Channel().Save(ch, -1)
require.NoError(t, err)
ch2 := &model.Channel{
TeamId: "",
DisplayName: "GM_display",
Name: NewTestId(),
Type: model.ChannelTypeGroup,
}
ch2, err = ss.Channel().Save(ch2, -1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
Locale: "es",
}
u1, err = ss.User().Save(u1)
require.NoError(t, err)
p1 := &model.Post{
UserId: u1.Id,
ChannelId: ch.Id,
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err = ss.Post().Save(p1)
require.NoError(t, err)
p2 := &model.Post{
UserId: u1.Id,
ChannelId: ch2.Id,
Message: "hi there 2",
Type: model.PostTypeDefault,
}
p2, err = ss.Post().Save(p2)
require.NoError(t, err)
meta, err := ss.Post().GetPostReminderMetadata(p1.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelId, ch.Id)
assert.Equal(t, meta.TeamName, team.Name)
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
meta, err = ss.Post().GetPostReminderMetadata(p2.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelId, ch2.Id)
assert.Equal(t, meta.TeamName, "")
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
}
func getPostIds(posts []*model.Post, morePosts ...*model.Post) []string {
ids := make([]string, 0, len(posts)+len(morePosts))
for _, p := range posts {
ids = append(ids, p.Id)
}
for _, p := range morePosts {
ids = append(ids, p.Id)
}
return ids
}
func testGetNthRecentPostTime(t *testing.T, ss store.Store) {
_, err := ss.Post().GetNthRecentPostTime(0)
assert.Error(t, err)
_, err = ss.Post().GetNthRecentPostTime(-1)
assert.Error(t, err)
diff := int64(10000)
now := utils.MillisFromTime(time.Now()) + diff
p1 := &model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = "test"
p1.CreateAt = now
p1, err = ss.Post().Save(p1)
require.NoError(t, err)
p2 := &model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = p1.UserId
p2.Message = p1.Message
now = now + diff
p2.CreateAt = now
p2, err = ss.Post().Save(p2)
require.NoError(t, err)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, err = ss.Bot().Save(bot1)
require.NoError(t, err)
b1 := &model.Post{}
b1.Message = "bot test"
b1.ChannelId = p1.ChannelId
b1.UserId = bot1.UserId
now = now + diff
b1.CreateAt = now
_, err = ss.Post().Save(b1)
require.NoError(t, err)
p3 := &model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = p1.UserId
p3.Message = p1.Message
now = now + diff
p3.CreateAt = now
p3, err = ss.Post().Save(p3)
require.NoError(t, err)
s1 := &model.Post{}
s1.Type = model.PostTypeJoinChannel
s1.ChannelId = p1.ChannelId
s1.UserId = model.NewId()
s1.Message = "system_join_channel message"
now = now + diff
s1.CreateAt = now
_, err = ss.Post().Save(s1)
require.NoError(t, err)
p4 := &model.Post{}
p4.ChannelId = p1.ChannelId
p4.UserId = p1.UserId
p4.Message = p1.Message
now = now + diff
p4.CreateAt = now
p4, err = ss.Post().Save(p4)
require.NoError(t, err)
r, err := ss.Post().GetNthRecentPostTime(1)
assert.NoError(t, err)
assert.Equal(t, p4.CreateAt, r)
// Skip system post
r, err = ss.Post().GetNthRecentPostTime(2)
assert.NoError(t, err)
assert.Equal(t, p3.CreateAt, r)
// Skip system & bot post
r, err = ss.Post().GetNthRecentPostTime(3)
assert.NoError(t, err)
assert.Equal(t, p2.CreateAt, r)
r, err = ss.Post().GetNthRecentPostTime(4)
assert.NoError(t, err)
assert.Equal(t, p1.CreateAt, r)
_, err = ss.Post().GetNthRecentPostTime(10000)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
}
func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) {
// users
user := model.User{Email: MakeEmail(), Username: model.NewId()}
u1 := model.User{Email: MakeEmail(), Username: model.NewId()}
u2 := model.User{Email: MakeEmail(), Username: model.NewId()}
u3 := model.User{Email: MakeEmail(), Username: model.NewId()}
u4 := model.User{Email: MakeEmail(), Username: model.NewId()}
u5 := model.User{Email: MakeEmail(), Username: model.NewId()}
_, err := ss.User().Save(&user)
require.NoError(t, err)
_, err = ss.User().Save(&u1)
require.NoError(t, err)
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, err = ss.User().Save(&u4)
require.NoError(t, err)
_, err = ss.User().Save(&u5)
require.NoError(t, err)
bot := &model.Bot{
Username: "bot_user",
Description: "bot",
OwnerId: model.NewId(),
UserId: u5.Id,
}
savedBot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// user direct messages
chUser1, nErr := ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
chUser2, nErr := ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
chUser3, nErr := ss.Channel().CreateDirectChannel(&u3, &user)
require.NoError(t, nErr)
// other user direct message
chUser3User4, nErr := ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
// bot direct message - should be ignored by top DMs
botUser, err := ss.User().Get(context.Background(), savedBot.UserId)
require.NoError(t, err)
chBot, nErr := ss.Channel().CreateDirectChannel(&user, botUser)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chBot.Id,
UserId: botUser.Id,
})
require.NoError(t, err)
// sample post data
// for u1
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
})
require.NoError(t, err)
// for u2: 1 post
postToDelete, err := ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
// create second post for u2: modify create at to a very old date to make sure it isn't counted
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
CreateAt: 100,
})
require.NoError(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
})
require.NoError(t, err)
}
// for u4-u3: 4 posts
u3u4Post1, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
u3u4Post2, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[0].OutgoingMessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[1].OutgoingMessageCount, int64(1))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
require.Equal(t, topDMs.Items[2].OutgoingMessageCount, int64(0))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
t.Run("topDMs will not consider self dms", func(t *testing.T) {
chUser, nErr := ss.Channel().CreateDirectChannel(&user, &user)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser.Id,
UserId: user.Id,
})
// delete u2 post
err := ss.Post().Delete(postToDelete.Id, 200, user.Id)
require.NoError(t, err)
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 2)
})
t.Run("topDMs will not consider deleted second user", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 1
require.Len(t, topDMs.Items, 1)
// delete user3
err = ss.User().PermanentDelete(u3.Id)
require.NoError(t, err)
// delete user3 posts
err = ss.Post().Delete(u3u4Post1.Id, 200, u3.Id)
require.NoError(t, err)
err = ss.Post().Delete(u3u4Post2.Id, 200, u3.Id)
require.NoError(t, err)
// delete channel memberships
err = ss.Channel().PermanentDeleteMembersByUser(u3.Id)
require.NoError(t, err)
topDMs, err = ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 0 since u3 is deleted
require.Len(t, topDMs.Items, 0)
})
}
func testGetEditHistoryForPost(t *testing.T, ss store.Store) {
t.Run("should return edit history for post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost)
require.NoError(t, err)
// get edit history
edits, err := ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
require.Len(t, edits, 1)
require.Equal(t, originalPost.Id, edits[0].Id)
require.Equal(t, originalPost.UserId, edits[0].UserId)
require.Equal(t, originalPost.Message, edits[0].Message)
})
t.Run("should return error for not edited posts", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for non-existent post", func(t *testing.T) {
// get edit history
_, err := ss.Post().GetEditHistoryForPost("non-existent")
require.Error(t, err)
})
t.Run("should return error for deleted post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// delete post
err = ss.Post().Delete(post.Id, 100, post.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for deleted edit", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost)
require.NoError(t, err)
// delete edit
err = ss.Post().Delete(savedUpdatedPost.Id, 100, savedUpdatedPost.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPreferenceStore(t *testing.T, ss store.Store) {
t.Run("PreferenceSave", func(t *testing.T) { testPreferenceSave(t, ss) })
t.Run("PreferenceGet", func(t *testing.T) { testPreferenceGet(t, ss) })
t.Run("PreferenceGetCategory", func(t *testing.T) { testPreferenceGetCategory(t, ss) })
t.Run("PreferenceGetAll", func(t *testing.T) { testPreferenceGetAll(t, ss) })
t.Run("PreferenceDeleteByUser", func(t *testing.T) { testPreferenceDeleteByUser(t, ss) })
t.Run("PreferenceDelete", func(t *testing.T) { testPreferenceDelete(t, ss) })
t.Run("PreferenceDeleteCategory", func(t *testing.T) { testPreferenceDeleteCategory(t, ss) })
t.Run("PreferenceDeleteCategoryAndName", func(t *testing.T) { testPreferenceDeleteCategoryAndName(t, ss) })
t.Run("PreferenceDeleteOrphanedRows", func(t *testing.T) { testPreferenceDeleteOrphanedRows(t, ss) })
}
func testPreferenceSave(t *testing.T, ss store.Store) {
id := model.NewId()
preferences := model.Preferences{
{
UserId: id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1a",
},
{
UserId: id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1b",
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err, "saving preference returned error")
for _, preference := range preferences {
data, _ := ss.Preference().Get(preference.UserId, preference.Category, preference.Name)
require.Equal(t, data, &preference, "got incorrect preference after first Save")
}
preferences[0].Value = "value2a"
preferences[1].Value = "value2b"
err = ss.Preference().Save(preferences)
require.NoError(t, err, "saving preference returned error")
for _, preference := range preferences {
data, _ := ss.Preference().Get(preference.UserId, preference.Category, preference.Name)
require.Equal(t, data, &preference, "got incorrect preference after second Save")
}
}
func testPreferenceGet(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
data, err := ss.Preference().Get(userId, category, name)
require.NoError(t, err)
require.Equal(t, &preferences[0], data, "got incorrect preference")
// make sure getting a missing preference fails
_, err = ss.Preference().Get(model.NewId(), model.NewId(), model.NewId())
require.Error(t, err, "no error on getting a missing preference")
}
func testPreferenceGetCategory(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
preferencesByCategory, err := ss.Preference().GetCategory(userId, category)
require.NoError(t, err)
require.Equal(t, 2, len(preferencesByCategory), "got the wrong number of preferences")
require.True(
t,
((preferencesByCategory[0] == preferences[0] && preferencesByCategory[1] == preferences[1]) || (preferencesByCategory[0] == preferences[1] && preferencesByCategory[1] == preferences[0])),
"got incorrect preferences",
)
// make sure getting a missing preference category doesn't fail
preferencesByCategory, err = ss.Preference().GetCategory(model.NewId(), model.NewId())
require.NoError(t, err)
require.Equal(t, 0, len(preferencesByCategory), "shouldn't have got any preferences")
}
func testPreferenceGetAll(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
result, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
require.Equal(t, 3, len(result), "got the wrong number of preferences")
for i := 0; i < 3; i++ {
assert.Falsef(t, result[0] != preferences[i] && result[1] != preferences[i] && result[2] != preferences[i], "got incorrect preferences")
}
}
func testPreferenceDeleteByUser(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
err = ss.Preference().PermanentDeleteByUser(userId)
require.NoError(t, err)
}
func testPreferenceDelete(t *testing.T, ss store.Store) {
preference := model.Preference{
UserId: model.NewId(),
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(preference.UserId)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
err = ss.Preference().Delete(preference.UserId, preference.Category, preference.Name)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(preference.UserId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preferences")
}
func testPreferenceDeleteCategory(t *testing.T, ss store.Store) {
category := model.NewId()
userId := model.NewId()
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Len(t, preferences, 2, "should've returned 2 preferences")
err = ss.Preference().DeleteCategory(userId, category)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preferences")
}
func testPreferenceDeleteCategoryAndName(t *testing.T, ss store.Store) {
category := model.NewId()
name := model.NewId()
userId := model.NewId()
userId2 := model.NewId()
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: name,
Value: "value1a",
}
preference2 := model.Preference{
UserId: userId2,
Category: category,
Name: name,
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
preferences, err = ss.Preference().GetAll(userId2)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
err = ss.Preference().DeleteCategoryAndName(category, name)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preference")
preferences, err = ss.Preference().GetAll(userId2)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preference")
}
func testPreferenceDeleteOrphanedRows(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
category := model.PreferenceCategoryFlaggedPost
userId := model.NewId()
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 3000,
})
require.NoError(t, err)
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: olderPost.Id,
Value: "true",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: newerPost.Id,
Value: "true",
}
nErr := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, nErr)
_, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
assert.NoError(t, nErr)
_, nErr = ss.Preference().DeleteOrphanedRows(limit)
assert.NoError(t, nErr)
_, nErr = ss.Preference().Get(userId, category, preference1.Name)
assert.Error(t, nErr, "older preference should have been deleted")
_, nErr = ss.Preference().Get(userId, category, preference2.Name)
assert.NoError(t, nErr, "newer preference should not have been deleted")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestProductNoticesStore(t *testing.T, ss store.Store) {
t.Run("TestAddViewed", func(t *testing.T) { testAddViewed(t, ss) })
t.Run("TestUpdateViewed", func(t *testing.T) { testUpdateViewed(t, ss) })
t.Run("TestClearOld", func(t *testing.T) { testClearOld(t, ss) })
}
func testAddViewed(t *testing.T, ss store.Store) {
notices := []string{"noticeA", "noticeB"}
defer ss.ProductNotices().Clear(notices)
err := ss.ProductNotices().View("testuser", notices)
require.NoError(t, err)
err = ss.ProductNotices().View("testuser2", notices)
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 2)
}
func testUpdateViewed(t *testing.T, ss store.Store) {
noticesA := []string{"noticeA", "noticeB"}
noticesB := []string{"noticeB", "noticeC"}
defer ss.ProductNotices().Clear(noticesA)
defer ss.ProductNotices().Clear(noticesB)
// mark two notices
err := ss.ProductNotices().View("testuser", noticesA)
require.NoError(t, err)
// mark one old and one new
err = ss.ProductNotices().View("testuser", noticesB)
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 3)
// make sure that one B has two views
require.Equal(t, res[0].Viewed, int32(1))
require.Equal(t, res[1].Viewed, int32(2))
require.Equal(t, res[2].Viewed, int32(1))
// make sure that B's timestamp was updated
require.GreaterOrEqual(t, res[1].Timestamp, res[0].Timestamp)
}
func testClearOld(t *testing.T, ss store.Store) {
noticesA := []string{"noticeA", "noticeB"}
defer ss.ProductNotices().Clear(noticesA)
// mark two notices
err := ss.ProductNotices().View("testuser", noticesA)
require.NoError(t, err)
err = ss.ProductNotices().ClearOldNotices(model.ProductNotices{
{
ID: "noticeA",
},
{
ID: "noticeC",
},
})
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/retrylayer"
)
func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ReactionSave", func(t *testing.T) { testReactionSave(t, ss) })
t.Run("ReactionDelete", func(t *testing.T) { testReactionDelete(t, ss) })
t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) })
t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) })
t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) })
t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) })
t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) })
}
func testReactionSave(t *testing.T, ss store.Store) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
firstUpdateAt := post.UpdateAt
reaction1 := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
time.Sleep(time.Millisecond)
reaction, nErr := ss.Reaction().Save(reaction1)
require.NoError(t, nErr)
saved := reaction
assert.Equal(t, saved.UserId, reaction1.UserId, "should've saved reaction user_id and returned it")
assert.Equal(t, saved.PostId, reaction1.PostId, "should've saved reaction post_id and returned it")
assert.Equal(t, saved.EmojiName, reaction1.EmojiName, "should've saved reaction emoji_name and returned it")
assert.NotZero(t, saved.UpdateAt, "should've saved reaction update_at and returned it")
assert.Equal(t, saved.ChannelId, post.ChannelId, "should've saved reaction update_at and returned it")
assert.Zero(t, saved.DeleteAt, "should've saved reaction delete_at with zero value and returned it")
var secondUpdateAt int64
postList, err := ss.Post().Get(context.Background(), reaction1.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = true on post")
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should've marked post as updated when HasReactions changed")
if postList.Posts[post.Id].HasReactions && postList.Posts[post.Id].UpdateAt != firstUpdateAt {
secondUpdateAt = postList.Posts[post.Id].UpdateAt
}
_, nErr = ss.Reaction().Save(reaction1)
assert.NoError(t, nErr, "should've allowed saving a duplicate reaction")
// different user
reaction2 := &model.Reaction{
UserId: model.NewId(),
PostId: reaction1.PostId,
EmojiName: reaction1.EmojiName,
}
time.Sleep(time.Millisecond)
_, nErr = ss.Reaction().Save(reaction2)
require.NoError(t, nErr)
postList, err = ss.Post().Get(context.Background(), reaction2.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, secondUpdateAt, "should've marked post as updated even if HasReactions doesn't change")
// different post
// create post1
post1, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction3 := &model.Reaction{
UserId: reaction1.UserId,
PostId: post1.Id,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction3)
require.NoError(t, nErr)
// different emoji
reaction4 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
EmojiName: model.NewId(),
}
_, nErr = ss.Reaction().Save(reaction4)
require.NoError(t, nErr)
// invalid reaction
reaction5 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
}
_, nErr = ss.Reaction().Save(reaction5)
require.Error(t, nErr, "should've failed for invalid reaction")
}
func testReactionDelete(t *testing.T, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
_, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
result, err := ss.Post().Get(context.Background(), reaction.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
firstUpdateAt := result.Posts[post.Id].UpdateAt
_, nErr = ss.Reaction().Delete(reaction)
require.NoError(t, nErr)
reactions, rErr := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, rErr)
assert.Empty(t, reactions, "should've deleted reaction")
postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = false on post")
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should mark post as updated after deleting reactions")
})
t.Run("Undelete", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
savedReaction, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
updateAt := savedReaction.UpdateAt
_, nErr = ss.Reaction().Delete(savedReaction)
require.NoError(t, nErr)
// add same reaction back and ensure update_at is set
_, nErr = ss.Reaction().Save(savedReaction)
require.NoError(t, nErr)
reactions, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
assert.Len(t, reactions, 1)
assert.GreaterOrEqual(t, reactions[0].UpdateAt, updateAt)
})
}
func testReactionGetForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
// create post
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
require.NoError(t, err)
postId := post.Id
post1Id := post1.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
},
{
UserId: post1Id,
PostId: postId,
EmojiName: "smile",
},
{
UserId: userId,
PostId: postId,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post1Id,
EmojiName: "angry",
},
}
for _, reaction := range reactions {
_, err = ss.Reaction().Save(reaction)
require.NoError(t, err)
}
// save and delete an additional reaction to test soft deletion
temp := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "grin",
}
savedTmp, err := ss.Reaction().Save(temp)
require.NoError(t, err)
_, err = ss.Reaction().Delete(savedTmp)
require.NoError(t, err)
returned, err := ss.Reaction().GetForPost(postId, false)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
for _, reaction := range reactions {
found := false
for _, returnedReaction := range returned {
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
returnedReaction.EmojiName == reaction.EmojiName && returnedReaction.UpdateAt > 0 {
found = true
break
}
}
if !found {
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
} else if found {
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
}
}
// Should return cached item
returned, err = ss.Reaction().GetForPost(postId, true)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
for _, reaction := range reactions {
found := false
for _, returnedReaction := range returned {
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
returnedReaction.EmojiName == reaction.EmojiName {
found = true
break
}
}
if !found {
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
} else if found {
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
}
}
}
func testReactionGetForPostSince(t *testing.T, ss store.Store, s SqlStore) {
now := model.GetMillis()
later := now + 1800000 // add 30 minutes
remoteId := model.NewId()
userId := model.NewId()
// create post
post, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post1, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
postId := post.Id
post1Id := post1.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
UpdateAt: later,
},
{
UserId: model.NewId(),
PostId: postId,
EmojiName: "smile",
},
{
UserId: userId,
PostId: postId,
EmojiName: "sad",
UpdateAt: later,
RemoteId: &remoteId,
},
{
UserId: userId,
PostId: post1Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: postId,
EmojiName: "angry",
DeleteAt: now + 1,
UpdateAt: later,
},
}
for _, reaction := range reactions {
delete := reaction.DeleteAt
update := reaction.UpdateAt
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
if delete > 0 {
_, err = ss.Reaction().Delete(reaction)
require.NoError(t, err)
}
if update > 0 {
err = forceUpdateAt(reaction, update, s)
require.NoError(t, err)
}
err = forceNULL(reaction, s) // test COALESCE
require.NoError(t, err)
}
t.Run("reactions since", func(t *testing.T) {
// should return 2 reactions that are not deleted for post
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", false)
require.NoError(t, err)
require.Len(t, returned, 2, "should've returned 2 non-deleted reactions")
for _, r := range returned {
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
}
})
t.Run("reactions since, incl deleted", func(t *testing.T) {
// should return 3 reactions for post, including one deleted
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", true)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
var count int
for _, r := range returned {
if r.DeleteAt > 0 {
count++
}
}
assert.Equal(t, 1, count, "should not have returned 1 deleted reaction")
})
t.Run("reactions since, filter remoteId", func(t *testing.T) {
// should return 1 reactions that are not deleted for post and have no remoteId
returned, err := ss.Reaction().GetForPostSince(postId, later-1, remoteId, false)
require.NoError(t, err)
require.Len(t, returned, 1, "should've returned 1 filtered reactions")
for _, r := range returned {
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
}
})
t.Run("reactions since, invalid post", func(t *testing.T) {
// should return 0 reactions for invalid post
returned, err := ss.Reaction().GetForPostSince(model.NewId(), later-1, "", true)
require.NoError(t, err)
require.Empty(t, returned, "should've returned 0 reactions")
})
t.Run("reactions since, far future", func(t *testing.T) {
// should return 0 reactions for since far in the future
returned, err := ss.Reaction().GetForPostSince(postId, later*2, "", true)
require.NoError(t, err)
require.Empty(t, returned, "should've returned 0 reactions")
})
}
func forceUpdateAt(reaction *model.Reaction, updateAt int64, s SqlStore) error {
params := map[string]any{
"userid": reaction.UserId,
"postid": reaction.PostId,
"emojiname": reaction.EmojiName,
"updateat": updateAt,
}
sqlResult, err := s.GetMasterX().NamedExec(`
UPDATE
Reactions
SET
UpdateAt=:updateat
WHERE
UserId = :userid AND
PostId = :postid AND
EmojiName = :emojiname`, params,
)
if err != nil {
return err
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return errors.New("expected one row affected")
}
return nil
}
func forceNULL(reaction *model.Reaction, s SqlStore) error {
if _, err := s.GetMasterX().Exec(`UPDATE Reactions SET UpdateAt = NULL WHERE UpdateAt = 0`); err != nil {
return err
}
if _, err := s.GetMasterX().Exec(`UPDATE Reactions SET DeleteAt = NULL WHERE DeleteAt = 0`); err != nil {
return err
}
return nil
}
func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore) {
emojiToDelete := model.NewId()
post, err1 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err1)
post2, err2 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err2)
post3, err3 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err3)
userId := model.NewId()
reactions := []*model.Reaction{
{
UserId: userId,
PostId: post.Id,
EmojiName: emojiToDelete,
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: emojiToDelete,
},
{
UserId: userId,
PostId: post.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: emojiToDelete,
},
}
for _, reaction := range reactions {
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
// make at least one Reaction record contain NULL for Update and DeleteAt to simulate post schema upgrade case.
if reaction.EmojiName == emojiToDelete {
err = forceNULL(reaction, s)
require.NoError(t, err)
}
}
err := ss.Reaction().DeleteAllWithEmojiName(emojiToDelete)
require.NoError(t, err)
// check that the reactions were deleted
returned, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "should've only removed reactions with emoji name")
for _, reaction := range returned {
assert.NotEqual(t, reaction.EmojiName, "smile", "should've removed reaction with emoji name")
}
returned, err = ss.Reaction().GetForPost(post2.Id, false)
require.NoError(t, err)
assert.Len(t, returned, 1, "should've only removed reactions with emoji name")
returned, err = ss.Reaction().GetForPost(post3.Id, false)
require.NoError(t, err)
assert.Empty(t, returned, "should've only removed reactions with emoji name")
// check that the posts are updated
postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
postList, err = ss.Post().Get(context.Background(), post2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post2.Id].HasReactions, "post should still have reactions")
postList, err = ss.Post().Get(context.Background(), post3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
}
func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 3000,
})
require.NoError(t, err)
// Reactions will be deleted based on the timestamp of their post. So the time at
// which a reaction was created doesn't matter.
reactions := []*model.Reaction{
{
UserId: model.NewId(),
PostId: olderPost.Id,
EmojiName: "sad",
},
{
UserId: model.NewId(),
PostId: olderPost.Id,
EmojiName: "sad",
},
{
UserId: model.NewId(),
PostId: newerPost.Id,
EmojiName: "smile",
},
}
for _, reaction := range reactions {
_, err = ss.Reaction().Save(reaction)
require.NoError(t, err)
}
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Reaction().DeleteOrphanedRows(limit)
require.NoError(t, err)
returned, err := ss.Reaction().GetForPost(olderPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 0, "reactions for older post should have been deleted")
returned, err = ss.Reaction().GetForPost(newerPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "reactions for newer post should not have been deleted")
}
func testReactionBulkGetForPosts(t *testing.T, ss store.Store) {
userId := model.NewId()
post, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
postId := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post2Id := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post3Id := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post4Id := post.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
},
{
UserId: model.NewId(),
PostId: post2Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post3Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: postId,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post2Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post4Id,
EmojiName: "angry",
},
}
for _, reaction := range reactions {
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
}
postIds := []string{postId, post2Id, post3Id}
returned, err := ss.Reaction().BulkGetForPosts(postIds)
require.NoError(t, err)
require.Len(t, returned, 5, "should've returned 5 reactions")
post4IdFound := false
for _, reaction := range returned {
if reaction.PostId == post4Id {
post4IdFound = true
break
}
}
require.False(t, post4IdFound, "Wrong reaction returned")
}
// testReactionDeadlock is a best-case attempt to recreate the deadlock scenario.
// It at least deadlocks 2 times out of 5.
func testReactionDeadlock(t *testing.T, ss store.Store) {
ss = retrylayer.New(ss)
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
postId := post.Id
post, err = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction1 := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
_, nErr := ss.Reaction().Save(reaction1)
require.NoError(t, nErr)
// different user
reaction2 := &model.Reaction{
UserId: model.NewId(),
PostId: reaction1.PostId,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction2)
require.NoError(t, nErr)
// different post
reaction3 := &model.Reaction{
UserId: reaction1.UserId,
PostId: postId,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction3)
require.NoError(t, nErr)
// different emoji
reaction4 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
EmojiName: model.NewId(),
}
_, nErr = ss.Reaction().Save(reaction4)
require.NoError(t, nErr)
var wg sync.WaitGroup
wg.Add(2)
// 1st tx
go func() {
defer wg.Done()
err := ss.Reaction().DeleteAllWithEmojiName(reaction1.EmojiName)
require.NoError(t, err)
}()
// 2nd tx
go func() {
defer wg.Done()
_, err := ss.Reaction().Delete(reaction2)
require.NoError(t, err)
}()
wg.Wait()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"strings"
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoteClusterStore(t *testing.T, ss store.Store) {
t.Run("RemoteClusterGetAllInChannel", func(t *testing.T) { testRemoteClusterGetAllInChannel(t, ss) })
t.Run("RemoteClusterGetAllNotInChannel", func(t *testing.T) { testRemoteClusterGetAllNotInChannel(t, ss) })
t.Run("RemoteClusterSave", func(t *testing.T) { testRemoteClusterSave(t, ss) })
t.Run("RemoteClusterDelete", func(t *testing.T) { testRemoteClusterDelete(t, ss) })
t.Run("RemoteClusterGet", func(t *testing.T) { testRemoteClusterGet(t, ss) })
t.Run("RemoteClusterGetAll", func(t *testing.T) { testRemoteClusterGetAll(t, ss) })
t.Run("RemoteClusterGetByTopic", func(t *testing.T) { testRemoteClusterGetByTopic(t, ss) })
t.Run("RemoteClusterUpdateTopics", func(t *testing.T) { testRemoteClusterUpdateTopics(t, ss) })
}
func testRemoteClusterSave(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote",
SiteURL: "somewhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
require.Equal(t, rc.Name, rcSaved.Name)
require.Equal(t, rc.SiteURL, rcSaved.SiteURL)
require.Greater(t, rc.CreateAt, int64(0))
require.Equal(t, rc.LastPingAt, int64(0))
})
t.Run("Save missing display name", func(t *testing.T) {
rc := &model.RemoteCluster{
SiteURL: "somewhere.com",
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
require.Error(t, err)
})
t.Run("Save missing creator id", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote_2",
SiteURL: "somewhere.com",
}
_, err := ss.RemoteCluster().Save(rc)
require.Error(t, err)
})
}
func testRemoteClusterDelete(t *testing.T, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote",
SiteURL: "nowhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
deleted, err := ss.RemoteCluster().Delete(rcSaved.RemoteId)
require.NoError(t, err)
require.True(t, deleted)
})
t.Run("Delete nonexistent", func(t *testing.T) {
deleted, err := ss.RemoteCluster().Delete(model.NewId())
require.NoError(t, err)
require.False(t, deleted)
})
}
func testRemoteClusterGet(t *testing.T, ss store.Store) {
t.Run("Get", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote_2",
SiteURL: "nowhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
rcGet, err := ss.RemoteCluster().Get(rcSaved.RemoteId)
require.NoError(t, err)
require.Equal(t, rcSaved.RemoteId, rcGet.RemoteId)
})
t.Run("Get not found", func(t *testing.T) {
_, err := ss.RemoteCluster().Get(model.NewId())
require.Error(t, err)
})
}
func testRemoteClusterGetAll(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
userId := model.NewId()
now := model.GetMillis()
pingLongAgo := model.GetMillis() - (model.RemoteOfflineAfterMillis * 3)
data := []*model.RemoteCluster{
{Name: "offline_remote", CreatorId: userId, SiteURL: "somewhere.com", LastPingAt: pingLongAgo, Topics: " shared incident "},
{Name: "some_online_remote", CreatorId: userId, SiteURL: "nowhere.com", LastPingAt: now, Topics: " shared incident "},
{Name: "another_online_remote", CreatorId: model.NewId(), SiteURL: "underwhere.com", LastPingAt: now, Topics: ""},
{Name: "another_offline_remote", CreatorId: model.NewId(), SiteURL: "knowhere.com", LastPingAt: pingLongAgo, Topics: " shared "},
{Name: "brand_new_offline_remote", CreatorId: userId, SiteURL: "", LastPingAt: 0, Topics: " bogus shared stuff "},
}
idsAll := make([]string, 0)
idsOnline := make([]string, 0)
idsShareTopic := make([]string, 0)
for _, item := range data {
online := item.LastPingAt == now
saved, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
idsAll = append(idsAll, saved.RemoteId)
if online {
idsOnline = append(idsOnline, saved.RemoteId)
}
if strings.Contains(saved.Topics, " shared ") {
idsShareTopic = append(idsShareTopic, saved.RemoteId)
}
}
t.Run("GetAll", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure all the test data remotes were returned.
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsAll)
})
t.Run("GetAll online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure all the online remotes were returned.
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsOnline)
})
t.Run("GetAll by topic", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only correct topic returned
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsShareTopic)
})
t.Run("GetAll online by topic", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only online remotes were returned.
ids := getIds(remotes)
assert.Subset(t, idsOnline, ids)
// make sure correct topic returned
assert.Subset(t, idsShareTopic, ids)
assert.Len(t, ids, 1)
})
t.Run("GetAll by Creator", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
CreatorId: userId,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only correct creator returned
assert.Len(t, remotes, 3)
for _, rc := range remotes {
assert.Equal(t, userId, rc.CreatorId)
}
})
t.Run("GetAll by Confirmed", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
OnlyConfirmed: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only confirmed returned
assert.Len(t, remotes, 4)
for _, rc := range remotes {
assert.NotEmpty(t, rc.SiteURL)
}
})
}
func testRemoteClusterGetAllInChannel(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
now := model.GetMillis()
userId := model.NewId()
channel1, err := createTestChannel(ss, "channel_1")
require.NoError(t, err)
channel2, err := createTestChannel(ss, "channel_2")
require.NoError(t, err)
channel3, err := createTestChannel(ss, "channel_3")
require.NoError(t, err)
// Create shared channels
scData := []*model.SharedChannel{
{ChannelId: channel1.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_1", CreatorId: model.NewId()},
{ChannelId: channel2.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_2", CreatorId: model.NewId()},
{ChannelId: channel3.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_3", CreatorId: model.NewId()},
}
for _, item := range scData {
_, err := ss.SharedChannel().Save(item)
require.NoError(t, err)
}
// Create some remote clusters
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId(), LastPingAt: 0},
{Name: "CCCC_Inc", CreatorId: userId, SiteURL: "cccc.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "DDDD_Inc", CreatorId: userId, SiteURL: "dddd.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "EEEE_Inc", CreatorId: userId, SiteURL: "eeee.com", RemoteId: model.NewId(), LastPingAt: 0},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
// Create some shared channel remotes
scrData := []*model.SharedChannelRemote{
{ChannelId: channel1.Id, RemoteId: rcData[0].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel1.Id, RemoteId: rcData[1].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[2].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[3].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[4].RemoteId, CreatorId: model.NewId()},
}
for _, item := range scrData {
_, err := ss.SharedChannel().SaveRemote(item)
require.NoError(t, err)
}
t.Run("Channel 1", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 1 should have 2 remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId}, ids)
})
t.Run("Channel 1 online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 1, "channel 1 should have 1 online remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId}, ids)
})
t.Run("Channel 2", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 2 online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 2 should have 2 online remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId}, ids)
})
t.Run("Channel 3", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Empty(t, list, "channel 3 should have 0 remote clusters")
})
}
func testRemoteClusterGetAllNotInChannel(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
userId := model.NewId()
channel1, err := createTestChannel(ss, "channel_1")
require.NoError(t, err)
channel2, err := createTestChannel(ss, "channel_2")
require.NoError(t, err)
channel3, err := createTestChannel(ss, "channel_3")
require.NoError(t, err)
// Create shared channels
scData := []*model.SharedChannel{
{ChannelId: channel1.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_1", CreatorId: model.NewId()},
{ChannelId: channel2.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_2", CreatorId: model.NewId()},
{ChannelId: channel3.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_3", CreatorId: model.NewId()},
}
for _, item := range scData {
_, err := ss.SharedChannel().Save(item)
require.NoError(t, err)
}
// Create some remote clusters
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId()},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId()},
{Name: "CCCC_Inc", CreatorId: userId, SiteURL: "cccc.com", RemoteId: model.NewId()},
{Name: "DDDD_Inc", CreatorId: userId, SiteURL: "dddd.com", RemoteId: model.NewId()},
{Name: "EEEE_Inc", CreatorId: userId, SiteURL: "eeee.com", RemoteId: model.NewId()},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
// Create some shared channel remotes
scrData := []*model.SharedChannelRemote{
{ChannelId: channel1.Id, RemoteId: rcData[0].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel1.Id, RemoteId: rcData[1].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[2].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[3].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel3.Id, RemoteId: rcData[4].RemoteId, CreatorId: model.NewId()},
}
for _, item := range scrData {
_, err := ss.SharedChannel().SaveRemote(item)
require.NoError(t, err)
}
t.Run("Channel 1", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 1 should have 3 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 2", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 3", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 4, "channel 3 should have 4 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[2].RemoteId, rcData[3].RemoteId}, ids)
})
t.Run("Channel with no share remotes", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: model.NewId(),
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 5, "should have 5 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[2].RemoteId, rcData[3].RemoteId,
rcData[4].RemoteId}, ids)
})
}
func getIds(remotes []*model.RemoteCluster) []string {
ids := make([]string, 0, len(remotes))
for _, r := range remotes {
ids = append(ids, r.RemoteId)
}
return ids
}
func testRemoteClusterGetByTopic(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: model.NewId(), SiteURL: "aaaa.com", RemoteId: model.NewId(), Topics: ""},
{Name: "BBBB_Inc", CreatorId: model.NewId(), SiteURL: "bbbb.com", RemoteId: model.NewId(), Topics: " share "},
{Name: "CCCC_Inc", CreatorId: model.NewId(), SiteURL: "cccc.com", RemoteId: model.NewId(), Topics: " incident share "},
{Name: "DDDD_Inc", CreatorId: model.NewId(), SiteURL: "dddd.com", RemoteId: model.NewId(), Topics: " bogus "},
{Name: "EEEE_Inc", CreatorId: model.NewId(), SiteURL: "eeee.com", RemoteId: model.NewId(), Topics: " logs share incident "},
{Name: "FFFF_Inc", CreatorId: model.NewId(), SiteURL: "ffff.com", RemoteId: model.NewId(), Topics: " bogus incident "},
{Name: "GGGG_Inc", CreatorId: model.NewId(), SiteURL: "gggg.com", RemoteId: model.NewId(), Topics: "*"},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
testData := []struct {
topic string
expectedCount int
expectError bool
}{
{topic: "", expectedCount: 7, expectError: false},
{topic: " ", expectedCount: 0, expectError: true},
{topic: "share", expectedCount: 4},
{topic: " share ", expectedCount: 4},
{topic: "bogus", expectedCount: 3},
{topic: "non-existent", expectedCount: 1},
{topic: "*", expectedCount: 0, expectError: true}, // can't query with wildcard
}
for _, tt := range testData {
filter := model.RemoteClusterQueryFilter{
Topic: tt.topic,
}
list, err := ss.RemoteCluster().GetAll(filter)
if tt.expectError {
assert.Errorf(t, err, "expected error for topic=%s", tt.topic)
} else {
assert.NoErrorf(t, err, "expected no error for topic=%s", tt.topic)
}
assert.Lenf(t, list, tt.expectedCount, "topic=%s", tt.topic)
}
}
func testRemoteClusterUpdateTopics(t *testing.T, ss store.Store) {
remoteId := model.NewId()
rc := &model.RemoteCluster{
DisplayName: "Blap Inc",
Name: "blap",
SiteURL: "blap.com",
RemoteId: remoteId,
Topics: "",
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
testData := []struct {
topics string
expected string
}{
{topics: "", expected: ""},
{topics: " ", expected: ""},
{topics: "share", expected: " share "},
{topics: " share ", expected: " share "},
{topics: "share incident", expected: " share incident "},
{topics: " share incident ", expected: " share incident "},
}
for _, tt := range testData {
_, err = ss.RemoteCluster().UpdateTopics(remoteId, tt.topics)
require.NoError(t, err)
rcUpdated, err := ss.RemoteCluster().Get(remoteId)
require.NoError(t, err)
require.Equal(t, tt.expected, rcUpdated.Topics)
}
}
func clearRemoteClusters(ss store.Store) error {
list, err := ss.RemoteCluster().GetAll(model.RemoteClusterQueryFilter{})
if err != nil {
return err
}
for _, rc := range list {
if _, err := ss.RemoteCluster().Delete(rc.RemoteId); err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestRetentionPolicyStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testRetentionPolicyStoreSave(t, ss, s) })
t.Run("Patch", func(t *testing.T) { testRetentionPolicyStorePatch(t, ss, s) })
t.Run("Get", func(t *testing.T) { testRetentionPolicyStoreGet(t, ss, s) })
t.Run("GetCount", func(t *testing.T) { testRetentionPolicyStoreGetCount(t, ss, s) })
t.Run("Delete", func(t *testing.T) { testRetentionPolicyStoreDelete(t, ss, s) })
t.Run("GetChannels", func(t *testing.T) { testRetentionPolicyStoreGetChannels(t, ss, s) })
t.Run("AddChannels", func(t *testing.T) { testRetentionPolicyStoreAddChannels(t, ss, s) })
t.Run("RemoveChannels", func(t *testing.T) { testRetentionPolicyStoreRemoveChannels(t, ss, s) })
t.Run("GetTeams", func(t *testing.T) { testRetentionPolicyStoreGetTeams(t, ss, s) })
t.Run("AddTeams", func(t *testing.T) { testRetentionPolicyStoreAddTeams(t, ss, s) })
t.Run("RemoveTeams", func(t *testing.T) { testRetentionPolicyStoreRemoveTeams(t, ss, s) })
t.Run("RemoveOrphanedRows", func(t *testing.T) { testRetentionPolicyStoreRemoveOrphanedRows(t, ss, s) })
t.Run("GetPoliciesForUser", func(t *testing.T) { testRetentionPolicyStoreGetPoliciesForUser(t, ss, s) })
}
func getRetentionPolicyWithTeamAndChannelIds(t *testing.T, ss store.Store, policyID string) *model.RetentionPolicyWithTeamAndChannelIDs {
policyWithCounts, err := ss.RetentionPolicy().Get(policyID)
require.NoError(t, err)
policyWithIds := model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policyID,
DisplayName: policyWithCounts.DisplayName,
PostDurationDays: policyWithCounts.PostDurationDays,
},
ChannelIDs: make([]string, int(policyWithCounts.ChannelCount)),
TeamIDs: make([]string, int(policyWithCounts.TeamCount)),
}
channels, err := ss.RetentionPolicy().GetChannels(policyID, 0, 1000)
require.NoError(t, err)
for i, channel := range channels {
policyWithIds.ChannelIDs[i] = channel.Id
}
teams, err := ss.RetentionPolicy().GetTeams(policyID, 0, 1000)
require.NoError(t, err)
for i, team := range teams {
policyWithIds.TeamIDs[i] = team.Id
}
return &policyWithIds
}
func CheckRetentionPolicyWithTeamAndChannelIdsAreEqual(t *testing.T, p1, p2 *model.RetentionPolicyWithTeamAndChannelIDs) {
require.Equal(t, p1.ID, p2.ID)
require.Equal(t, p1.DisplayName, p2.DisplayName)
require.Equal(t, p1.PostDurationDays, p2.PostDurationDays)
require.Equal(t, len(p1.ChannelIDs), len(p2.ChannelIDs))
if p1.ChannelIDs == nil || p2.ChannelIDs == nil {
require.Equal(t, p1.ChannelIDs, p2.ChannelIDs)
} else {
sort.Strings(p1.ChannelIDs)
sort.Strings(p2.ChannelIDs)
}
for i := range p1.ChannelIDs {
require.Equal(t, p1.ChannelIDs[i], p2.ChannelIDs[i])
}
if p1.TeamIDs == nil || p2.TeamIDs == nil {
require.Equal(t, p1.TeamIDs, p2.TeamIDs)
} else {
sort.Strings(p1.TeamIDs)
sort.Strings(p2.TeamIDs)
}
require.Equal(t, len(p1.TeamIDs), len(p2.TeamIDs))
for i := range p1.TeamIDs {
require.Equal(t, p1.TeamIDs[i], p2.TeamIDs[i])
}
}
func CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t *testing.T, p1, p2 *model.RetentionPolicyWithTeamAndChannelCounts) {
require.Equal(t, p1.ID, p2.ID)
require.Equal(t, p1.DisplayName, p2.DisplayName)
require.Equal(t, p1.PostDurationDays, p2.PostDurationDays)
require.Equal(t, p1.ChannelCount, p2.ChannelCount)
require.Equal(t, p1.TeamCount, p2.TeamCount)
}
func checkRetentionPolicyLikeThisExists(t *testing.T, ss store.Store, expected *model.RetentionPolicyWithTeamAndChannelIDs) {
retrieved := getRetentionPolicyWithTeamAndChannelIds(t, ss, expected.ID)
CheckRetentionPolicyWithTeamAndChannelIdsAreEqual(t, expected, retrieved)
}
func copyRetentionPolicyWithTeamAndChannelIds(policy *model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelIDs {
cpy := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: policy.RetentionPolicy,
ChannelIDs: make([]string, len(policy.ChannelIDs)),
TeamIDs: make([]string, len(policy.TeamIDs)),
}
copy(cpy.ChannelIDs, policy.ChannelIDs)
copy(cpy.TeamIDs, policy.TeamIDs)
return cpy
}
func createChannelsForRetentionPolicy(t *testing.T, ss store.Store, teamId string, numChannels int) (channelIDs []string) {
channelIDs = make([]string, numChannels)
for i := range channelIDs {
name := "channel" + model.NewId()
channel := &model.Channel{
TeamId: teamId,
DisplayName: "Channel " + name,
Name: name,
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
channelIDs[i] = channel.Id
}
return
}
func createTeamsForRetentionPolicy(t *testing.T, ss store.Store, numTeams int) (teamIDs []string) {
teamIDs = make([]string, numTeams)
for i := range teamIDs {
name := "team" + model.NewId()
team := &model.Team{
DisplayName: "Team " + name,
Name: name,
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
teamIDs[i] = team.Id
}
return
}
func createTeamsAndChannelsForRetentionPolicy(t *testing.T, ss store.Store) (teamIDs, channelIDs []string) {
teamIDs = createTeamsForRetentionPolicy(t, ss, 2)
channels1 := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 1)
channels2 := createChannelsForRetentionPolicy(t, ss, teamIDs[1], 2)
channelIDs = append(channels1, channels2...)
return
}
func cleanupRetentionPolicyTest(s SqlStore) {
// Manually clear tables until testlib can handle cleanups
tables := []string{"RetentionPolicies", "RetentionPoliciesChannels", "RetentionPoliciesTeams"}
for _, table := range tables {
if _, err := s.GetMasterX().Exec("DELETE FROM " + table); err != nil {
panic(err)
}
}
}
func deleteTeamsAndChannels(ss store.Store, teamIDs, channelIDs []string) {
for _, teamID := range teamIDs {
if err := ss.Team().PermanentDelete(teamID); err != nil {
panic(err)
}
}
for _, channelID := range channelIDs {
if err := ss.Channel().PermanentDelete(channelID); err != nil {
panic(err)
}
}
}
func createRetentionPolicyWithTeamAndChannelIds(displayName string, teamIDs, channelIDs []string) *model.RetentionPolicyWithTeamAndChannelIDs {
return &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: displayName,
PostDurationDays: model.NewInt64(30),
},
TeamIDs: teamIDs,
ChannelIDs: channelIDs,
}
}
// saveRetentionPolicyWithTeamAndChannelIds creates a model.RetentionPolicyWithTeamAndChannelIds struct using
// the display name, team IDs, and channel IDs. The new policy ID will be assigned to the struct and returned.
// The team IDs and channel IDs are kept the same.
func saveRetentionPolicyWithTeamAndChannelIds(t *testing.T, ss store.Store, displayName string, teamIDs, channelIDs []string) *model.RetentionPolicyWithTeamAndChannelIDs {
proposal := createRetentionPolicyWithTeamAndChannelIds(displayName, teamIDs, channelIDs)
policyWithCounts, err := ss.RetentionPolicy().Save(proposal)
require.NoError(t, err)
proposal.ID = policyWithCounts.ID
return proposal
}
func restoreRetentionPolicy(t *testing.T, ss store.Store, policy *model.RetentionPolicyWithTeamAndChannelIDs) {
_, err := ss.RetentionPolicy().Patch(policy)
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
}
func testRetentionPolicyStoreSave(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("teams and channels are nil", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
policy.ChannelIDs = []string{}
policy.TeamIDs = []string{}
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("teams and channels are empty", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", []string{}, []string{})
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("some teams and channels are specified", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 3", teamIDs, channelIDs)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("team specified does not exist", func(t *testing.T) {
policy := createRetentionPolicyWithTeamAndChannelIds("Policy 4", []string{"no_such_team"}, []string{})
_, err := ss.RetentionPolicy().Save(policy)
require.Error(t, err)
})
t.Run("channel specified does not exist", func(t *testing.T) {
policy := createRetentionPolicyWithTeamAndChannelIds("Policy 5", []string{}, []string{"no_such_channel"})
_, err := ss.RetentionPolicy().Save(policy)
require.Error(t, err)
})
}
func testRetentionPolicyStorePatch(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("modify DisplayName", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
DisplayName: "something new",
},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.DisplayName = patch.DisplayName
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("modify PostDuration", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
PostDurationDays: model.NewInt64(10000),
},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.PostDurationDays = patch.PostDurationDays
checkRetentionPolicyLikeThisExists(t, ss, expected)
// Store a negative value (= infinity)
patch.PostDurationDays = model.NewInt64(-1)
_, err = ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected = copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.PostDurationDays = patch.PostDurationDays
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("clear TeamIds", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
TeamIDs: make([]string, 0),
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.TeamIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add team which does not exist", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
TeamIDs: []string{"no_such_team"},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.Error(t, err)
})
t.Run("clear ChannelIds", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
ChannelIDs: make([]string, 0),
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.ChannelIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add channel which does not exist", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
ChannelIDs: []string{"no_such_channel"},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.Error(t, err)
})
}
func testRetentionPolicyStoreGet(t *testing.T, ss store.Store, s SqlStore) {
t.Run("get none", func(t *testing.T) {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(0, 10)
require.NoError(t, err)
require.NotNil(t, retrievedPolicies)
require.Equal(t, 0, len(retrievedPolicies))
})
// create multiple policies
policiesWithCounts := make([]*model.RetentionPolicyWithTeamAndChannelCounts, 0)
for i := 0; i < 3; i++ {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policyWithIds := createRetentionPolicyWithTeamAndChannelIds(
"Policy "+strconv.Itoa(i+1), teamIDs, channelIDs)
policyWithCounts, err := ss.RetentionPolicy().Save(policyWithIds)
require.NoError(t, err)
policiesWithCounts = append(policiesWithCounts, policyWithCounts)
}
defer cleanupRetentionPolicyTest(s)
t.Run("get all", func(t *testing.T) {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(0, 60)
require.NoError(t, err)
require.Equal(t, len(policiesWithCounts), len(retrievedPolicies))
for i := range policiesWithCounts {
CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t, policiesWithCounts[i], retrievedPolicies[i])
}
})
t.Run("get all with limit", func(t *testing.T) {
for i := range policiesWithCounts {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(i, 1)
require.NoError(t, err)
require.Equal(t, 1, len(retrievedPolicies))
CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t, policiesWithCounts[i], retrievedPolicies[0])
}
})
t.Run("get all with same display name", func(t *testing.T) {
for i := 0; i < 5; i++ {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
proposal := createRetentionPolicyWithTeamAndChannelIds(
"Policy Name", teamIDs, channelIDs)
_, err := ss.RetentionPolicy().Save(proposal)
require.NoError(t, err)
}
policies, err := ss.RetentionPolicy().GetAll(0, 60)
require.NoError(t, err)
for i := 1; i < len(policies); i++ {
require.True(t,
policies[i-1].DisplayName < policies[i].DisplayName ||
(policies[i-1].DisplayName == policies[i].DisplayName &&
policies[i-1].ID < policies[i].ID),
"policies with the same display name should be sorted by ID")
}
})
}
func testRetentionPolicyStoreGetCount(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no policies", func(t *testing.T) {
count, err := ss.RetentionPolicy().GetCount()
require.NoError(t, err)
require.Equal(t, int64(0), count)
})
t.Run("some policies", func(t *testing.T) {
for i := 0; i < 2; i++ {
saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy "+strconv.Itoa(i), nil, nil)
}
count, err := ss.RetentionPolicy().GetCount()
require.NoError(t, err)
require.Equal(t, int64(2), count)
})
}
func testRetentionPolicyStoreDelete(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("delete policy", func(t *testing.T) {
err := ss.RetentionPolicy().Delete(policy.ID)
require.NoError(t, err)
policies, err := ss.RetentionPolicy().GetAll(0, 1)
require.NoError(t, err)
require.Empty(t, policies)
})
}
func testRetentionPolicyStoreGetChannels(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no channels", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
channels, err := ss.RetentionPolicy().GetChannels(policy.ID, 0, 1)
require.NoError(t, err)
require.Len(t, channels, 0)
})
t.Run("some channels", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", teamIDs, channelIDs)
channels, err := ss.RetentionPolicy().GetChannels(policy.ID, 0, len(channelIDs))
require.NoError(t, err)
require.Len(t, channels, len(channelIDs))
sort.Strings(channelIDs)
sort.Slice(channels, func(i, j int) bool {
return channels[i].Id < channels[j].Id
})
for i := range channelIDs {
require.Equal(t, channelIDs[i], channels[i].Id)
}
})
}
func testRetentionPolicyStoreAddChannels(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("add empty array", func(t *testing.T) {
err := ss.RetentionPolicy().AddChannels(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("add new channels", func(t *testing.T) {
channelIDs := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 2)
defer deleteTeamsAndChannels(ss, nil, channelIDs)
err := ss.RetentionPolicy().AddChannels(policy.ID, channelIDs)
require.NoError(t, err)
// verify that the channels were actually added
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.ChannelIDs = append(copy.ChannelIDs, channelIDs...)
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add channel which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().AddChannels(policy.ID, []string{"no_such_channel"})
require.Error(t, err)
})
t.Run("add channel to policy which does not exist", func(t *testing.T) {
channelIDs := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 1)
defer deleteTeamsAndChannels(ss, nil, channelIDs)
err := ss.RetentionPolicy().AddChannels("no_such_policy", channelIDs)
require.Error(t, err)
})
}
func testRetentionPolicyStoreRemoveChannels(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("remove empty array", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("remove existing channel", func(t *testing.T) {
channelID := channelIDs[0]
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{channelID})
require.NoError(t, err)
// verify that the channel was actually removed
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.ChannelIDs = make([]string, 0)
for _, oldChannelID := range policy.ChannelIDs {
if oldChannelID != channelID {
copy.ChannelIDs = append(copy.ChannelIDs, oldChannelID)
}
}
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("remove channel which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{"no_such_channel"})
require.NoError(t, err)
// verify that the policy did not change
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
}
func testRetentionPolicyStoreGetTeams(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no teams", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
teams, err := ss.RetentionPolicy().GetTeams(policy.ID, 0, 1)
require.NoError(t, err)
require.Len(t, teams, 0)
})
t.Run("some teams", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", teamIDs, channelIDs)
teams, err := ss.RetentionPolicy().GetTeams(policy.ID, 0, len(teamIDs))
require.NoError(t, err)
require.Len(t, teams, len(teamIDs))
sort.Strings(teamIDs)
sort.Slice(teams, func(i, j int) bool {
return teams[i].Id < teams[j].Id
})
for i := range teamIDs {
require.Equal(t, teamIDs[i], teams[i].Id)
}
})
}
func testRetentionPolicyStoreAddTeams(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("add empty array", func(t *testing.T) {
err := ss.RetentionPolicy().AddTeams(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("add new teams", func(t *testing.T) {
teamIDs := createTeamsForRetentionPolicy(t, ss, 2)
defer deleteTeamsAndChannels(ss, teamIDs, nil)
err := ss.RetentionPolicy().AddTeams(policy.ID, teamIDs)
require.NoError(t, err)
// verify that the teams were actually added
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.TeamIDs = append(copy.TeamIDs, teamIDs...)
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add team which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().AddTeams(policy.ID, []string{"no_such_team"})
require.Error(t, err)
})
t.Run("add team to policy which does not exist", func(t *testing.T) {
teamIDs := createTeamsForRetentionPolicy(t, ss, 1)
defer deleteTeamsAndChannels(ss, teamIDs, nil)
err := ss.RetentionPolicy().AddTeams("no_such_policy", teamIDs)
require.Error(t, err)
})
}
func testRetentionPolicyStoreRemoveTeams(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("remove empty array", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("remove existing team", func(t *testing.T) {
teamID := teamIDs[0]
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{teamID})
require.NoError(t, err)
// verify that the team was actually removed
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.TeamIDs = make([]string, 0)
for _, oldTeamID := range policy.TeamIDs {
if oldTeamID != teamID {
copy.TeamIDs = append(copy.TeamIDs, oldTeamID)
}
}
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("remove team which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{"no_such_team"})
require.NoError(t, err)
// verify that the policy did not change
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
}
func testRetentionPolicyStoreGetPoliciesForUser(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
user, userSaveErr := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
})
require.NoError(t, userSaveErr)
t.Run("user has no relevant policies", func(t *testing.T) {
// Teams
teamPolicies, err := ss.RetentionPolicy().GetTeamPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Empty(t, teamPolicies)
count, err := ss.RetentionPolicy().GetTeamPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(0), count)
// Channels
channelPolicies, err := ss.RetentionPolicy().GetChannelPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Empty(t, channelPolicies)
count, err = ss.RetentionPolicy().GetChannelPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(0), count)
})
t.Run("user has relevant policies", func(t *testing.T) {
for _, teamID := range teamIDs {
_, err := ss.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, -1)
require.NoError(t, err)
}
for _, channelID := range channelIDs {
_, err := ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channelID, UserId: user.Id, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, err)
}
// Teams
teamPolicies, err := ss.RetentionPolicy().GetTeamPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Len(t, teamPolicies, len(teamIDs))
count, err := ss.RetentionPolicy().GetTeamPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(len(teamIDs)), count)
// Channels
channelPolicies, err := ss.RetentionPolicy().GetChannelPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Len(t, channelPolicies, len(channelIDs))
count, err = ss.RetentionPolicy().GetChannelPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(len(channelIDs)), count)
})
}
func testRetentionPolicyStoreRemoveOrphanedRows(t *testing.T, ss store.Store, s SqlStore) {
teamID := createTeamsForRetentionPolicy(t, ss, 1)[0]
channelID := createChannelsForRetentionPolicy(t, ss, teamID, 1)[0]
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1",
[]string{teamID}, []string{channelID})
err := ss.Channel().PermanentDelete(channelID)
require.NoError(t, err)
err = ss.Team().PermanentDelete(teamID)
require.NoError(t, err)
_, err = ss.RetentionPolicy().DeleteOrphanedRows(1000)
require.NoError(t, err)
policy.ChannelIDs = make([]string, 0)
policy.TeamIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, policy)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestRoleStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testRoleStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testRoleStoreGet(t, ss) })
t.Run("GetAll", func(t *testing.T) { testRoleStoreGetAll(t, ss) })
t.Run("GetByName", func(t *testing.T) { testRoleStoreGetByName(t, ss) })
t.Run("GetNames", func(t *testing.T) { testRoleStoreGetByNames(t, ss) })
t.Run("Delete", func(t *testing.T) { testRoleStoreDelete(t, ss) })
t.Run("PermanentDeleteAll", func(t *testing.T) { testRoleStorePermanentDeleteAll(t, ss) })
t.Run("LowerScopedChannelSchemeRoles_AllChannelSchemeRoles", func(t *testing.T) { testRoleStoreLowerScopedChannelSchemeRoles(t, ss) })
t.Run("ChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest", func(t *testing.T) { testRoleStoreChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest(t, ss, s) })
}
func testRoleStoreSave(t *testing.T, ss store.Store) {
// Save a new role.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, r1.Name, d1.Name)
assert.Equal(t, r1.DisplayName, d1.DisplayName)
assert.Equal(t, r1.Description, d1.Description)
assert.Equal(t, r1.Permissions, d1.Permissions)
assert.Equal(t, r1.SchemeManaged, d1.SchemeManaged)
// Change the role permissions and update.
d1.Permissions = []string{
"invite_user",
"add_user_to_team",
"delete_public_channel",
}
d2, err := ss.Role().Save(d1)
assert.NoError(t, err)
assert.Len(t, d2.Id, 26)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, d1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Try saving one with an invalid ID set.
r3 := &model.Role{
Id: model.NewId(),
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r3)
assert.Error(t, err)
// Try saving one with a duplicate "name" field.
r4 := &model.Role{
Name: r1.Name,
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r4)
assert.Error(t, err)
}
func testRoleStoreGetAll(t *testing.T, ss store.Store) {
prev, err := ss.Role().GetAll()
require.NoError(t, err)
prevCount := len(prev)
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r1)
require.NoError(t, err)
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r2)
require.NoError(t, err)
data, err := ss.Role().GetAll()
require.NoError(t, err)
assert.Len(t, data, prevCount+2)
}
func testRoleStoreGet(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid role
d2, err := ss.Role().Get(d1.Id)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, r1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Get an invalid role
_, err = ss.Role().Get(model.NewId())
assert.Error(t, err)
}
func testRoleStoreGetByName(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid role
d2, err := ss.Role().GetByName(context.Background(), d1.Name)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, r1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Get an invalid role
_, err = ss.Role().GetByName(context.Background(), model.NewId())
assert.Error(t, err)
}
func testRoleStoreGetByNames(t *testing.T, ss store.Store) {
// Save some roles to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"read_channel",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r3 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"delete_private_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
d2, err := ss.Role().Save(r2)
assert.NoError(t, err)
assert.Len(t, d2.Id, 26)
d3, err := ss.Role().Save(r3)
assert.NoError(t, err)
assert.Len(t, d3.Id, 26)
// Get two valid roles.
n4 := []string{r1.Name, r2.Name}
roles4, err := ss.Role().GetByNames(n4)
assert.NoError(t, err)
assert.Len(t, roles4, 2)
assert.Contains(t, roles4, d1)
assert.Contains(t, roles4, d2)
assert.NotContains(t, roles4, d3)
// Get two invalid roles.
n5 := []string{model.NewId(), model.NewId()}
roles5, err := ss.Role().GetByNames(n5)
assert.NoError(t, err)
assert.Empty(t, roles5)
// Get one valid one and one invalid one.
n6 := []string{r1.Name, model.NewId()}
roles6, err := ss.Role().GetByNames(n6)
assert.NoError(t, err)
assert.Len(t, roles6, 1)
assert.Contains(t, roles6, d1)
assert.NotContains(t, roles6, d2)
assert.NotContains(t, roles6, d3)
}
func testRoleStoreDelete(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Check the role is there.
_, err = ss.Role().Get(d1.Id)
assert.NoError(t, err)
// Delete the role.
_, err = ss.Role().Delete(d1.Id)
assert.NoError(t, err)
// Check the role is deleted there.
d2, err := ss.Role().Get(d1.Id)
assert.NoError(t, err)
assert.NotZero(t, d2.DeleteAt)
d3, err := ss.Role().GetByName(context.Background(), d1.Name)
assert.NoError(t, err)
assert.NotZero(t, d3.DeleteAt)
// Try and delete a role that does not exist.
_, err = ss.Role().Delete(model.NewId())
assert.Error(t, err)
}
func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) {
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"read_channel",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err := ss.Role().Save(r1)
require.NoError(t, err)
_, err = ss.Role().Save(r2)
require.NoError(t, err)
roles, err := ss.Role().GetByNames([]string{r1.Name, r2.Name})
assert.NoError(t, err)
assert.Len(t, roles, 2)
err = ss.Role().PermanentDeleteAll()
assert.NoError(t, err)
roles, err = ss.Role().GetByNames([]string{r1.Name, r2.Name})
assert.NoError(t, err)
assert.Empty(t, roles)
}
func testRoleStoreLowerScopedChannelSchemeRoles(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
teamScheme1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme1, err := ss.Scheme().Save(teamScheme1)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme1.Id)
teamScheme2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme2, err = ss.Scheme().Save(teamScheme2)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme2.Id)
channelScheme1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme1, err = ss.Scheme().Save(channelScheme1)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme1.Id)
channelScheme2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme2, err = ss.Scheme().Save(channelScheme2)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme1.Id)
team1 := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme1.Id,
}
team1, err = ss.Team().Save(team1)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team1.Id)
team2 := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme2.Id,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team2.Id)
channel1 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme1.Id,
}
channel1, nErr := ss.Channel().Save(channel1, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel1.Id, 0)
channel2 := &model.Channel{
TeamId: team2.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme2.Id,
}
channel2, nErr = ss.Channel().Save(channel2, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel2.Id, 0)
t.Run("ChannelRolesUnderTeamRole", func(t *testing.T) {
t.Run("guest role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelGuestRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelGuestRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelGuestRole)
})
t.Run("user role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelUserRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelUserRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelUserRole)
})
t.Run("admin role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelAdminRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelAdminRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelAdminRole)
})
})
t.Run("AllChannelSchemeRoles", func(t *testing.T) {
t.Run("guest role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().AllChannelSchemeRoles()
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
allRoleNames := []string{
channelScheme1.DefaultChannelGuestRole,
channelScheme2.DefaultChannelGuestRole,
channelScheme1.DefaultChannelUserRole,
channelScheme2.DefaultChannelUserRole,
channelScheme1.DefaultChannelAdminRole,
channelScheme2.DefaultChannelAdminRole,
}
for _, roleName := range allRoleNames {
require.Contains(t, actualRoleNames, roleName)
}
})
})
}
func testRoleStoreChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest(t *testing.T, ss store.Store, s SqlStore) {
teamScheme := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme, err := ss.Scheme().Save(teamScheme)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme.Id)
channelScheme := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme, err = ss.Scheme().Save(channelScheme)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme.Id)
team := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme.Id,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team.Id)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme.Id,
}
channel, nErr := ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel.Id, 0)
channelSchemeUserRole, err := ss.Role().GetByName(context.Background(), channelScheme.DefaultChannelUserRole)
require.NoError(t, err)
channelSchemeUserRole.Permissions = []string{}
_, err = ss.Role().Save(channelSchemeUserRole)
require.NoError(t, err)
teamSchemeUserRole, err := ss.Role().GetByName(context.Background(), teamScheme.DefaultChannelUserRole)
require.NoError(t, err)
teamSchemeUserRole.Permissions = []string{model.PermissionUploadFile.Id}
_, err = ss.Role().Save(teamSchemeUserRole)
require.NoError(t, err)
// get the channel scheme user role again and ensure that it has the permission inherited from the team
// scheme user role
roleMapBefore, err := ss.Role().ChannelHigherScopedPermissions([]string{channelSchemeUserRole.Name})
require.NoError(t, err)
// blank-out the guest role to simulate an old team scheme, ensure it's blank
result, sqlErr := s.GetMasterX().Exec(fmt.Sprintf("UPDATE Schemes SET DefaultChannelGuestRole = '' WHERE Id = '%s'", teamScheme.Id))
require.NoError(t, sqlErr)
rows, serr := result.RowsAffected()
require.NoError(t, serr)
require.Equal(t, int64(1), rows)
teamScheme, err = ss.Scheme().Get(teamScheme.Id)
require.NoError(t, err)
require.Equal(t, "", teamScheme.DefaultChannelGuestRole)
// trigger a cache clear
_, err = ss.Role().Save(channelSchemeUserRole)
require.NoError(t, err)
roleMapAfter, err := ss.Role().ChannelHigherScopedPermissions([]string{channelSchemeUserRole.Name})
require.NoError(t, err)
require.Equal(t, len(roleMapBefore), len(roleMapAfter))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSchemeStore(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testSchemeStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testSchemeStoreGet(t, ss) })
t.Run("GetAllPage", func(t *testing.T) { testSchemeStoreGetAllPage(t, ss) })
t.Run("Delete", func(t *testing.T) { testSchemeStoreDelete(t, ss) })
t.Run("PermanentDeleteAll", func(t *testing.T) { testSchemeStorePermanentDeleteAll(t, ss) })
t.Run("GetByName", func(t *testing.T) { testSchemeStoreGetByName(t, ss) })
t.Run("CountByScope", func(t *testing.T) { testSchemeStoreCountByScope(t, ss) })
t.Run("CountWithoutPermission", func(t *testing.T) { testCountWithoutPermission(t, ss) })
}
func createDefaultRoles(ss store.Store) {
ss.Role().Save(&model.Role{
Name: model.TeamAdminRoleId,
DisplayName: model.TeamAdminRoleId,
Permissions: []string{
model.PermissionDeleteOthersPosts.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.TeamUserRoleId,
DisplayName: model.TeamUserRoleId,
Permissions: []string{
model.PermissionViewTeam.Id,
model.PermissionAddUserToTeam.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.TeamGuestRoleId,
DisplayName: model.TeamGuestRoleId,
Permissions: []string{
model.PermissionViewTeam.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelAdminRoleId,
DisplayName: model.ChannelAdminRoleId,
Permissions: []string{
model.PermissionManagePublicChannelMembers.Id,
model.PermissionManagePrivateChannelMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelUserRoleId,
DisplayName: model.ChannelUserRoleId,
Permissions: []string{
model.PermissionReadChannel.Id,
model.PermissionCreatePost.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelGuestRoleId,
DisplayName: model.ChannelGuestRoleId,
Permissions: []string{
model.PermissionReadChannel.Id,
model.PermissionCreatePost.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.PlaybookAdminRoleId,
DisplayName: model.PlaybookAdminRoleId,
Permissions: []string{
model.PermissionPrivatePlaybookManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.PlaybookMemberRoleId,
DisplayName: model.PlaybookMemberRoleId,
Permissions: []string{
model.PermissionPrivatePlaybookManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.RunAdminRoleId,
DisplayName: model.RunAdminRoleId,
Permissions: []string{
model.PermissionRunManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.RunMemberRoleId,
DisplayName: model.RunMemberRoleId,
Permissions: []string{
model.PermissionRunManageMembers.Id,
},
})
}
func testSchemeStoreSave(t *testing.T, ss store.Store) {
// Save a new scheme.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
// Check all fields saved correctly.
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, s1.DisplayName, d1.DisplayName)
assert.Equal(t, s1.Name, d1.Name)
assert.Equal(t, s1.Description, d1.Description)
assert.NotZero(t, d1.CreateAt)
assert.NotZero(t, d1.UpdateAt)
assert.Zero(t, d1.DeleteAt)
assert.Equal(t, s1.Scope, d1.Scope)
assert.Len(t, d1.DefaultTeamAdminRole, 26)
assert.Len(t, d1.DefaultTeamUserRole, 26)
assert.Len(t, d1.DefaultTeamGuestRole, 26)
assert.Len(t, d1.DefaultChannelAdminRole, 26)
assert.Len(t, d1.DefaultChannelUserRole, 26)
assert.Len(t, d1.DefaultChannelGuestRole, 26)
// Check the default roles were created correctly.
role1, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.Equal(t, role1.Permissions, []string{"delete_others_posts"})
assert.True(t, role1.SchemeManaged)
role2, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
assert.True(t, role2.SchemeManaged)
role3, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
assert.True(t, role3.SchemeManaged)
role4, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role4.SchemeManaged)
role5, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.Equal(t, role5.Permissions, []string{"view_team"})
assert.True(t, role5.SchemeManaged)
role6, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.Equal(t, role6.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role6.SchemeManaged)
// Change the scheme description and update.
d1.Description = model.NewId()
d2, err := ss.Scheme().Save(d1)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Try saving one with an invalid ID set.
s3 := &model.Scheme{
Id: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
_, err = ss.Scheme().Save(s3)
assert.Error(t, err)
}
func testSchemeStoreGet(t *testing.T, ss store.Store) {
// Save a scheme to test with.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid scheme
d2, err := ss.Scheme().Get(d1.Id)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Get an invalid scheme
_, err = ss.Scheme().Get(model.NewId())
assert.Error(t, err)
}
func testSchemeStoreGetByName(t *testing.T, ss store.Store) {
// Save a scheme to test with.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid scheme
d2, err := ss.Scheme().GetByName(d1.Name)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Get an invalid scheme
_, err = ss.Scheme().GetByName(model.NewId())
assert.Error(t, err)
}
func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) {
// Save a scheme to test with.
schemes := []*model.Scheme{
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
},
}
for _, scheme := range schemes {
_, err := ss.Scheme().Save(scheme)
require.NoError(t, err)
}
s1, err := ss.Scheme().GetAllPage("", 0, 2)
assert.NoError(t, err)
assert.Len(t, s1, 2)
s2, err := ss.Scheme().GetAllPage("", 2, 2)
assert.NoError(t, err)
assert.Len(t, s2, 2)
assert.NotEqual(t, s1[0].DisplayName, s2[0].DisplayName)
assert.NotEqual(t, s1[0].DisplayName, s2[1].DisplayName)
assert.NotEqual(t, s1[1].DisplayName, s2[0].DisplayName)
assert.NotEqual(t, s1[1].DisplayName, s2[1].DisplayName)
assert.NotEqual(t, s1[0].Name, s2[0].Name)
assert.NotEqual(t, s1[0].Name, s2[1].Name)
assert.NotEqual(t, s1[1].Name, s2[0].Name)
assert.NotEqual(t, s1[1].Name, s2[1].Name)
s3, err := ss.Scheme().GetAllPage("team", 0, 1000)
assert.NoError(t, err)
assert.NotZero(t, len(s3))
for _, s := range s3 {
assert.Equal(t, "team", s.Scope)
}
s4, err := ss.Scheme().GetAllPage("channel", 0, 1000)
assert.NoError(t, err)
assert.NotZero(t, len(s4))
for _, s := range s4 {
assert.Equal(t, "channel", s.Scope)
}
}
func testSchemeStoreDelete(t *testing.T, ss store.Store) {
// Save a new scheme.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
// Check all fields saved correctly.
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, s1.DisplayName, d1.DisplayName)
assert.Equal(t, s1.Name, d1.Name)
assert.Equal(t, s1.Description, d1.Description)
assert.NotZero(t, d1.CreateAt)
assert.NotZero(t, d1.UpdateAt)
assert.Zero(t, d1.DeleteAt)
assert.Equal(t, s1.Scope, d1.Scope)
assert.Len(t, d1.DefaultTeamAdminRole, 26)
assert.Len(t, d1.DefaultTeamUserRole, 26)
assert.Len(t, d1.DefaultTeamGuestRole, 26)
assert.Len(t, d1.DefaultChannelAdminRole, 26)
assert.Len(t, d1.DefaultChannelUserRole, 26)
assert.Len(t, d1.DefaultChannelGuestRole, 26)
// Check the default roles were created correctly.
role1, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.Equal(t, role1.Permissions, []string{"delete_others_posts"})
assert.True(t, role1.SchemeManaged)
role2, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
assert.True(t, role2.SchemeManaged)
role3, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
assert.True(t, role3.SchemeManaged)
role4, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role4.SchemeManaged)
role5, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.Equal(t, role5.Permissions, []string{"view_team"})
assert.True(t, role5.SchemeManaged)
role6, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.Equal(t, role6.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role6.SchemeManaged)
// Delete the scheme.
d2, err := ss.Scheme().Delete(d1.Id)
require.NoError(t, err)
assert.NotZero(t, d2.DeleteAt)
// Check that the roles are deleted too.
role7, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.NotZero(t, role7.DeleteAt)
role8, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.NotZero(t, role8.DeleteAt)
role9, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.NotZero(t, role9.DeleteAt)
role10, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.NotZero(t, role10.DeleteAt)
role11, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.NotZero(t, role11.DeleteAt)
role12, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.NotZero(t, role12.DeleteAt)
// Try deleting a scheme that does not exist.
_, err = ss.Scheme().Delete(model.NewId())
assert.Error(t, err)
// Try deleting a team scheme that's in use.
s4 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d4, err := ss.Scheme().Save(s4)
assert.NoError(t, err)
t4 := &model.Team{
Name: "xx" + model.NewId(),
DisplayName: model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &d4.Id,
}
t4, err = ss.Team().Save(t4)
require.NoError(t, err)
_, err = ss.Scheme().Delete(d4.Id)
assert.NoError(t, err)
t5, err := ss.Team().Get(t4.Id)
require.NoError(t, err)
assert.Equal(t, "", *t5.SchemeId)
// Try deleting a channel scheme that's in use.
s5 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
d5, err := ss.Scheme().Save(s5)
assert.NoError(t, err)
c5 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &d5.Id,
}
c5, nErr := ss.Channel().Save(c5, -1)
assert.NoError(t, nErr)
_, err = ss.Scheme().Delete(d5.Id)
assert.NoError(t, err)
c6, nErr := ss.Channel().Get(c5.Id, true)
assert.NoError(t, nErr)
assert.Equal(t, "", *c6.SchemeId)
}
func testSchemeStorePermanentDeleteAll(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
s2 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
err = ss.Scheme().PermanentDeleteAll()
assert.NoError(t, err)
_, err = ss.Scheme().Get(s1.Id)
assert.Error(t, err)
_, err = ss.Scheme().Get(s2.Id)
assert.Error(t, err)
schemes, err := ss.Scheme().GetAllPage("", 0, 100000)
assert.NoError(t, err)
assert.Empty(t, schemes)
}
func testSchemeStoreCountByScope(t *testing.T, ss store.Store) {
testCounts := func(expectedTeamCount, expectedChannelCount int) {
actualCount, err := ss.Scheme().CountByScope(model.SchemeScopeTeam)
require.NoError(t, err)
require.Equal(t, int64(expectedTeamCount), actualCount)
actualCount, err = ss.Scheme().CountByScope(model.SchemeScopeChannel)
require.NoError(t, err)
require.Equal(t, int64(expectedChannelCount), actualCount)
}
createScheme := func(scope string) {
_, err := ss.Scheme().Save(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: scope,
})
require.NoError(t, err)
}
err := ss.Scheme().PermanentDeleteAll()
require.NoError(t, err)
createScheme(model.SchemeScopeChannel)
createScheme(model.SchemeScopeTeam)
testCounts(1, 1)
createScheme(model.SchemeScopeTeam)
testCounts(2, 1)
createScheme(model.SchemeScopeChannel)
testCounts(2, 2)
}
func testCountWithoutPermission(t *testing.T, ss store.Store) {
perm := model.PermissionCreatePost.Id
createScheme := func(scope string) *model.Scheme {
scheme, err := ss.Scheme().Save(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: scope,
})
require.NoError(t, err)
return scheme
}
getRoles := func(scheme *model.Scheme) (channelUser, channelGuest *model.Role) {
var err error
channelUser, err = ss.Role().GetByName(context.Background(), scheme.DefaultChannelUserRole)
require.NoError(t, err)
require.NotNil(t, channelUser)
channelGuest, err = ss.Role().GetByName(context.Background(), scheme.DefaultChannelGuestRole)
require.NoError(t, err)
require.NotNil(t, channelGuest)
return
}
teamScheme1 := createScheme(model.SchemeScopeTeam)
defer ss.Scheme().Delete(teamScheme1.Id)
teamScheme2 := createScheme(model.SchemeScopeTeam)
defer ss.Scheme().Delete(teamScheme2.Id)
channelScheme1 := createScheme(model.SchemeScopeChannel)
defer ss.Scheme().Delete(channelScheme1.Id)
channelScheme2 := createScheme(model.SchemeScopeChannel)
defer ss.Scheme().Delete(channelScheme2.Id)
ts1User, ts1Guest := getRoles(teamScheme1)
ts2User, ts2Guest := getRoles(teamScheme2)
cs1User, cs1Guest := getRoles(channelScheme1)
cs2User, cs2Guest := getRoles(channelScheme2)
allRoles := []*model.Role{
ts1User,
ts1Guest,
ts2User,
ts2Guest,
cs1User,
cs1Guest,
cs2User,
cs2Guest,
}
teamUserCount, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(0), teamUserCount)
teamGuestCount, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(0), teamGuestCount)
var tests = []struct {
removePermissionFromRole *model.Role
expectTeamSchemeChannelUserCount int
expectTeamSchemeChannelGuestCount int
expectChannelSchemeChannelUserCount int
expectChannelSchemeChannelGuestCount int
}{
{ts1User, 1, 0, 0, 0},
{ts1Guest, 1, 1, 0, 0},
{ts2User, 2, 1, 0, 0},
{ts2Guest, 2, 2, 0, 0},
{cs1User, 2, 2, 1, 0},
{cs1Guest, 2, 2, 1, 1},
{cs2User, 2, 2, 2, 1},
{cs2Guest, 2, 2, 2, 2},
}
removePermission := func(targetRole *model.Role) {
roleMatched := false
for _, role := range allRoles {
if targetRole == role {
roleMatched = true
role.Permissions = []string{}
_, err = ss.Role().Save(role)
require.NoError(t, err)
}
}
require.True(t, roleMatched)
}
for _, test := range tests {
removePermission(test.removePermissionFromRole)
count, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(test.expectTeamSchemeChannelUserCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(test.expectTeamSchemeChannelGuestCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeChannel, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(test.expectChannelSchemeChannelUserCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeChannel, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(test.expectChannelSchemeChannelGuestCount), count)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
TenMinutes = 600000
)
func TestSessionStore(t *testing.T, ss store.Store) {
// Run serially to prevent interfering with other tests
testSessionCleanup(t, ss)
t.Run("Save", func(t *testing.T) { testSessionStoreSave(t, ss) })
t.Run("SessionGet", func(t *testing.T) { testSessionGet(t, ss) })
t.Run("SessionGetWithDeviceId", func(t *testing.T) { testSessionGetWithDeviceId(t, ss) })
t.Run("SessionRemove", func(t *testing.T) { testSessionRemove(t, ss) })
t.Run("SessionRemoveAll", func(t *testing.T) { testSessionRemoveAll(t, ss) })
t.Run("SessionRemoveByUser", func(t *testing.T) { testSessionRemoveByUser(t, ss) })
t.Run("SessionRemoveToken", func(t *testing.T) { testSessionRemoveToken(t, ss) })
t.Run("SessionUpdateDeviceId", func(t *testing.T) { testSessionUpdateDeviceId(t, ss) })
t.Run("SessionUpdateDeviceId2", func(t *testing.T) { testSessionUpdateDeviceId2(t, ss) })
t.Run("UpdateExpiresAt", func(t *testing.T) { testSessionStoreUpdateExpiresAt(t, ss) })
t.Run("UpdateLastActivityAt", func(t *testing.T) { testSessionStoreUpdateLastActivityAt(t, ss) })
t.Run("SessionCount", func(t *testing.T) { testSessionCount(t, ss) })
t.Run("GetSessionsExpired", func(t *testing.T) { testGetSessionsExpired(t, ss) })
t.Run("UpdateExpiredNotify", func(t *testing.T) { testUpdateExpiredNotify(t, ss) })
}
func testSessionStoreSave(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
_, err := ss.Session().Save(s1)
require.NoError(t, err)
}
func testSessionGet(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
_, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = s1.UserId
s3.ExpiresAt = 1
_, err = ss.Session().Save(s3)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
session.Props[model.SessionPropOs] = "linux"
session.Props[model.SessionPropBrowser] = "Chrome"
err = ss.Session().UpdateProps(session)
require.NoError(t, err)
session2, err := ss.Session().Get(context.Background(), session.Id)
require.NoError(t, err)
require.Equal(t, session.Props, session2.Props, "should match")
data, err := ss.Session().GetSessions(s1.UserId)
require.NoError(t, err)
require.Len(t, data, 3, "should match len")
}
func testSessionGetWithDeviceId(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = model.GetMillis() + 10000
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
s2.DeviceId = model.NewId()
s2.ExpiresAt = model.GetMillis() + 10000
_, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = s1.UserId
s3.ExpiresAt = 1
s3.DeviceId = model.NewId()
_, err = ss.Session().Save(s3)
require.NoError(t, err)
data, err := ss.Session().GetSessionsWithActiveDeviceIds(s1.UserId)
require.NoError(t, err)
require.Len(t, data, 1, "should match len")
}
func testSessionRemove(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().Remove(s1.Id)
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveAll(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().RemoveAllSessions()
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveByUser(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
deleteErr := ss.Session().PermanentDeleteSessionsByUser(s1.UserId)
require.NoError(t, deleteErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveToken(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().Remove(s1.Token)
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
data, err := ss.Session().GetSessions(s1.UserId)
require.NoError(t, err)
require.Empty(t, data, "should match len")
}
func testSessionUpdateDeviceId(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s1.Id, model.PushNotifyApple+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s2.Id, model.PushNotifyApple+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
}
func testSessionUpdateDeviceId2(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s1.Id, model.PushNotifyAppleReactNative+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s2.Id, model.PushNotifyAppleReactNative+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
}
func testSessionStoreUpdateExpiresAt(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
err = ss.Session().UpdateExpiresAt(s1.Id, 1234567890)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.EqualValues(t, session.ExpiresAt, 1234567890, "ExpiresAt not updated correctly")
}
func testSessionStoreUpdateLastActivityAt(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
err = ss.Session().UpdateLastActivityAt(s1.Id, 1234567890)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.EqualValues(t, session.LastActivityAt, 1234567890, "LastActivityAt not updated correctly")
}
func testSessionCount(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = model.GetMillis() + 100000
_, err := ss.Session().Save(s1)
require.NoError(t, err)
count, err := ss.Session().AnalyticsSessionCount()
require.NoError(t, err)
require.NotZero(t, count, "should have at least 1 session")
}
func testSessionCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = 0 // never expires
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
s2.ExpiresAt = now + 1000000 // expires in the future
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = model.NewId()
s3.ExpiresAt = 1 // expired
s3, err = ss.Session().Save(s3)
require.NoError(t, err)
s4 := &model.Session{}
s4.UserId = model.NewId()
s4.ExpiresAt = 2 // expired
s4, err = ss.Session().Save(s4)
require.NoError(t, err)
err = ss.Session().Cleanup(now, 1)
require.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s1.Id)
assert.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s2.Id)
assert.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s3.Id)
assert.Error(t, err)
_, err = ss.Session().Get(context.Background(), s4.Id)
assert.Error(t, err)
removeErr := ss.Session().Remove(s1.Id)
require.NoError(t, removeErr)
removeErr = ss.Session().Remove(s2.Id)
require.NoError(t, removeErr)
}
func testGetSessionsExpired(t *testing.T, ss store.Store) {
now := model.GetMillis()
// Clear existing sessions.
err := ss.Session().RemoveAllSessions()
require.NoError(t, err)
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.DeviceId = model.NewId()
s1.ExpiresAt = 0 // never expires
_, err = ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2.DeviceId = model.NewId()
s2.ExpiresAt = now - TenMinutes // expired within threshold
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = model.NewId()
s3.DeviceId = model.NewId()
s3.ExpiresAt = now - (TenMinutes * 100) // expired outside threshold
_, err = ss.Session().Save(s3)
require.NoError(t, err)
s4 := &model.Session{}
s4.UserId = model.NewId()
s4.ExpiresAt = now - TenMinutes // expired within threshold, but not mobile
s4, err = ss.Session().Save(s4)
require.NoError(t, err)
s5 := &model.Session{}
s5.UserId = model.NewId()
s5.DeviceId = model.NewId()
s5.ExpiresAt = now + (TenMinutes * 100000) // not expired
_, err = ss.Session().Save(s5)
require.NoError(t, err)
sessions, err := ss.Session().GetSessionsExpired(TenMinutes*2, true, true) // mobile only
require.NoError(t, err)
require.Len(t, sessions, 1)
require.Equal(t, s2.Id, sessions[0].Id)
sessions, err = ss.Session().GetSessionsExpired(TenMinutes*2, false, true) // all client types
require.NoError(t, err)
require.Len(t, sessions, 2)
expected := []string{s2.Id, s4.Id}
for _, sess := range sessions {
require.Contains(t, expected, sess.Id)
}
}
func testUpdateExpiredNotify(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.DeviceId = model.NewId()
s1.ExpiresAt = model.GetMillis() + TenMinutes
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.False(t, session.ExpiredNotify)
err = ss.Session().UpdateExpiredNotify(session.Id, true)
require.NoError(t, err)
session, err = ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.True(t, session.ExpiredNotify)
err = ss.Session().UpdateExpiredNotify(session.Id, false)
require.NoError(t, err)
session, err = ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.False(t, session.ExpiredNotify)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"flag"
"fmt"
"net/url"
"os"
"path"
"strings"
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defaultMysqlDSN = "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true"
defaultPostgresqlDSN = "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"
defaultMysqlRootPWD = "mostest"
defaultMysqlReplicaDSN = "root:mostest@tcp(localhost:3307)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s"
)
func getEnv(name, defaultValue string) string {
if value := os.Getenv(name); value != "" {
return value
}
return defaultValue
}
func log(message string) {
verbose := false
if verboseFlag := flag.Lookup("test.v"); verboseFlag != nil {
verbose = verboseFlag.Value.String() != ""
}
if verboseFlag := flag.Lookup("v"); verboseFlag != nil {
verbose = verboseFlag.Value.String() != ""
}
if verbose {
fmt.Println(message)
}
}
func getDefaultMysqlDSN() string {
if os.Getenv("IS_CI") == "true" {
return strings.ReplaceAll(defaultMysqlDSN, "localhost", "mysql")
}
return defaultMysqlDSN
}
func getDefaultPostgresqlDSN() string {
if os.Getenv("IS_CI") == "true" {
return strings.ReplaceAll(defaultPostgresqlDSN, "localhost", "postgres")
}
return defaultPostgresqlDSN
}
// MySQLSettings returns the database settings to connect to the MySQL unittesting database.
// The database name is generated randomly and must be created before use.
func MySQLSettings(withReplica bool) *model.SqlSettings {
dsn := os.Getenv("TEST_DATABASE_MYSQL_DSN")
if dsn == "" {
dsn = getDefaultMysqlDSN()
mlog.Info("No TEST_DATABASE_MYSQL_DSN override, using default", mlog.String("default_dsn", dsn))
} else {
mlog.Info("Using TEST_DATABASE_MYSQL_DSN override", mlog.String("dsn", dsn))
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.DBName = "db" + model.NewId()
mySQLSettings := databaseSettings("mysql", cfg.FormatDSN())
if withReplica {
mySQLSettings.DataSourceReplicas = []string{getEnv("TEST_DATABASE_MYSQL_REPLICA_DSN", defaultMysqlReplicaDSN)}
}
return mySQLSettings
}
// PostgresSQLSettings returns the database settings to connect to the PostgreSQL unittesting database.
// The database name is generated randomly and must be created before use.
func PostgreSQLSettings() *model.SqlSettings {
dsn := os.Getenv("TEST_DATABASE_POSTGRESQL_DSN")
if dsn == "" {
dsn = getDefaultPostgresqlDSN()
mlog.Info("No TEST_DATABASE_POSTGRESQL_DSN override, using default", mlog.String("default_dsn", dsn))
} else {
mlog.Info("Using TEST_DATABASE_POSTGRESQL_DSN override", mlog.String("dsn", dsn))
}
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
// Generate a random database name
dsnURL.Path = "db" + model.NewId()
return databaseSettings("postgres", dsnURL.String())
}
func mySQLRootDSN(dsn string) string {
rootPwd := getEnv("TEST_DATABASE_MYSQL_ROOT_PASSWD", defaultMysqlRootPWD)
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.User = "root"
cfg.Passwd = rootPwd
cfg.DBName = "mysql"
return cfg.FormatDSN()
}
func postgreSQLRootDSN(dsn string) string {
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
// // Assume the unittesting database has the same password.
// password := ""
// if dsnUrl.User != nil {
// password, _ = dsnUrl.User.Password()
// }
// dsnUrl.User = url.UserPassword("", password)
dsnURL.Path = "postgres"
return dsnURL.String()
}
func mySQLDSNDatabase(dsn string) string {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
return cfg.DBName
}
func postgreSQLDSNDatabase(dsn string) string {
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
return path.Base(dsnURL.Path)
}
func databaseSettings(driver, dataSource string) *model.SqlSettings {
settings := &model.SqlSettings{
DriverName: &driver,
DataSource: &dataSource,
DataSourceReplicas: []string{},
DataSourceSearchReplicas: []string{},
MaxIdleConns: new(int),
ConnMaxLifetimeMilliseconds: new(int),
ConnMaxIdleTimeMilliseconds: new(int),
MaxOpenConns: new(int),
Trace: model.NewBool(false),
AtRestEncryptKey: model.NewString(model.NewRandomString(32)),
QueryTimeout: new(int),
MigrationsStatementTimeoutSeconds: new(int),
}
*settings.MaxIdleConns = 10
*settings.ConnMaxLifetimeMilliseconds = 3600000
*settings.ConnMaxIdleTimeMilliseconds = 300000
*settings.MaxOpenConns = 100
*settings.QueryTimeout = 60
*settings.MigrationsStatementTimeoutSeconds = 10
return settings
}
// execAsRoot executes the given sql as root against the testing database
func execAsRoot(settings *model.SqlSettings, sqlCommand string) error {
var dsn string
var driver = *settings.DriverName
switch driver {
case model.DatabaseDriverMysql:
dsn = mySQLRootDSN(*settings.DataSource)
case model.DatabaseDriverPostgres:
dsn = postgreSQLRootDSN(*settings.DataSource)
default:
return fmt.Errorf("unsupported driver %s", driver)
}
db, err := sql.Open(driver, dsn)
if err != nil {
return errors.Wrapf(err, "failed to connect to %s database as root", driver)
}
defer db.Close()
if _, err = db.Exec(sqlCommand); err != nil {
return errors.Wrapf(err, "failed to execute `%s` against %s database as root", sqlCommand, driver)
}
return nil
}
func replaceMySQLDatabaseName(dsn, newDBName string) string {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.DBName = newDBName
return cfg.FormatDSN()
}
// MakeSqlSettings creates a randomly named database and returns the corresponding sql settings
func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings {
var settings *model.SqlSettings
var dbName string
switch driver {
case model.DatabaseDriverMysql:
settings = MySQLSettings(withReplica)
dbName = mySQLDSNDatabase(*settings.DataSource)
newDSRs := []string{}
for _, dataSource := range settings.DataSourceReplicas {
newDSRs = append(newDSRs, replaceMySQLDatabaseName(dataSource, dbName))
}
settings.DataSourceReplicas = newDSRs
case model.DatabaseDriverPostgres:
settings = PostgreSQLSettings()
dbName = postgreSQLDSNDatabase(*settings.DataSource)
default:
panic("unsupported driver " + driver)
}
if err := execAsRoot(settings, "CREATE DATABASE "+dbName); err != nil {
panic("failed to create temporary database " + dbName + ": " + err.Error())
}
switch driver {
case model.DatabaseDriverMysql:
if err := execAsRoot(settings, "GRANT ALL PRIVILEGES ON "+dbName+".* TO 'mmuser'"); err != nil {
panic("failed to grant mmuser permission to " + dbName + ":" + err.Error())
}
case model.DatabaseDriverPostgres:
if err := execAsRoot(settings, "GRANT ALL PRIVILEGES ON DATABASE \""+dbName+"\" TO mmuser"); err != nil {
panic("failed to grant mmuser permission to " + dbName + ":" + err.Error())
}
default:
panic("unsupported driver " + driver)
}
log("Created temporary " + driver + " database " + dbName)
return settings
}
func CleanupSqlSettings(settings *model.SqlSettings) {
var driver = *settings.DriverName
var dbName string
switch driver {
case model.DatabaseDriverMysql:
dbName = mySQLDSNDatabase(*settings.DataSource)
case model.DatabaseDriverPostgres:
dbName = postgreSQLDSNDatabase(*settings.DataSource)
default:
panic("unsupported driver " + driver)
}
if err := execAsRoot(settings, "DROP DATABASE "+dbName); err != nil {
panic("failed to drop temporary database " + dbName + ": " + err.Error())
}
log("Dropped temporary database " + dbName)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSharedChannelStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveSharedChannel", func(t *testing.T) { testSaveSharedChannel(t, ss) })
t.Run("GetSharedChannel", func(t *testing.T) { testGetSharedChannel(t, ss) })
t.Run("HasSharedChannel", func(t *testing.T) { testHasSharedChannel(t, ss) })
t.Run("GetSharedChannels", func(t *testing.T) { testGetSharedChannels(t, ss) })
t.Run("UpdateSharedChannel", func(t *testing.T) { testUpdateSharedChannel(t, ss) })
t.Run("DeleteSharedChannel", func(t *testing.T) { testDeleteSharedChannel(t, ss) })
t.Run("SaveSharedChannelRemote", func(t *testing.T) { testSaveSharedChannelRemote(t, ss) })
t.Run("UpdateSharedChannelRemote", func(t *testing.T) { testUpdateSharedChannelRemote(t, ss) })
t.Run("GetSharedChannelRemote", func(t *testing.T) { testGetSharedChannelRemote(t, ss) })
t.Run("GetSharedChannelRemoteByIds", func(t *testing.T) { testGetSharedChannelRemoteByIds(t, ss) })
t.Run("GetSharedChannelRemotes", func(t *testing.T) { testGetSharedChannelRemotes(t, ss) })
t.Run("HasRemote", func(t *testing.T) { testHasRemote(t, ss) })
t.Run("GetRemoteForUser", func(t *testing.T) { testGetRemoteForUser(t, ss) })
t.Run("UpdateSharedChannelRemoteNextSyncAt", func(t *testing.T) { testUpdateSharedChannelRemoteCursor(t, ss) })
t.Run("DeleteSharedChannelRemote", func(t *testing.T) { testDeleteSharedChannelRemote(t, ss) })
t.Run("SaveSharedChannelUser", func(t *testing.T) { testSaveSharedChannelUser(t, ss) })
t.Run("GetSharedChannelSingleUser", func(t *testing.T) { testGetSingleSharedChannelUser(t, ss) })
t.Run("GetSharedChannelUser", func(t *testing.T) { testGetSharedChannelUser(t, ss) })
t.Run("GetSharedChannelUsersForSync", func(t *testing.T) { testGetSharedChannelUsersForSync(t, ss) })
t.Run("UpdateSharedChannelUserLastSyncAt", func(t *testing.T) { testUpdateSharedChannelUserLastSyncAt(t, ss) })
t.Run("SaveSharedChannelAttachment", func(t *testing.T) { testSaveSharedChannelAttachment(t, ss) })
t.Run("UpsertSharedChannelAttachment", func(t *testing.T) { testUpsertSharedChannelAttachment(t, ss) })
t.Run("GetSharedChannelAttachment", func(t *testing.T) { testGetSharedChannelAttachment(t, ss) })
t.Run("UpdateSharedChannelAttachmentLastSyncAt", func(t *testing.T) { testUpdateSharedChannelAttachmentLastSyncAt(t, ss) })
}
func testSaveSharedChannel(t *testing.T, ss store.Store) {
t.Run("Save shared channel (home)", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel")
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
// ensure channel's Shared flag is set
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.True(t, channelMod.IsShared())
})
t.Run("Save shared channel (remote)", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save2")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
// ensure channel's Shared flag is set
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.True(t, channelMod.IsShared())
})
t.Run("Save invalid shared channel", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: "",
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
_, err := ss.SharedChannel().Save(sc)
require.Error(t, err, "should error saving invalid shared channel", err)
})
t.Run("Save with invalid channel id", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: model.NewId(),
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().Save(sc)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testGetSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_get")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Get existing shared channel", func(t *testing.T) {
sc, err := ss.SharedChannel().Get(scSaved.ChannelId)
require.NoError(t, err, "couldn't get shared channel", err)
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
})
t.Run("Get non-existent shared channel", func(t *testing.T) {
sc, err := ss.SharedChannel().Get(model.NewId())
require.Error(t, err)
require.Nil(t, sc)
})
}
func testHasSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_get")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Get existing shared channel", func(t *testing.T) {
exists, err := ss.SharedChannel().HasChannel(scSaved.ChannelId)
require.NoError(t, err, "couldn't get shared channel", err)
assert.True(t, exists)
})
t.Run("Get non-existent shared channel", func(t *testing.T) {
exists, err := ss.SharedChannel().HasChannel(model.NewId())
require.NoError(t, err)
assert.False(t, exists)
})
}
func testGetSharedChannels(t *testing.T, ss store.Store) {
require.NoError(t, clearSharedChannels(ss))
user, err := createTestUser(ss, "gary.goodspeed")
require.NoError(t, err)
creator := model.NewId()
team1 := model.NewId()
team2 := model.NewId()
rid := model.NewId()
data := []model.SharedChannel{
{CreatorId: creator, TeamId: team1, ShareName: "test1", Home: true},
{CreatorId: creator, TeamId: team1, ShareName: "test2", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team1, ShareName: "test3", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team1, ShareName: "test4", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test5", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test6", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team2, ShareName: "test7", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team2, ShareName: "test8", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test9", Home: true},
}
for i, sc := range data {
channel, err := createTestChannelWithUser(ss, "test_get2_"+strconv.Itoa(i), user)
require.NoError(t, err)
sc.ChannelId = channel.Id
_, err = ss.SharedChannel().Save(&sc)
require.NoError(t, err, "error saving shared channel")
}
t.Run("Get shared channels home only", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeRemote: true,
CreatorId: creator,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
home, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(home))
require.Len(t, home, 5, "should be 5 home channels")
for _, sc := range home {
require.True(t, sc.Home, "should be home channel")
}
})
t.Run("Get shared channels remote only", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeHome: true,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 remote channels")
for _, sc := range remotes {
require.False(t, sc.Home, "should be remote channel")
}
})
t.Run("Get shared channels bad opts", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeHome: true,
ExcludeRemote: true,
}
_, err := ss.SharedChannel().GetAll(0, 100, opts)
require.Error(t, err, "error expected")
})
t.Run("Get shared channels by team", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 matching channels")
for _, sc := range remotes {
require.Equal(t, team1, sc.TeamId)
}
})
t.Run("Get shared channels invalid pagination", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
}
_, err := ss.SharedChannel().GetAll(-1, 100, opts)
require.Error(t, err)
_, err = ss.SharedChannel().GetAll(0, -100, opts)
require.Error(t, err)
})
t.Run("Get shared channels for member", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
MemberId: user.Id,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 matching channels")
for _, sc := range remotes {
require.Equal(t, team1, sc.TeamId)
}
})
t.Run("Get shared channels for non-member", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
MemberId: model.NewId(),
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 0, "should be 0 matching channels")
})
}
func testUpdateSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_update")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Update existing shared channel", func(t *testing.T) {
id := model.NewId()
scMod := scSaved // copy struct (contains basic types only)
scMod.ShareName = "newname"
scMod.ShareDisplayName = "For testing"
scMod.ShareHeader = "This is a header."
scMod.RemoteId = id
scUpdated, err := ss.SharedChannel().Update(scMod)
require.NoError(t, err, "couldn't update shared channel", err)
require.Equal(t, "newname", scUpdated.ShareName)
require.Equal(t, "For testing", scUpdated.ShareDisplayName)
require.Equal(t, "This is a header.", scUpdated.ShareHeader)
require.Equal(t, id, scUpdated.RemoteId)
})
t.Run("Update non-existent shared channel", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: model.NewId(),
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "missingshare",
}
_, err := ss.SharedChannel().Update(sc)
require.Error(t, err, "should error when updating non-existent shared channel", err)
})
}
func testDeleteSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_delete")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
_, err = ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
// add some remotes
for i := 0; i < 10; i++ {
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't add remote", err)
}
t.Run("Delete existing shared channel", func(t *testing.T) {
deleted, err := ss.SharedChannel().Delete(channel.Id)
require.NoError(t, err, "delete existing shared channel should not error", err)
require.True(t, deleted, "expected true from delete shared channel")
sc, err := ss.SharedChannel().Get(channel.Id)
require.Error(t, err)
require.Nil(t, sc)
// make sure the remotes were deleted.
remotes, err := ss.SharedChannel().GetRemotes(model.SharedChannelRemoteFilterOpts{ChannelId: channel.Id})
require.NoError(t, err)
require.Len(t, remotes, 0, "expected empty remotes list")
// ensure channel's Shared flag is unset
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.False(t, channelMod.IsShared())
})
t.Run("Delete non-existent shared channel", func(t *testing.T) {
deleted, err := ss.SharedChannel().Delete(model.NewId())
require.NoError(t, err, "delete non-existent shared channel should not error", err)
require.False(t, deleted, "expected false from delete shared channel")
})
}
func testSaveSharedChannelRemote(t *testing.T, ss store.Store) {
t.Run("Save shared channel remote", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save_remote")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save shared channel remote", err)
require.Equal(t, remote.ChannelId, remoteSaved.ChannelId)
require.Equal(t, remote.CreatorId, remoteSaved.CreatorId)
})
t.Run("Save invalid shared channel remote", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: "",
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.Error(t, err, "should error saving invalid remote", err)
})
t.Run("Save shared channel remote with invalid channel id", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: model.NewId(),
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testUpdateSharedChannelRemote(t *testing.T, ss store.Store) {
t.Run("Update shared channel remote", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_update_remote")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save shared channel remote", err)
remoteSaved.IsInviteAccepted = true
remoteSaved.IsInviteConfirmed = true
remoteUpdated, err := ss.SharedChannel().UpdateRemote(remoteSaved)
require.NoError(t, err, "couldn't update shared channel remote", err)
require.Equal(t, true, remoteUpdated.IsInviteAccepted)
require.Equal(t, true, remoteUpdated.IsInviteConfirmed)
})
t.Run("Update invalid shared channel remote", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: "",
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpdateRemote(remote)
require.Error(t, err, "should error updating invalid remote", err)
})
t.Run("Update shared channel remote with invalid channel id", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: model.NewId(),
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpdateRemote(remote)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testGetSharedChannelRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_get")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
t.Run("Get existing shared channel remote", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.NoError(t, err, "could not get shared channel remote", err)
require.Equal(t, remoteSaved.Id, r.Id)
require.Equal(t, remoteSaved.ChannelId, r.ChannelId)
require.Equal(t, remoteSaved.CreatorId, r.CreatorId)
require.Equal(t, remoteSaved.RemoteId, r.RemoteId)
})
t.Run("Get non-existent shared channel remote", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemote(model.NewId())
require.Error(t, err)
require.Nil(t, r)
})
}
func testGetSharedChannelRemoteByIds(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_get_by_ids")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "could not save remote", err)
t.Run("Get existing shared channel remote by ids", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemoteByIds(remoteSaved.ChannelId, remoteSaved.RemoteId)
require.NoError(t, err, "couldn't get shared channel remote by ids", err)
require.Equal(t, remoteSaved.Id, r.Id)
require.Equal(t, remoteSaved.ChannelId, r.ChannelId)
require.Equal(t, remoteSaved.CreatorId, r.CreatorId)
require.Equal(t, remoteSaved.RemoteId, r.RemoteId)
})
t.Run("Get non-existent shared channel remote by ids", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemoteByIds(model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, r)
})
}
func testGetSharedChannelRemotes(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remotes_get2")
require.NoError(t, err)
creator := model.NewId()
remoteId := model.NewId()
data := []model.SharedChannelRemote{
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId},
}
for i, r := range data {
if r.ChannelId == "" {
c, err := createTestChannel(ss, "test_remotes_get2_"+strconv.Itoa(i))
require.NoError(t, err)
r.ChannelId = c.Id
}
_, err := ss.SharedChannel().SaveRemote(&r)
require.NoError(t, err, "error saving shared channel remote")
}
t.Run("Get shared channel remotes by channel_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
ChannelId: channel.Id,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 3)
for _, r := range remotes {
require.Equal(t, channel.Id, r.ChannelId)
}
})
t.Run("Get shared channel remotes by invalid channel_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
ChannelId: model.NewId(),
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 0)
})
t.Run("Get shared channel remotes by remote_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: remoteId,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 2) // only confirmed invitations
for _, r := range remotes {
require.Equal(t, remoteId, r.RemoteId)
require.True(t, r.IsInviteConfirmed)
}
})
t.Run("Get shared channel remotes by invalid remote_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: model.NewId(),
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 0)
})
t.Run("Get shared channel remotes by remote_id including unconfirmed", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: remoteId,
InclUnconfirmed: true,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 3)
for _, r := range remotes {
require.Equal(t, remoteId, r.RemoteId)
}
})
}
func testHasRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remotes_get2")
require.NoError(t, err)
remote1 := model.NewId()
remote2 := model.NewId()
creator := model.NewId()
data := []model.SharedChannelRemote{
{ChannelId: channel.Id, CreatorId: creator, RemoteId: remote1},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: remote2},
}
for _, r := range data {
_, err := ss.SharedChannel().SaveRemote(&r)
require.NoError(t, err, "error saving shared channel remote")
}
t.Run("has remote", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(channel.Id, remote1)
require.NoError(t, err)
assert.True(t, has)
has, err = ss.SharedChannel().HasRemote(channel.Id, remote2)
require.NoError(t, err)
assert.True(t, has)
})
t.Run("wrong channel id ", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(model.NewId(), remote1)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("wrong remote id", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(channel.Id, model.NewId())
require.NoError(t, err)
assert.False(t, has)
})
}
func testGetRemoteForUser(t *testing.T, ss store.Store) {
// add remotes, and users to simulated shared channels.
teamId := model.NewId()
channel, err := createSharedTestChannel(ss, "share_test_channel", true, nil)
require.NoError(t, err)
remotes := []*model.RemoteCluster{
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_1"},
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_2"},
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_3"},
}
for _, rc := range remotes {
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
scr := &model.SharedChannelRemote{Id: model.NewId(), CreatorId: rc.CreatorId, ChannelId: channel.Id, RemoteId: rc.RemoteId}
_, err = ss.SharedChannel().SaveRemote(scr)
require.NoError(t, err)
}
users := []string{model.NewId(), model.NewId(), model.NewId()}
for _, id := range users {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
}
_, err := ss.Channel().SaveMember(member)
require.NoError(t, err)
}
t.Run("user is member", func(t *testing.T) {
for _, rc := range remotes {
for _, userId := range users {
rcFound, err := ss.SharedChannel().GetRemoteForUser(rc.RemoteId, userId)
assert.NoError(t, err, "remote should be found for user")
assert.Equal(t, rc.RemoteId, rcFound.RemoteId, "remoteIds should match")
}
}
})
t.Run("user is not a member", func(t *testing.T) {
for _, rc := range remotes {
rcFound, err := ss.SharedChannel().GetRemoteForUser(rc.RemoteId, model.NewId())
assert.Error(t, err, "remote should not be found for user")
assert.Nil(t, rcFound)
}
})
t.Run("unknown remote id", func(t *testing.T) {
rcFound, err := ss.SharedChannel().GetRemoteForUser(model.NewId(), users[0])
assert.Error(t, err, "remote should not be found for unknown remote id")
assert.Nil(t, rcFound)
})
}
func testUpdateSharedChannelRemoteCursor(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_update_next_sync_at")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
future := model.GetMillis() + 3600000 // 1 hour in the future
postID := model.NewId()
cursor := model.GetPostsSinceForSyncCursor{
LastPostUpdateAt: future,
LastPostId: postID,
}
t.Run("Update NextSyncAt for remote", func(t *testing.T) {
err := ss.SharedChannel().UpdateRemoteCursor(remoteSaved.Id, cursor)
require.NoError(t, err, "update NextSyncAt should not error", err)
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.NoError(t, err)
require.Equal(t, future, r.LastPostUpdateAt)
require.Equal(t, postID, r.LastPostId)
})
t.Run("Update NextSyncAt for non-existent shared channel remote", func(t *testing.T) {
err := ss.SharedChannel().UpdateRemoteCursor(model.NewId(), cursor)
require.Error(t, err, "update non-existent remote should error", err)
})
}
func testDeleteSharedChannelRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_delete")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
t.Run("Delete existing shared channel remote", func(t *testing.T) {
deleted, err := ss.SharedChannel().DeleteRemote(remoteSaved.Id)
require.NoError(t, err, "delete existing remote should not error", err)
require.True(t, deleted, "expected true from delete remote")
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.Error(t, err)
require.Nil(t, r)
})
t.Run("Delete non-existent shared channel remote", func(t *testing.T) {
deleted, err := ss.SharedChannel().DeleteRemote(model.NewId())
require.NoError(t, err, "delete non-existent remote should not error", err)
require.False(t, deleted, "expected false from delete remote")
})
}
func createTestUser(ss store.Store, username string) (*model.User, error) {
user := &model.User{
Username: username,
Email: "gary@example.com",
}
return ss.User().Save(user)
}
func createTestChannel(ss store.Store, name string) (*model.Channel, error) {
channel, err := createSharedTestChannel(ss, name, false, nil)
return channel, err
}
func createTestChannelWithUser(ss store.Store, name string, member *model.User) (*model.Channel, error) {
channel, err := createSharedTestChannel(ss, name, false, member)
return channel, err
}
func createSharedTestChannel(ss store.Store, name string, shared bool, member *model.User) (*model.Channel, error) {
channel := &model.Channel{
TeamId: model.NewId(),
Type: model.ChannelTypeOpen,
Name: name,
DisplayName: name + " display name",
Header: name + " header",
Purpose: name + "purpose",
CreatorId: model.NewId(),
Shared: model.NewBool(shared),
}
channel, err := ss.Channel().Save(channel, 10000)
if err != nil {
return nil, err
}
if member != nil {
newMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: member.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: member.IsGuest(),
SchemeUser: !member.IsGuest(),
}
_, err = ss.Channel().SaveMember(newMember)
if err != nil {
return nil, err
}
}
if shared {
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: channel.CreatorId,
ShareName: channel.Name,
Home: true,
}
_, err = ss.SharedChannel().Save(sc)
if err != nil {
return nil, err
}
}
return channel, nil
}
func clearSharedChannels(ss store.Store) error {
opts := model.SharedChannelFilterOpts{}
all, err := ss.SharedChannel().GetAll(0, 1000, opts)
if err != nil {
return err
}
for _, sc := range all {
if _, err := ss.SharedChannel().Delete(sc.ChannelId); err != nil {
return err
}
}
return nil
}
func testSaveSharedChannelUser(t *testing.T, ss store.Store) {
t.Run("Save shared channel user", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
userSaved, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "couldn't save shared channel user", err)
require.Equal(t, scUser.UserId, userSaved.UserId)
require.Equal(t, scUser.RemoteId, userSaved.RemoteId)
})
t.Run("Save invalid shared channel user", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: "",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.Error(t, err, "should error saving invalid user", err)
})
t.Run("Save shared channel user with invalid remote id", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: "bogus",
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.Error(t, err, "expected error for invalid remote id")
})
}
func testGetSingleSharedChannelUser(t *testing.T, ss store.Store) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
userSaved, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "could not save user", err)
t.Run("Get existing shared channel user", func(t *testing.T) {
r, err := ss.SharedChannel().GetSingleUser(userSaved.UserId, userSaved.ChannelId, userSaved.RemoteId)
require.NoError(t, err, "couldn't get shared channel user", err)
require.Equal(t, userSaved.Id, r.Id)
require.Equal(t, userSaved.UserId, r.UserId)
require.Equal(t, userSaved.RemoteId, r.RemoteId)
require.Equal(t, userSaved.CreateAt, r.CreateAt)
})
t.Run("Get non-existent shared channel user", func(t *testing.T) {
u, err := ss.SharedChannel().GetSingleUser(model.NewId(), model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, u)
})
}
func testGetSharedChannelUser(t *testing.T, ss store.Store) {
userId := model.NewId()
for i := 0; i < 10; i++ {
scUser := &model.SharedChannelUser{
UserId: userId,
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "could not save user", err)
}
t.Run("Get existing shared channel user", func(t *testing.T) {
scus, err := ss.SharedChannel().GetUsersForUser(userId)
require.NoError(t, err, "couldn't get shared channel user", err)
require.Len(t, scus, 10, "should be 10 shared channel user records")
require.Equal(t, userId, scus[0].UserId)
})
t.Run("Get non-existent shared channel user", func(t *testing.T) {
scus, err := ss.SharedChannel().GetUsersForUser(model.NewId())
require.NoError(t, err, "should not error when not found")
require.Empty(t, scus, "should be empty")
})
}
func testGetSharedChannelUsersForSync(t *testing.T, ss store.Store) {
channelID := model.NewId()
remoteID := model.NewId()
earlier := model.GetMillis() - 300000
later := model.GetMillis() + 300000
var users []*model.User
for i := 0; i < 10; i++ { // need real users
u := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis(),
}
u, err := ss.User().Save(u)
require.NoError(t, err)
users = append(users, u)
}
data := []model.SharedChannelUser{
{UserId: users[0].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[1].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[1].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[1].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: later},
{UserId: users[2].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[3].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[4].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[5].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: earlier},
{UserId: users[6].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: later},
}
for i, u := range data {
scu := &model.SharedChannelUser{
UserId: u.UserId,
ChannelId: u.ChannelId,
RemoteId: u.RemoteId,
LastSyncAt: u.LastSyncAt,
}
_, err := ss.SharedChannel().SaveUser(scu)
require.NoError(t, err, "could not save user #", i, err)
}
t.Run("Filter by channelId", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: false,
ChannelID: channelID,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 2)
for _, user := range usersFound {
require.Contains(t, []string{users[3].Id, users[5].Id}, user.Id)
}
})
t.Run("Filter by channelId for profile image", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: true,
ChannelID: channelID,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 2)
for _, user := range usersFound {
require.Contains(t, []string{users[3].Id, users[5].Id}, user.Id)
}
})
t.Run("Filter by channelId with Limit", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: true,
ChannelID: channelID,
Limit: 1,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 1)
})
}
func testUpdateSharedChannelUserLastSyncAt(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis() - 300000, // 5 mins
}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis() + 300000,
}
u2, err = ss.User().Save(u2)
require.NoError(t, err)
channelID := model.NewId()
remoteID := model.NewId()
scUser1 := &model.SharedChannelUser{
UserId: u1.Id,
RemoteId: remoteID,
ChannelId: channelID,
}
_, err = ss.SharedChannel().SaveUser(scUser1)
require.NoError(t, err, "couldn't save user", err)
scUser2 := &model.SharedChannelUser{
UserId: u2.Id,
RemoteId: remoteID,
ChannelId: channelID,
}
_, err = ss.SharedChannel().SaveUser(scUser2)
require.NoError(t, err, "couldn't save user", err)
t.Run("Update LastSyncAt for user via UpdateAt", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(u1.Id, channelID, remoteID)
require.NoError(t, err, "updateLastSyncAt should not error", err)
scu, err := ss.SharedChannel().GetSingleUser(u1.Id, channelID, remoteID)
require.NoError(t, err)
require.Equal(t, u1.UpdateAt, scu.LastSyncAt)
})
t.Run("Update LastSyncAt for user via LastPictureUpdate", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(u2.Id, channelID, remoteID)
require.NoError(t, err, "updateLastSyncAt should not error", err)
scu, err := ss.SharedChannel().GetSingleUser(u2.Id, channelID, remoteID)
require.NoError(t, err)
require.Equal(t, u2.LastPictureUpdate, scu.LastSyncAt)
})
t.Run("Update LastSyncAt for non-existent shared channel user", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(model.NewId(), channelID, remoteID)
require.Error(t, err, "update non-existent user should error", err)
})
}
func testSaveSharedChannelAttachment(t *testing.T, ss store.Store) {
t.Run("Save shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save shared channel attachment", err)
require.Equal(t, attachment.FileId, saved.FileId)
require.Equal(t, attachment.RemoteId, saved.RemoteId)
})
t.Run("Save invalid shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: "",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveAttachment(attachment)
require.Error(t, err, "should error saving invalid attachment", err)
})
t.Run("Save shared channel attachment with invalid remote id", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: "bogus",
}
_, err := ss.SharedChannel().SaveAttachment(attachment)
require.Error(t, err, "expected error for invalid remote id")
})
}
func testUpsertSharedChannelAttachment(t *testing.T, ss store.Store) {
t.Run("Upsert new shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpsertAttachment(attachment)
require.NoError(t, err, "couldn't upsert shared channel attachment", err)
saved, err := ss.SharedChannel().GetAttachment(attachment.FileId, attachment.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.NotZero(t, saved.CreateAt)
require.Equal(t, saved.CreateAt, saved.LastSyncAt)
})
t.Run("Upsert existing shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save shared channel attachment", err)
// make sure enough time passed that GetMillis returns a different value
time.Sleep(2 * time.Millisecond)
_, err = ss.SharedChannel().UpsertAttachment(saved)
require.NoError(t, err, "couldn't upsert shared channel attachment", err)
updated, err := ss.SharedChannel().GetAttachment(attachment.FileId, attachment.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.NotZero(t, updated.CreateAt)
require.Greater(t, updated.LastSyncAt, updated.CreateAt)
})
t.Run("Upsert invalid shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: "",
RemoteId: model.NewId(),
}
id, err := ss.SharedChannel().UpsertAttachment(attachment)
require.Error(t, err, "should error upserting invalid attachment", err)
require.Empty(t, id)
})
t.Run("Upsert shared channel attachment with invalid remote id", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: "bogus",
}
id, err := ss.SharedChannel().UpsertAttachment(attachment)
require.Error(t, err, "expected error for invalid remote id")
require.Empty(t, id)
})
}
func testGetSharedChannelAttachment(t *testing.T, ss store.Store) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "could not save attachment", err)
t.Run("Get existing shared channel attachment", func(t *testing.T) {
r, err := ss.SharedChannel().GetAttachment(saved.FileId, saved.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.Equal(t, saved.Id, r.Id)
require.Equal(t, saved.FileId, r.FileId)
require.Equal(t, saved.RemoteId, r.RemoteId)
require.Equal(t, saved.CreateAt, r.CreateAt)
})
t.Run("Get non-existent shared channel attachment", func(t *testing.T) {
u, err := ss.SharedChannel().GetAttachment(model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, u)
})
}
func testUpdateSharedChannelAttachmentLastSyncAt(t *testing.T, ss store.Store) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save attachment", err)
future := model.GetMillis() + 3600000 // 1 hour in the future
t.Run("Update LastSyncAt for attachment", func(t *testing.T) {
err := ss.SharedChannel().UpdateAttachmentLastSyncAt(saved.Id, future)
require.NoError(t, err, "updateLastSyncAt should not error", err)
f, err := ss.SharedChannel().GetAttachment(saved.FileId, saved.RemoteId)
require.NoError(t, err)
require.Equal(t, future, f.LastSyncAt)
})
t.Run("Update LastSyncAt for non-existent shared channel attachment", func(t *testing.T) {
err := ss.SharedChannel().UpdateAttachmentLastSyncAt(model.NewId(), future)
require.Error(t, err, "update non-existent attachment should error", err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestStatusStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testStatusStore(t, ss) })
t.Run("ActiveUserCount", func(t *testing.T) { testActiveUserCount(t, ss) })
t.Run("UpdateExpiredDNDStatuses", func(t *testing.T) { testUpdateExpiredDNDStatuses(t, ss) })
}
func testStatusStore(t *testing.T, ss store.Store) {
status := &model.Status{UserId: model.NewId(), Status: model.StatusOnline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status))
status.LastActivityAt = 10
_, err := ss.Status().Get(status.UserId)
require.NoError(t, err)
status2 := &model.Status{UserId: model.NewId(), Status: model.StatusAway, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status2))
status3 := &model.Status{UserId: model.NewId(), Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status3))
statuses, err := ss.Status().GetByIds([]string{status.UserId, "junk"})
require.NoError(t, err)
require.Len(t, statuses, 1, "should only have 1 status")
err = ss.Status().ResetAll()
require.NoError(t, err)
statusParameter, err := ss.Status().Get(status.UserId)
require.NoError(t, err)
require.Equal(t, statusParameter.Status, model.StatusOffline, "should be offline")
err = ss.Status().UpdateLastActivityAt(status.UserId, 10)
require.NoError(t, err)
}
func testActiveUserCount(t *testing.T, ss store.Store) {
status := &model.Status{UserId: model.NewId(), Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status))
count, err := ss.Status().GetTotalActiveUsersCount()
require.NoError(t, err)
require.True(t, count > 0, "expected count > 0, got %d", count)
}
type ByUserId []*model.Status
func (s ByUserId) Len() int { return len(s) }
func (s ByUserId) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByUserId) Less(i, j int) bool { return s[i].UserId < s[j].UserId }
func testUpdateExpiredDNDStatuses(t *testing.T, ss store.Store) {
userID := NewTestId()
status := &model.Status{UserId: userID, Status: model.StatusDnd, Manual: true,
DNDEndTime: time.Now().Add(5 * time.Second).Unix(), PrevStatus: model.StatusOnline}
require.NoError(t, ss.Status().SaveOrUpdate(status))
time.Sleep(2 * time.Second)
// after 2 seconds no statuses should be expired
statuses, err := ss.Status().UpdateExpiredDNDStatuses()
require.NoError(t, err)
require.Len(t, statuses, 0)
time.Sleep(3 * time.Second)
// after 3 more seconds test status should be updated
statuses, err = ss.Status().UpdateExpiredDNDStatuses()
require.NoError(t, err)
require.Len(t, statuses, 1)
updatedStatus := *statuses[0]
require.Equal(t, updatedStatus.UserId, userID)
require.Equal(t, updatedStatus.Status, model.StatusOnline)
require.Equal(t, updatedStatus.DNDEndTime, int64(0))
require.Equal(t, updatedStatus.PrevStatus, model.StatusDnd)
require.Equal(t, updatedStatus.Manual, false)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"database/sql"
"time"
"github.com/stretchr/testify/mock"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
)
// Store can be used to provide mock stores for testing.
type Store struct {
TeamStore mocks.TeamStore
ChannelStore mocks.ChannelStore
PostStore mocks.PostStore
UserStore mocks.UserStore
RetentionPolicyStore mocks.RetentionPolicyStore
BotStore mocks.BotStore
AuditStore mocks.AuditStore
ClusterDiscoveryStore mocks.ClusterDiscoveryStore
RemoteClusterStore mocks.RemoteClusterStore
ComplianceStore mocks.ComplianceStore
SessionStore mocks.SessionStore
OAuthStore mocks.OAuthStore
SystemStore mocks.SystemStore
WebhookStore mocks.WebhookStore
CommandStore mocks.CommandStore
CommandWebhookStore mocks.CommandWebhookStore
PreferenceStore mocks.PreferenceStore
LicenseStore mocks.LicenseStore
TokenStore mocks.TokenStore
EmojiStore mocks.EmojiStore
ThreadStore mocks.ThreadStore
StatusStore mocks.StatusStore
FileInfoStore mocks.FileInfoStore
UploadSessionStore mocks.UploadSessionStore
ReactionStore mocks.ReactionStore
JobStore mocks.JobStore
UserAccessTokenStore mocks.UserAccessTokenStore
PluginStore mocks.PluginStore
ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore
RoleStore mocks.RoleStore
SchemeStore mocks.SchemeStore
TermsOfServiceStore mocks.TermsOfServiceStore
GroupStore mocks.GroupStore
UserTermsOfServiceStore mocks.UserTermsOfServiceStore
LinkMetadataStore mocks.LinkMetadataStore
SharedChannelStore mocks.SharedChannelStore
ProductNoticesStore mocks.ProductNoticesStore
DraftStore mocks.DraftStore
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
TrueUpReviewStore mocks.TrueUpReviewStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
func (s *Store) Context() context.Context { return s.context }
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore }
func (s *Store) Post() store.PostStore { return &s.PostStore }
func (s *Store) User() store.UserStore { return &s.UserStore }
func (s *Store) RetentionPolicy() store.RetentionPolicyStore { return &s.RetentionPolicyStore }
func (s *Store) Bot() store.BotStore { return &s.BotStore }
func (s *Store) ProductNotices() store.ProductNoticesStore { return &s.ProductNoticesStore }
func (s *Store) Audit() store.AuditStore { return &s.AuditStore }
func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore }
func (s *Store) RemoteCluster() store.RemoteClusterStore { return &s.RemoteClusterStore }
func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore }
func (s *Store) Session() store.SessionStore { return &s.SessionStore }
func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore }
func (s *Store) System() store.SystemStore { return &s.SystemStore }
func (s *Store) Webhook() store.WebhookStore { return &s.WebhookStore }
func (s *Store) Command() store.CommandStore { return &s.CommandStore }
func (s *Store) CommandWebhook() store.CommandWebhookStore { return &s.CommandWebhookStore }
func (s *Store) Preference() store.PreferenceStore { return &s.PreferenceStore }
func (s *Store) License() store.LicenseStore { return &s.LicenseStore }
func (s *Store) Token() store.TokenStore { return &s.TokenStore }
func (s *Store) Emoji() store.EmojiStore { return &s.EmojiStore }
func (s *Store) Thread() store.ThreadStore { return &s.ThreadStore }
func (s *Store) Status() store.StatusStore { return &s.StatusStore }
func (s *Store) FileInfo() store.FileInfoStore { return &s.FileInfoStore }
func (s *Store) UploadSession() store.UploadSessionStore { return &s.UploadSessionStore }
func (s *Store) Reaction() store.ReactionStore { return &s.ReactionStore }
func (s *Store) Job() store.JobStore { return &s.JobStore }
func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore }
func (s *Store) Plugin() store.PluginStore { return &s.PluginStore }
func (s *Store) Role() store.RoleStore { return &s.RoleStore }
func (s *Store) Scheme() store.SchemeStore { return &s.SchemeStore }
func (s *Store) TermsOfService() store.TermsOfServiceStore { return &s.TermsOfServiceStore }
func (s *Store) UserTermsOfService() store.UserTermsOfServiceStore { return &s.UserTermsOfServiceStore }
func (s *Store) Draft() store.DraftStore { return &s.DraftStore }
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
func (s *Store) UnlockFromMaster() { /* do nothing */ }
func (s *Store) DropAllTables() { /* do nothing */ }
func (s *Store) GetDbVersion(bool) (string, error) { return "", nil }
func (s *Store) GetInternalMasterDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil }
func (s *Store) RecycleDBConnections(time.Duration) {}
func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil }
func (s *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) {
return []model.AppliedMigration{}, nil
}
func (s *Store) TotalMasterDbConnections() int { return 1 }
func (s *Store) TotalReadDbConnections() int { return 1 }
func (s *Store) TotalSearchDbConnections() int { return 1 }
func (s *Store) CheckIntegrity() <-chan model.IntegrityCheckResult {
return make(chan model.IntegrityCheckResult)
}
func (s *Store) ReplicaLagAbs() error { return nil }
func (s *Store) ReplicaLagTime() error { return nil }
func (s *Store) AssertExpectations(t mock.TestingT) bool {
return mock.AssertExpectationsForObjects(t,
&s.TeamStore,
&s.ChannelStore,
&s.PostStore,
&s.UserStore,
&s.BotStore,
&s.AuditStore,
&s.ClusterDiscoveryStore,
&s.RemoteClusterStore,
&s.ComplianceStore,
&s.SessionStore,
&s.OAuthStore,
&s.SystemStore,
&s.WebhookStore,
&s.CommandStore,
&s.CommandWebhookStore,
&s.PreferenceStore,
&s.LicenseStore,
&s.TokenStore,
&s.EmojiStore,
&s.StatusStore,
&s.FileInfoStore,
&s.UploadSessionStore,
&s.ReactionStore,
&s.JobStore,
&s.UserAccessTokenStore,
&s.ChannelMemberHistoryStore,
&s.PluginStore,
&s.RoleStore,
&s.SchemeStore,
&s.ThreadStore,
&s.ProductNoticesStore,
&s.SharedChannelStore,
&s.DraftStore,
&s.NotifyAdminStore,
&s.PostPriorityStore,
&s.PostAcknowledgementStore,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func MakeEmail() string {
return "success_" + model.NewId() + "@simulator.amazonses.com"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSystemStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testSystemStore(t, ss) })
t.Run("SaveOrUpdate", func(t *testing.T) { testSystemStoreSaveOrUpdate(t, ss) })
t.Run("PermanentDeleteByName", func(t *testing.T) { testSystemStorePermanentDeleteByName(t, ss) })
t.Run("InsertIfExists", func(t *testing.T) {
testInsertIfExists(t, ss)
})
t.Run("SaveOrUpdateWithWarnMetricHandling", func(t *testing.T) { testSystemStoreSaveOrUpdateWithWarnMetricHandling(t, ss) })
t.Run("GetByNameNoEntries", func(t *testing.T) { testSystemStoreGetByNameNoEntries(t, ss) })
}
func testSystemStore(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().Save(system)
require.NoError(t, err)
system2 := &model.System{Name: model.NewId(), Value: "value2"}
err = ss.System().Save(system2)
require.NoError(t, err)
systems, err := ss.System().Get()
require.NoError(t, err)
require.Equal(t, system.Value, systems[system.Name])
system.Value = "value1"
err = ss.System().Update(system)
require.NoError(t, err)
systems2, err := ss.System().Get()
require.NoError(t, err)
require.Equal(t, system.Value, systems2[system.Name])
require.Equal(t, system2.Value, systems2[system2.Name])
rsystem, err := ss.System().GetByName(system.Name)
require.NoError(t, err)
require.Equal(t, system.Value, rsystem.Value)
}
func testSystemStoreSaveOrUpdate(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().SaveOrUpdate(system)
require.NoError(t, err)
res, err := ss.System().GetByName(system.Name)
require.NoError(t, err)
assert.Equal(t, system.Value, res.Value)
system.Value = "value2"
err = ss.System().SaveOrUpdate(system)
require.NoError(t, err)
res, err = ss.System().GetByName(system.Name)
require.NoError(t, err)
assert.Equal(t, system.Value, res.Value)
}
func testSystemStoreSaveOrUpdateWithWarnMetricHandling(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
_, err = ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.Error(t, err)
system.Name = "warn_metric_number_of_active_users_100"
system.Value = model.WarnMetricStatusRunonce
err = ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
val1, nerr := ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.NoError(t, nerr)
system.Name = "warn_metric_number_of_active_users_100"
system.Value = model.WarnMetricStatusAck
err = ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
val2, nerr := ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.NoError(t, nerr)
assert.Equal(t, val1, val2)
}
func testSystemStoreGetByNameNoEntries(t *testing.T, ss store.Store) {
res, nErr := ss.System().GetByName(model.SystemFirstAdminVisitMarketplace)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, res)
}
func testSystemStorePermanentDeleteByName(t *testing.T, ss store.Store) {
s1 := &model.System{Name: model.NewId(), Value: "value"}
s2 := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().Save(s1)
require.NoError(t, err)
err = ss.System().Save(s2)
require.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().PermanentDeleteByName(s1.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.Error(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().PermanentDeleteByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.Error(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.Error(t, err)
}
func testInsertIfExists(t *testing.T, ss store.Store) {
t.Run("Serial", func(t *testing.T) {
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "somekey"}
s2, err := ss.System().InsertIfExists(s1)
require.NoError(t, err)
assert.Equal(t, s1.Value, s2.Value)
s1New := &model.System{Name: model.SystemClusterEncryptionKey, Value: "anotherKey"}
s3, err := ss.System().InsertIfExists(s1New)
require.NoError(t, err)
assert.Equal(t, s1.Value, s3.Value)
})
t.Run("Concurrent", func(t *testing.T) {
var s2, s3 *model.System
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "firstKey"}
var err error
s2, err = ss.System().InsertIfExists(s1)
require.NoError(t, err)
}()
go func() {
defer wg.Done()
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "secondKey"}
var err error
s3, err = ss.System().InsertIfExists(s1)
require.NoError(t, err)
}()
wg.Wait()
assert.Equal(t, s2.Value, s3.Value)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func cleanupTeamStore(t *testing.T, ss store.Store) {
allTeams, err := ss.Team().GetAll()
for _, team := range allTeams {
ss.Team().PermanentDelete(team.Id)
}
assert.NoError(t, err)
}
func TestTeamStore(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testTeamStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testTeamStoreUpdate(t, ss) })
t.Run("Get", func(t *testing.T) { testTeamStoreGet(t, ss) })
t.Run("GetMany", func(t *testing.T) { testTeamStoreGetMany(t, ss) })
t.Run("GetByName", func(t *testing.T) { testTeamStoreGetByName(t, ss) })
t.Run("GetByNames", func(t *testing.T) { testTeamStoreGetByNames(t, ss) })
t.Run("SearchAll", func(t *testing.T) { testTeamStoreSearchAll(t, ss) })
t.Run("SearchOpen", func(t *testing.T) { testTeamStoreSearchOpen(t, ss) })
t.Run("SearchPrivate", func(t *testing.T) { testTeamStoreSearchPrivate(t, ss) })
t.Run("GetByInviteId", func(t *testing.T) { testTeamStoreGetByInviteId(t, ss) })
t.Run("ByUserId", func(t *testing.T) { testTeamStoreByUserId(t, ss) })
t.Run("GetAllTeamListing", func(t *testing.T) { testGetAllTeamListing(t, ss) })
t.Run("GetAllTeamPage", func(t *testing.T) { testTeamStoreGetAllPage(t, ss) })
t.Run("GetAllTeamPageListing", func(t *testing.T) { testGetAllTeamPageListing(t, ss) })
t.Run("GetAllPrivateTeamListing", func(t *testing.T) { testGetAllPrivateTeamListing(t, ss) })
t.Run("GetAllPrivateTeamPageListing", func(t *testing.T) { testGetAllPrivateTeamPageListing(t, ss) })
t.Run("GetAllPublicTeamPageListing", func(t *testing.T) { testGetAllPublicTeamPageListing(t, ss) })
t.Run("Delete", func(t *testing.T) { testDelete(t, ss) })
t.Run("TeamCount", func(t *testing.T) { testTeamCount(t, ss) })
t.Run("TeamPublicCount", func(t *testing.T) { testPublicTeamCount(t, ss) })
t.Run("TeamPrivateCount", func(t *testing.T) { testPrivateTeamCount(t, ss) })
t.Run("TeamMembers", func(t *testing.T) { testTeamMembers(t, ss) })
t.Run("TestGetMembers", func(t *testing.T) { testGetMembers(t, ss) })
t.Run("SaveMember", func(t *testing.T) { testTeamSaveMember(t, ss) })
t.Run("SaveMultipleMembers", func(t *testing.T) { testTeamSaveMultipleMembers(t, ss) })
t.Run("UpdateMember", func(t *testing.T) { testTeamUpdateMember(t, ss) })
t.Run("UpdateMultipleMembers", func(t *testing.T) { testTeamUpdateMultipleMembers(t, ss) })
t.Run("RemoveMember", func(t *testing.T) { testTeamRemoveMember(t, ss) })
t.Run("RemoveMembers", func(t *testing.T) { testTeamRemoveMembers(t, ss) })
t.Run("SaveTeamMemberMaxMembers", func(t *testing.T) { testSaveTeamMemberMaxMembers(t, ss) })
t.Run("GetTeamMember", func(t *testing.T) { testGetTeamMember(t, ss) })
t.Run("GetTeamMembersByIds", func(t *testing.T) { testGetTeamMembersByIds(t, ss) })
t.Run("MemberCount", func(t *testing.T) { testTeamStoreMemberCount(t, ss) })
t.Run("GetChannelUnreadsForAllTeams", func(t *testing.T) { testGetChannelUnreadsForAllTeams(t, ss) })
t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) })
t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) })
t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) })
t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) })
t.Run("ResetAllTeamSchemes", func(t *testing.T) { testResetAllTeamSchemes(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testTeamStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("AnalyticsGetTeamCountForScheme", func(t *testing.T) { testTeamStoreAnalyticsGetTeamCountForScheme(t, ss) })
t.Run("GetAllForExportAfter", func(t *testing.T) { testTeamStoreGetAllForExportAfter(t, ss) })
t.Run("GetTeamMembersForExport", func(t *testing.T) { testTeamStoreGetTeamMembersForExport(t, ss) })
t.Run("GetTeamsForUserWithPagination", func(t *testing.T) { testTeamMembersWithPagination(t, ss) })
t.Run("GroupSyncedTeamCount", func(t *testing.T) { testGroupSyncedTeamCount(t, ss) })
t.Run("GetNewTeamMembersSince", func(t *testing.T) { testGetNewTeamMembersSince(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Team().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
o1.Id = ""
_, err = ss.Team().Save(&o1)
require.Error(t, err, "should be unique domain")
}
func testTeamStoreUpdate(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
_, err = ss.Team().Update(&o1)
require.NoError(t, err)
o1.Id = "missing"
_, err = ss.Team().Update(&o1)
require.Error(t, err, "Update should have failed because of missing key")
o1.Id = model.NewId()
_, err = ss.Team().Update(&o1)
require.Error(t, err, "Update should have faile because id change")
}
func testTeamStoreGet(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
r1, err := ss.Team().Get(o1.Id)
require.NoError(t, err)
require.Equal(t, r1, &o1)
_, err = ss.Team().Get("")
require.Error(t, err, "Missing id should have failed")
}
func testTeamStoreGetMany(t *testing.T, ss store.Store) {
o1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
o2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
res, err := ss.Team().GetMany([]string{o1.Id, o2.Id})
require.NoError(t, err)
assert.Len(t, res, 2)
res, err = ss.Team().GetMany([]string{o1.Id, "notexists"})
require.NoError(t, err)
assert.Len(t, res, 1)
_, err = ss.Team().GetMany([]string{"whereisit", "notexists"})
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
}
func testTeamStoreGetByNames(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName2"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
t.Run("Get empty list", func(t *testing.T) {
var teams []*model.Team
teams, err = ss.Team().GetByNames([]string{})
require.NoError(t, err)
require.Empty(t, teams)
})
t.Run("Get existing teams", func(t *testing.T) {
var teams []*model.Team
teams, err = ss.Team().GetByNames([]string{o1.Name, o2.Name})
require.NoError(t, err)
teamsIds := []string{}
for _, team := range teams {
teamsIds = append(teamsIds, team.Id)
}
assert.Contains(t, teamsIds, o1.Id, "invalid returned team")
assert.Contains(t, teamsIds, o2.Id, "invalid returned team")
})
t.Run("Get existing team and one invalid team name", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{o1.Name, ""})
require.Error(t, err)
})
t.Run("Get existing team and not existing team", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{o1.Name, "not-existing-team-name"})
require.Error(t, err)
})
t.Run("Get not existing teams", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{"not-existing-team-name", "not-existing-team-name-2"})
require.Error(t, err)
})
}
func testTeamStoreGetByName(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
t.Run("Get existing team", func(t *testing.T) {
var team *model.Team
team, err = ss.Team().GetByName(o1.Name)
require.NoError(t, err)
require.Equal(t, *team, o1, "invalid returned team")
})
t.Run("Get invalid team name", func(t *testing.T) {
_, err = ss.Team().GetByName("")
require.Error(t, err, "Missing id should have failed")
})
t.Run("Get not existing team", func(t *testing.T) {
_, err = ss.Team().GetByName("not-existing-team-name")
require.Error(t, err, "Missing id should have failed")
})
}
func testTeamStoreSearchAll(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = "searchterm-" + NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "BDisplayName" + NewTestId()
p.Name = "searchterm-" + NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
g := model.Team{}
g.DisplayName = "CDisplayName" + NewTestId()
g.Name = "searchterm-" + NewTestId()
g.Email = MakeEmail()
g.Type = model.TeamOpen
g.AllowOpenInvite = false
g.GroupConstrained = model.NewBool(true)
_, err = ss.Team().Save(&g)
require.NoError(t, err)
q := &model.Team{}
q.DisplayName = "CHOCOLATE"
q.Name = "ilovecake"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = false
q, err = ss.Team().Save(q)
require.NoError(t, err)
_, err = ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{q.Id},
})
require.NoError(t, err)
testCases := []struct {
Name string
Opts *model.TeamSearch
ExpectedLenth int
ExpectedTeamIds []string
}{
{
"Search chocolate by display name",
&model.TeamSearch{Term: "ocola"},
1,
[]string{q.Id},
},
{
"Search chocolate by display name",
&model.TeamSearch{Term: "choc"},
1,
[]string{q.Id},
},
{
"Search chocolate by display name",
&model.TeamSearch{Term: "late"},
1,
[]string{q.Id},
},
{
"Search chocolate by name",
&model.TeamSearch{Term: "ilov"},
1,
[]string{q.Id},
},
{
"Search chocolate by name",
&model.TeamSearch{Term: "ecake"},
1,
[]string{q.Id},
},
{
"Search for open team name",
&model.TeamSearch{Term: o.Name},
1,
[]string{o.Id},
},
{
"Search for open team displayName",
&model.TeamSearch{Term: o.DisplayName},
1,
[]string{o.Id},
},
{
"Search for open team without results",
&model.TeamSearch{Term: "nonexistent"},
0,
[]string{},
},
{
"Search for private team",
&model.TeamSearch{Term: p.DisplayName},
1,
[]string{p.Id},
},
{
"Search for all 3 searchterm teams",
&model.TeamSearch{Term: "searchterm"},
3,
[]string{o.Id, p.Id, g.Id},
},
{
"Search for all 3 teams filter by allow open invite",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(true)},
1,
[]string{o.Id},
},
{
"Search for all 3 teams filter by allow open invite = false",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(false)},
1,
[]string{p.Id},
},
{
"Search for all 3 teams filter by group constrained",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(true)},
1,
[]string{g.Id},
},
{
"Search for all 3 teams filter by group constrained = false",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false)},
2,
[]string{o.Id, p.Id},
},
{
"Search for all 3 teams filter by allow open invite and include group constrained",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(true), GroupConstrained: model.NewBool(true)},
2,
[]string{o.Id, g.Id},
},
{
"Search for all 3 teams filter by group constrained and not open invite",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(true), AllowOpenInvite: model.NewBool(false)},
2,
[]string{g.Id, p.Id},
},
{
"Search for all 3 teams filter by group constrained false and open invite",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(true)},
2,
[]string{o.Id, p.Id},
},
{
"Search for all 3 teams filter by group constrained false and open invite false",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(false)},
2,
[]string{p.Id, o.Id},
},
{
"Search for teams which are not part of a data retention policy",
&model.TeamSearch{Term: "", ExcludePolicyConstrained: model.NewBool(true)},
3,
[]string{o.Id, p.Id, g.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
response, err := ss.Team().SearchAll(tc.Opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedLenth, len(response))
responseTeamIds := []string{}
for _, team := range response {
responseTeamIds = append(responseTeamIds, team.Id)
}
require.ElementsMatch(t, tc.ExpectedTeamIds, responseTeamIds)
})
}
}
func testTeamStoreSearchOpen(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "ADisplayName" + NewTestId()
p.Name = NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
q := model.Team{}
q.DisplayName = "PINEAPPLEPIE"
q.Name = "ihadsomepineapplepiewithstrawberry"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = true
_, err = ss.Team().Save(&q)
require.NoError(t, err)
testCases := []struct {
Name string
Term string
ExpectedLength int
ExpectedFirstId string
}{
{
"Search PINEAPPLEPIE by display name",
"neapplep",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by display name",
"pine",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by display name",
"epie",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by name",
"ihadsome",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by name",
"pineapplepiewithstrawberry",
1,
q.Id,
},
{
"Search for open team name",
o.Name,
1,
o.Id,
},
{
"Search for open team displayName",
o.DisplayName,
1,
o.Id,
},
{
"Search for open team without results",
"nonexistent",
0,
"",
},
{
"Search for a private team (expected no results)",
p.DisplayName,
0,
"",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
r1, err := ss.Team().SearchOpen(&model.TeamSearch{Term: tc.Term})
require.NoError(t, err)
results := r1
require.Equal(t, tc.ExpectedLength, len(results))
if tc.ExpectedFirstId != "" {
assert.Equal(t, tc.ExpectedFirstId, results[0].Id)
}
})
}
}
func testTeamStoreSearchPrivate(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "ADisplayName" + NewTestId()
p.Name = NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
q := model.Team{}
q.DisplayName = "FOOBARDISPLAYNAME"
q.Name = "averylongname"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = false
_, err = ss.Team().Save(&q)
require.NoError(t, err)
testCases := []struct {
Name string
Term string
ExpectedLength int
ExpectedFirstId string
}{
{
"Search FooBar by display name from text in the middle of display name",
"oobardisplay",
1,
q.Id,
},
{
"Search FooBar by display name from text at the beginning of display name",
"foobar",
1,
q.Id,
},
{
"Search FooBar by display name from text at the end of display name",
"bardisplayname",
1,
q.Id,
},
{
"Search FooBar by name from text at the beginning name",
"averyl",
1,
q.Id,
},
{
"Search FooBar by name from text at the end of name",
"ongname",
1,
q.Id,
},
{
"Search for private team name",
p.Name,
1,
p.Id,
},
{
"Search for private team displayName",
p.DisplayName,
1,
p.Id,
},
{
"Search for private team without results",
"nonexistent",
0,
"",
},
{
"Search for a open team (expected no results)",
o.DisplayName,
0,
"",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
r1, err := ss.Team().SearchPrivate(&model.TeamSearch{Term: tc.Term})
require.NoError(t, err)
results := r1
require.Equal(t, tc.ExpectedLength, len(results))
if tc.ExpectedFirstId != "" {
assert.Equal(t, tc.ExpectedFirstId, results[0].Id)
}
})
}
}
func testTeamStoreGetByInviteId(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.InviteId = model.NewId()
save1, err := ss.Team().Save(&o1)
require.NoError(t, err)
r1, err := ss.Team().GetByInviteId(save1.InviteId)
require.NoError(t, err)
require.Equal(t, *r1, o1, "invalid returned team")
_, err = ss.Team().GetByInviteId("")
require.Error(t, err, "Missing id should have failed")
}
func testTeamStoreByUserId(t *testing.T, ss store.Store) {
o1 := &model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.InviteId = model.NewId()
o1, err := ss.Team().Save(o1)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: o1.Id, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
teams, err := ss.Team().GetTeamsByUserId(m1.UserId)
require.NoError(t, err)
require.Len(t, teams, 1, "Should return a team")
require.Equal(t, teams[0].Id, o1.Id, "should be a member")
}
func testTeamStoreGetAllPage(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + model.NewId()
o.Name = "zz" + model.NewId() + "a"
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
policy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
TeamIDs: []string{o.Id},
})
require.NoError(t, err)
// Without ExcludePolicyConstrained
teams, err := ss.Team().GetAllPage(0, 100, nil)
require.NoError(t, err)
found := false
for _, team := range teams {
if team.Id == o.Id {
found = true
require.Nil(t, team.PolicyID)
break
}
}
require.True(t, found)
// With ExcludePolicyConstrained
teams, err = ss.Team().GetAllPage(0, 100, &model.TeamSearch{ExcludePolicyConstrained: model.NewBool(true)})
require.NoError(t, err)
found = false
for _, team := range teams {
if team.Id == o.Id {
found = true
break
}
}
require.False(t, found)
// With policy ID
teams, err = ss.Team().GetAllPage(0, 100, &model.TeamSearch{IncludePolicyID: model.NewBool(true)})
require.NoError(t, err)
found = false
for _, team := range teams {
if team.Id == o.Id {
found = true
require.Equal(t, *team.PolicyID, policy.ID)
break
}
}
require.True(t, found)
}
func testGetAllTeamListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
teams, err := ss.Team().GetAllTeamListing()
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.NotEmpty(t, teams, "failed team listing")
}
func testGetAllTeamPageListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(true)}
teams, err := ss.Team().GetAllPage(0, 10, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 10, "should have returned max of 10 teams")
o5 := model.Team{}
o5.DisplayName = "DisplayName"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
_, err = ss.Team().Save(&o5)
require.NoError(t, err)
teams, err = ss.Team().GetAllPage(0, 4, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 4, "should have returned max of 4 teams")
teams, err = ss.Team().GetAllPage(1, 1, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 1, "should have returned max of 1 team")
}
func testGetAllPrivateTeamListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
teams, err := ss.Team().GetAllPrivateTeamListing()
require.NoError(t, err)
require.NotEmpty(t, teams, "failed team listing")
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
}
func testGetAllPrivateTeamPageListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(false)}
teams, listErr := ss.Team().GetAllPage(0, 10, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 10, "should have returned max of 10 teams")
o5 := model.Team{}
o5.DisplayName = "DisplayName"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
_, err = ss.Team().Save(&o5)
require.NoError(t, err)
teams, listErr = ss.Team().GetAllPage(0, 4, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 4, "should have returned max of 4 teams")
teams, listErr = ss.Team().GetAllPage(1, 1, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 1, "should have returned max of 1 team")
}
func testGetAllPublicTeamPageListing(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName1"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
t1, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName2"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName3"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
t3, err := ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName4"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(true)}
teams, err := ss.Team().GetAllPage(0, 10, opts)
assert.NoError(t, err)
assert.Equal(t, []*model.Team{t1, t3}, teams)
o5 := model.Team{}
o5.DisplayName = "DisplayName5"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
t5, err := ss.Team().Save(&o5)
require.NoError(t, err)
teams, err = ss.Team().GetAllPage(0, 4, opts)
assert.NoError(t, err)
assert.Equal(t, []*model.Team{t1, t3, t5}, teams)
_, err = ss.Team().GetAllPage(1, 1, opts)
assert.NoError(t, err)
}
func testDelete(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
r1 := ss.Team().PermanentDelete(o1.Id)
require.NoError(t, r1)
}
func testPublicTeamCount(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamOpen
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
teamCount, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{AllowOpenInvite: model.NewBool(true)})
require.NoError(t, err)
require.Equal(t, int64(2), teamCount, "should only be 1 team")
}
func testPrivateTeamCount(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = false
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = true
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamOpen
o3.AllowOpenInvite = false
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
teamCount, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{AllowOpenInvite: model.NewBool(false)})
require.NoError(t, err)
require.Equal(t, int64(2), teamCount, "should only be 1 team")
}
func testTeamCount(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
team, err := ss.Team().Save(&o1)
require.NoError(t, err)
// not including deleted teams
teamCount, err := ss.Team().AnalyticsTeamCount(nil)
require.NoError(t, err)
require.NotEqual(t, 0, int(teamCount), "should be at least 1 team")
// delete the team for the next check
team.DeleteAt = model.GetMillis()
_, err = ss.Team().Update(team)
require.NoError(t, err)
// get the count of teams not including deleted
countNotIncludingDeleted, err := ss.Team().AnalyticsTeamCount(nil)
require.NoError(t, err)
// get the count of teams including deleted
countIncludingDeleted, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{IncludeDeleted: model.NewBool(true)})
require.NoError(t, err)
// count including deleted should be one greater than not including deleted
require.Equal(t, countNotIncludingDeleted+1, countIncludingDeleted)
}
func testGetMembers(t *testing.T, ss store.Store) {
// Each user should have a mention count of exactly 1 in the DB at this point.
t.Run("Test GetMembers Order By UserID", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: "55555555555555555555555555"}
m2 := &model.TeamMember{TeamId: teamId1, UserId: "11111111111111111111111111"}
m3 := &model.TeamMember{TeamId: teamId1, UserId: "33333333333333333333333333"}
m4 := &model.TeamMember{TeamId: teamId1, UserId: "22222222222222222222222222"}
m5 := &model.TeamMember{TeamId: teamId1, UserId: "44444444444444444444444444"}
m6 := &model.TeamMember{TeamId: teamId2, UserId: "00000000000000000000000000"}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6}, -1)
require.NoError(t, nErr)
// Gets users ordered by UserId
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
assert.Len(t, ms, 5)
assert.Equal(t, "11111111111111111111111111", ms[0].UserId)
assert.Equal(t, "22222222222222222222222222", ms[1].UserId)
assert.Equal(t, "33333333333333333333333333", ms[2].UserId)
assert.Equal(t, "44444444444444444444444444", ms[3].UserId)
assert.Equal(t, "55555555555555555555555555", ms[4].UserId)
})
t.Run("Test GetMembers Order By Username And Exclude Deleted Members", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
u1 := &model.User{Username: "a", Email: MakeEmail(), DeleteAt: int64(1)}
u2 := &model.User{Username: "c", Email: MakeEmail()}
u3 := &model.User{Username: "b", Email: MakeEmail(), DeleteAt: int64(1)}
u4 := &model.User{Username: "f", Email: MakeEmail()}
u5 := &model.User{Username: "e", Email: MakeEmail(), DeleteAt: int64(1)}
u6 := &model.User{Username: "d", Email: MakeEmail()}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2, err = ss.User().Save(u2)
require.NoError(t, err)
u3, err = ss.User().Save(u3)
require.NoError(t, err)
u4, err = ss.User().Save(u4)
require.NoError(t, err)
u5, err = ss.User().Save(u5)
require.NoError(t, err)
u6, err = ss.User().Save(u6)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamId1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamId1, UserId: u4.Id}
m5 := &model.TeamMember{TeamId: teamId1, UserId: u5.Id}
m6 := &model.TeamMember{TeamId: teamId2, UserId: u6.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6}, -1)
require.NoError(t, nErr)
// Gets users ordered by UserName
ms, nErr := ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{Sort: model.USERNAME})
require.NoError(t, nErr)
assert.Len(t, ms, 5)
assert.Equal(t, u1.Id, ms[0].UserId)
assert.Equal(t, u3.Id, ms[1].UserId)
assert.Equal(t, u2.Id, ms[2].UserId)
assert.Equal(t, u5.Id, ms[3].UserId)
assert.Equal(t, u4.Id, ms[4].UserId)
// Gets users ordered by UserName and excludes deleted members
ms, nErr = ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{Sort: model.USERNAME, ExcludeDeletedUsers: true})
require.NoError(t, nErr)
assert.Len(t, ms, 2)
assert.Equal(t, u2.Id, ms[0].UserId)
assert.Equal(t, u4.Id, ms[1].UserId)
})
t.Run("Test GetMembers Excluded Deleted Users", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
u1 := &model.User{Email: MakeEmail()}
u2 := &model.User{Email: MakeEmail(), DeleteAt: int64(1)}
u3 := &model.User{Email: MakeEmail()}
u4 := &model.User{Email: MakeEmail(), DeleteAt: int64(3)}
u5 := &model.User{Email: MakeEmail()}
u6 := &model.User{Email: MakeEmail(), DeleteAt: int64(5)}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2, err = ss.User().Save(u2)
require.NoError(t, err)
u3, err = ss.User().Save(u3)
require.NoError(t, err)
u4, err = ss.User().Save(u4)
require.NoError(t, err)
u5, err = ss.User().Save(u5)
require.NoError(t, err)
u6, err = ss.User().Save(u6)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamId1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamId1, UserId: u4.Id}
m5 := &model.TeamMember{TeamId: teamId1, UserId: u5.Id}
m6 := &model.TeamMember{TeamId: teamId2, UserId: u6.Id}
t1, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
t3, nErr := ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m4, -1)
require.NoError(t, nErr)
t5, nErr := ss.Team().SaveMember(m5, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m6, -1)
require.NoError(t, nErr)
// Gets users ordered by UserName
ms, nErr := ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{ExcludeDeletedUsers: true})
require.NoError(t, nErr)
assert.Len(t, ms, 3)
require.ElementsMatch(t, ms, [3]*model.TeamMember{t1, t3, t5})
})
}
func testTeamMembers(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m3 := &model.TeamMember{TeamId: teamId2, UserId: model.NewId()}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
require.NoError(t, nErr)
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
assert.Len(t, ms, 2)
ms, err = ss.Team().GetMembers(teamId2, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m3.UserId, ms[0].UserId)
ctx := context.Background()
ms, err = ss.Team().GetTeamsForUser(ctx, m1.UserId, "", true)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m1.TeamId, ms[0].TeamId)
err = ss.Team().RemoveMember(teamId1, m1.UserId)
require.NoError(t, err)
ms, err = ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m2.UserId, ms[0].UserId)
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
err = ss.Team().RemoveAllMembersByTeam(teamId1)
require.NoError(t, err)
ms, err = ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Empty(t, ms)
uid := model.NewId()
m4 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m5 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m4, m5}, -1)
require.NoError(t, nErr)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", true)
require.NoError(t, err)
require.Len(t, ms, 2)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, teamId2, true)
require.NoError(t, err)
require.Len(t, ms, 1)
m4.DeleteAt = model.GetMillis()
_, err = ss.Team().UpdateMember(m4)
require.NoError(t, err)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", true)
require.NoError(t, err)
require.Len(t, ms, 2)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", false)
require.NoError(t, err)
require.Len(t, ms, 1)
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
ms, err = ss.Team().GetTeamsForUser(ctx, m1.UserId, "", true)
require.NoError(t, err)
require.Empty(t, ms)
}
func testTeamSaveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("not valid team member", func(t *testing.T) {
member := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
_, nErr := ss.Team().SaveMember(member, -1)
require.Error(t, nErr)
require.Equal(t, "TeamMember.IsValid: model.team_member.is_valid.team_id.app_error", nErr.Error())
})
t.Run("too many members", func(t *testing.T) {
member := &model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}
_, nErr := ss.Team().SaveMember(member, 0)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 1 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members because previous existing members", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, 1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
_, nErr = ss.Team().SaveMember(m2, 1)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 2 metadata: team members limit exceeded", nErr.Error())
})
t.Run("duplicated entries should fail", func(t *testing.T) {
teamID1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert member correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
member, nErr := ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
defer ss.Team().RemoveMember(team.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
member, nErr := ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
defer ss.Team().RemoveMember(team.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamSaveMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("any not valid team member", func(t *testing.T) {
m1 := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
m2 := &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
require.Error(t, nErr)
require.Equal(t, "TeamMember.IsValid: model.team_member.is_valid.team_id.app_error", nErr.Error())
})
t.Run("too many members in one team", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 0)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 2 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members in one team because previous existing members", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 3)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m3, m4}, 3)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 4 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members, but in different teams", func(t *testing.T) {
teamID1 := model.NewId()
teamID2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID2, UserId: u1.Id}
m5 := &model.TeamMember{TeamId: teamID2, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5}, 2)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 3 metadata: team members limit exceeded", nErr.Error())
})
t.Run("duplicated entries should fail", func(t *testing.T) {
teamID1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 10)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
otherMember := &model.TeamMember{
TeamId: team.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
var members []*model.TeamMember
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Team().RemoveMember(team.Id, u1.Id)
defer ss.Team().RemoveMember(team.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
otherMember := &model.TeamMember{
TeamId: team.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Team().RemoveMember(team.Id, u1.Id)
defer ss.Team().RemoveMember(team.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamUpdateMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("not valid team member", func(t *testing.T) {
member := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
_, nErr := ss.Team().UpdateMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.team_member.is_valid.team_id.app_error", appErr.Id)
})
t.Run("insert member correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
member, nErr = ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Team().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
member, nErr = ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Team().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamUpdateMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("any not valid team member", func(t *testing.T) {
m1 := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
m2 := &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}
_, nErr := ss.Team().UpdateMultipleMembers([]*model.TeamMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.team_member.is_valid.team_id.app_error", appErr.Id)
})
t.Run("update members correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
otherMember := &model.TeamMember{TeamId: team.Id, UserId: u2.Id}
var members []*model.TeamMember
members, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.TeamMember
members, nErr = ss.Team().UpdateMultipleMembers([]*model.TeamMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
otherMember := &model.TeamMember{TeamId: team.Id, UserId: u2.Id}
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
members, err := ss.Team().UpdateMultipleMembers([]*model.TeamMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamRemoveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
t.Run("remove member from not existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember("not-existing-team", u1.Id)
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing member from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember(teamID, model.NewId())
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove existing member from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember(teamID, u1.Id)
require.NoError(t, nErr)
defer ss.Team().SaveMember(m1, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 3)
})
}
func testTeamRemoveMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
t.Run("remove members from not existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers("not-existing-team", []string{u1.Id, u2.Id, u3.Id, u4.Id})
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{model.NewId(), model.NewId()})
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing and not existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{u1.Id, u2.Id, model.NewId(), model.NewId()})
require.NoError(t, nErr)
defer ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 2)
})
t.Run("remove existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{u1.Id, u2.Id, u3.Id})
require.NoError(t, nErr)
defer ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 1)
})
}
func testTeamMembersWithPagination(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m3 := &model.TeamMember{TeamId: teamId2, UserId: model.NewId()}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
require.NoError(t, nErr)
ms, errTeam := ss.Team().GetTeamsForUserWithPagination(m1.UserId, 0, 1)
require.NoError(t, errTeam)
require.Len(t, ms, 1)
require.Equal(t, m1.TeamId, ms[0].TeamId)
e := ss.Team().RemoveMember(teamId1, m1.UserId)
require.NoError(t, e)
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m2.UserId, ms[0].UserId)
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
err = ss.Team().RemoveAllMembersByTeam(teamId1)
require.NoError(t, err)
uid := model.NewId()
m4 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m5 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m4, m5}, -1)
require.NoError(t, nErr)
result, err := ss.Team().GetTeamsForUserWithPagination(uid, 0, 1)
require.NoError(t, err)
require.Len(t, result, 1)
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
result, err = ss.Team().GetTeamsForUserWithPagination(uid, 1, 1)
require.NoError(t, err)
require.Empty(t, result)
}
func testSaveTeamMemberMaxMembers(t *testing.T, ss store.Store) {
maxUsersPerTeam := 5
team, errSave := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, errSave)
defer func() {
ss.Team().PermanentDelete(team.Id)
}()
userIds := make([]string, maxUsersPerTeam)
for i := 0; i < maxUsersPerTeam; i++ {
user, err := ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, err)
userIds[i] = user.Id
defer func(userId string) {
ss.User().PermanentDelete(userId)
}(userIds[i])
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userIds[i],
}, maxUsersPerTeam)
require.NoError(t, nErr)
defer func(userId string) {
ss.Team().RemoveMember(team.Id, userId)
}(userIds[i])
}
totalMemberCount, err := ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, err)
require.Equal(t, int(totalMemberCount), maxUsersPerTeam, "should start with 5 team members, had %v instead", totalMemberCount)
user, nErr := ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, nErr)
newUserId := user.Id
defer func() {
ss.User().PermanentDelete(newUserId)
}()
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: newUserId,
}, maxUsersPerTeam)
require.Error(t, nErr, "shouldn't be able to save member when at maximum members per team")
totalMemberCount, teamErr := ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam, int(totalMemberCount), "should still have 5 team members, had %v instead", totalMemberCount)
// Leaving the team from the UI sets DeleteAt instead of using TeamStore.RemoveMember
_, teamErr = ss.Team().UpdateMember(&model.TeamMember{
TeamId: team.Id,
UserId: userIds[0],
DeleteAt: 1234,
})
require.NoError(t, teamErr)
totalMemberCount, teamErr = ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam-1, int(totalMemberCount), "should now only have 4 team members, had %v instead", totalMemberCount)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: newUserId}, maxUsersPerTeam)
require.NoError(t, nErr, "should've been able to save new member after deleting one")
defer ss.Team().RemoveMember(team.Id, newUserId)
totalMemberCount, teamErr = ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam, int(totalMemberCount), "should have 5 team members again, had %v instead", totalMemberCount)
// Deactivating a user should make them stop counting against max members
user2, nErr := ss.User().Get(context.Background(), userIds[1])
require.NoError(t, nErr)
user2.DeleteAt = 1234
_, nErr = ss.User().Update(user2, true)
require.NoError(t, nErr)
user, nErr = ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, nErr)
newUserId2 := user.Id
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: newUserId2}, maxUsersPerTeam)
require.NoError(t, nErr, "should've been able to save new member after deleting one")
defer ss.Team().RemoveMember(team.Id, newUserId2)
}
func testGetTeamMember(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
var rm1 *model.TeamMember
rm1, err := ss.Team().GetMember(context.Background(), m1.TeamId, m1.UserId)
require.NoError(t, err)
require.Equal(t, rm1.TeamId, m1.TeamId, "bad team id")
require.Equal(t, rm1.UserId, m1.UserId, "bad user id")
_, err = ss.Team().GetMember(context.Background(), m1.TeamId, "")
require.Error(t, err, "empty user id - should have failed")
_, err = ss.Team().GetMember(context.Background(), "", m1.UserId)
require.Error(t, err, "empty team id - should have failed")
// Test with a custom team scheme.
s2 := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s2, nErr = ss.Scheme().Save(s2)
require.NoError(t, nErr)
t.Log(s2)
t2, nErr := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.TeamOpen,
SchemeId: &s2.Id,
})
require.NoError(t, nErr)
defer func() {
ss.Team().PermanentDelete(t2.Id)
}()
m2 := &model.TeamMember{TeamId: t2.Id, UserId: model.NewId(), SchemeUser: true}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
m3, err := ss.Team().GetMember(context.Background(), m2.TeamId, m2.UserId)
require.NoError(t, err)
t.Log(m3)
assert.Equal(t, s2.DefaultTeamUserRole, m3.Roles)
m4 := &model.TeamMember{TeamId: t2.Id, UserId: model.NewId(), SchemeGuest: true}
_, nErr = ss.Team().SaveMember(m4, -1)
require.NoError(t, nErr)
m5, err := ss.Team().GetMember(context.Background(), m4.TeamId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, s2.DefaultTeamGuestRole, m5.Roles)
}
func testGetTeamMembersByIds(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
var r []*model.TeamMember
r, err := ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}, nil)
require.NoError(t, err)
rm1 := r[0]
require.Equal(t, rm1.TeamId, m1.TeamId, "bad team id")
require.Equal(t, rm1.UserId, m1.UserId, "bad user id")
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
rm, err := ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}, nil)
require.NoError(t, err)
require.Len(t, rm, 2, "return wrong number of results")
_, err = ss.Team().GetMembersByIds(m1.TeamId, []string{}, nil)
require.Error(t, err, "empty user ids - should have failed")
}
func testTeamStoreMemberCount(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.DeleteAt = 1
_, err = ss.User().Save(u2)
require.NoError(t, err)
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
var totalMemberCount int64
totalMemberCount, nErr = ss.Team().GetTotalMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, int(totalMemberCount), 2, "wrong count")
var result int64
result, nErr = ss.Team().GetActiveMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 1, int(result), "wrong count")
m3 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
totalMemberCount, nErr = ss.Team().GetTotalMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 2, int(totalMemberCount), "wrong count")
result, nErr = ss.Team().GetActiveMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 1, int(result), "wrong count")
}
func testGetChannelUnreadsForAllTeams(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m2 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, err := ss.Channel().SaveMember(cm1)
require.NoError(t, err)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
ms1, nErr := ss.Team().GetChannelUnreadsForAllTeams("", uid)
require.NoError(t, nErr)
membersMap := make(map[string]bool)
for i := range ms1 {
id := ms1[i].TeamId
if _, ok := membersMap[id]; !ok {
membersMap[id] = true
}
}
require.Len(t, membersMap, 2, "Should be the unreads for all the teams")
require.Equal(t, 10, int(ms1[0].MsgCount), "subtraction failed")
ms2, nErr := ss.Team().GetChannelUnreadsForAllTeams(teamId1, uid)
require.NoError(t, nErr)
membersMap = make(map[string]bool)
for i := range ms2 {
id := ms2[i].TeamId
if _, ok := membersMap[id]; !ok {
membersMap[id] = true
}
}
require.Len(t, membersMap, 1, "Should be the unreads for just one team")
require.Equal(t, 10, int(ms2[0].MsgCount), "subtraction failed")
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
}
func testGetChannelUnreadsForTeam(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, nErr = ss.Channel().SaveMember(cm1)
require.NoError(t, nErr)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, nErr = ss.Channel().SaveMember(cm2)
require.NoError(t, nErr)
ms, err := ss.Team().GetChannelUnreadsForTeam(m1.TeamId, m1.UserId)
require.NoError(t, err)
require.Len(t, ms, 2, "wrong length")
require.Equal(t, 10, int(ms[0].MsgCount), "subtraction failed")
}
func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) {
// team icon initially updated a second ago
lastTeamIconUpdateInitial := model.GetMillis() - 1000
o1 := &model.Team{}
o1.DisplayName = "Display Name"
o1.Name = "z-z-z" + model.NewId() + "b"
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.LastTeamIconUpdate = lastTeamIconUpdateInitial
o1, err := ss.Team().Save(o1)
require.NoError(t, err)
curTime := model.GetMillis()
err = ss.Team().UpdateLastTeamIconUpdate(o1.Id, curTime)
require.NoError(t, err)
ro1, err := ss.Team().Get(o1.Id)
require.NoError(t, err)
require.Greater(t, ro1.LastTeamIconUpdate, lastTeamIconUpdateInitial, "LastTeamIconUpdate not updated")
}
func testGetTeamsByScheme(t *testing.T, ss store.Store) {
// Create some schemes.
s1 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s2 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
// Create and save some teams.
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t3 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(t1)
require.NoError(t, err)
_, err = ss.Team().Save(t2)
require.NoError(t, err)
_, err = ss.Team().Save(t3)
require.NoError(t, err)
// Get the teams by a valid Scheme ID.
d, err := ss.Team().GetTeamsByScheme(s1.Id, 0, 100)
assert.NoError(t, err)
assert.Len(t, d, 2)
// Get the teams by a valid Scheme ID where there aren't any matching Teams.
d, err = ss.Team().GetTeamsByScheme(s2.Id, 0, 100)
assert.NoError(t, err)
assert.Empty(t, d)
// Get the teams by an invalid Scheme ID.
d, err = ss.Team().GetTeamsByScheme(model.NewId(), 0, 100)
assert.NoError(t, err)
assert.Empty(t, d)
}
func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
t1 := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
SchemeId: &s1,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
tm1 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "team_admin team_user",
}
tm2 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "team_user",
}
tm3 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "something_else",
}
memberships, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{tm1, tm2, tm3}, -1)
require.NoError(t, nErr)
require.Len(t, memberships, 3)
tm1 = memberships[0]
tm2 = memberships[1]
tm3 = memberships[2]
lastDoneTeamId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
res, e := ss.Team().MigrateTeamMembers(lastDoneTeamId, lastDoneUserId)
if assert.NoError(t, e) {
if res == nil {
break
}
lastDoneTeamId = res["TeamId"]
lastDoneUserId = res["UserId"]
}
}
tm1b, err := ss.Team().GetMember(context.Background(), tm1.TeamId, tm1.UserId)
assert.NoError(t, err)
assert.Equal(t, "", tm1b.ExplicitRoles)
assert.True(t, tm1b.SchemeUser)
assert.True(t, tm1b.SchemeAdmin)
tm2b, err := ss.Team().GetMember(context.Background(), tm2.TeamId, tm2.UserId)
assert.NoError(t, err)
assert.Equal(t, "", tm2b.ExplicitRoles)
assert.True(t, tm2b.SchemeUser)
assert.False(t, tm2b.SchemeAdmin)
tm3b, err := ss.Team().GetMember(context.Background(), tm3.TeamId, tm3.UserId)
assert.NoError(t, err)
assert.Equal(t, "something_else", tm3b.ExplicitRoles)
assert.False(t, tm3b.SchemeUser)
assert.False(t, tm3b.SchemeAdmin)
}
func testResetAllTeamSchemes(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t1, err = ss.Team().Save(t1)
require.NoError(t, err)
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
assert.Equal(t, s1.Id, *t1.SchemeId)
assert.Equal(t, s1.Id, *t2.SchemeId)
res := ss.Team().ResetAllTeamSchemes()
assert.NoError(t, res)
t1, err = ss.Team().Get(t1.Id)
require.NoError(t, err)
t2, err = ss.Team().Get(t2.Id)
require.NoError(t, err)
assert.Equal(t, "", *t1.SchemeId)
assert.Equal(t, "", *t2.SchemeId)
}
func testTeamStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
m1 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_post_all_public team_user team_admin",
}
m2 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_user custom_role team_admin another_custom_role",
}
m3 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_user",
}
m4 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "custom_only",
}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
require.NoError(t, (ss.Team().ClearAllCustomRoleAssignments()))
r1, err := ss.Team().GetMember(context.Background(), m1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Equal(t, m1.ExplicitRoles, r1.Roles)
r2, err := ss.Team().GetMember(context.Background(), m2.TeamId, m2.UserId)
require.NoError(t, err)
assert.Equal(t, "team_user team_admin", r2.Roles)
r3, err := ss.Team().GetMember(context.Background(), m3.TeamId, m3.UserId)
require.NoError(t, err)
assert.Equal(t, m3.ExplicitRoles, r3.Roles)
r4, err := ss.Team().GetMember(context.Background(), m4.TeamId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, "", r4.Roles)
}
func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
count1, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(0), count1)
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
_, err = ss.Team().Save(t1)
require.NoError(t, err)
count2, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(1), count2)
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
_, err = ss.Team().Save(t2)
require.NoError(t, err)
count3, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count3)
t3 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(t3)
require.NoError(t, err)
count4, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count4)
t4 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
DeleteAt: model.GetMillis(),
}
_, err = ss.Team().Save(t4)
require.NoError(t, err)
count5, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count5)
}
func testTeamStoreGetAllForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
d1, err := ss.Team().GetAllForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, team := range d1 {
if team.Id == t1.Id {
found = true
assert.Equal(t, t1.Id, team.Id)
assert.Nil(t, team.SchemeId)
assert.Equal(t, t1.Name, team.Name)
}
}
assert.True(t, found)
}
func testTeamStoreGetTeamMembersForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = NewTestId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = NewTestId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
require.NoError(t, nErr)
d1, err := ss.Team().GetTeamMembersForExport(u1.Id)
assert.NoError(t, err)
assert.Len(t, d1, 1)
tmfe1 := d1[0]
assert.Equal(t, t1.Id, tmfe1.TeamId)
assert.Equal(t, u1.Id, tmfe1.UserId)
assert.Equal(t, t1.Name, tmfe1.TeamName)
}
func testGroupSyncedTeamCount(t *testing.T, ss store.Store) {
team1, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
})
require.NoError(t, err)
require.True(t, team1.IsGroupConstrained())
defer ss.Team().PermanentDelete(team1.Id)
team2, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamInvite,
})
require.NoError(t, err)
require.False(t, team2.IsGroupConstrained())
defer ss.Team().PermanentDelete(team2.Id)
count, err := ss.Team().GroupSyncedTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
team2.GroupConstrained = model.NewBool(true)
team2, err = ss.Team().Update(team2)
require.NoError(t, err)
require.True(t, team2.IsGroupConstrained())
countAfter, err := ss.Team().GroupSyncedTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testGetNewTeamMembersSince(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
})
require.NoError(t, err)
_, _, err = ss.Team().GetNewTeamMembersSince(team.Id, 0, 0, 1000)
require.NoError(t, err)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestTermsOfServiceStore(t *testing.T, ss store.Store) {
t.Run("TestSaveTermsOfService", func(t *testing.T) { testSaveTermsOfService(t, ss) })
t.Run("TestGetLatestTermsOfService", func(t *testing.T) { testGetLatestTermsOfService(t, ss) })
t.Run("TestGetTermsOfService", func(t *testing.T) { testGetTermsOfService(t, ss) })
}
func cleanUpTOS(ss store.Store) {
// Clearing out the table before starting the test.
// Otherwise the row inserted by the previous Save call from testSaveTermsOfService
// gets picked up.
// We call DropAllTables but we actually need to delete only TermsOfService.
// However, there is no straightforward way to just clear that table without introducing
// new methods. So we use the hammer.
ss.DropAllTables()
}
func testSaveTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service", UserId: u1.Id}
savedTermsOfService, err := ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
require.Len(t, savedTermsOfService.Id, 26, "Id should have been populated")
require.NotEqual(t, savedTermsOfService.CreateAt, 0, "Create at should have been populated")
}
func testGetLatestTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service 2", UserId: u1.Id}
_, err = ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
fetchedTermsOfService, err := ss.TermsOfService().GetLatest(true)
require.NoError(t, err)
assert.Equal(t, termsOfService.Text, fetchedTermsOfService.Text)
assert.Equal(t, termsOfService.UserId, fetchedTermsOfService.UserId)
}
func testGetTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service", UserId: u1.Id}
_, err = ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
r1, err := ss.TermsOfService().Get("an_invalid_id", true)
assert.Error(t, err)
assert.Nil(t, r1)
receivedTermsOfService, err := ss.TermsOfService().Get(termsOfService.Id, true)
assert.NoError(t, err)
assert.Equal(t, "terms of service", receivedTermsOfService.Text)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ThreadStorePopulation", func(t *testing.T) { testThreadStorePopulation(t, ss) })
t.Run("ThreadStorePermanentDeleteBatchForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchForRetentionPolicies(t, ss)
})
t.Run("ThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t, ss, s)
})
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, ss) })
t.Run("GetVarious", func(t *testing.T) { testVarious(t, ss) })
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
}
func testThreadStorePopulation(t *testing.T, ss store.Store) {
makeSomePosts := func(urgent bool) []*model.Post {
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u, err := ss.User().Save(&u1)
require.NoError(t, err)
c, err2 := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Type: model.ChannelTypeOpen,
Name: model.NewId(),
}, 999)
require.NoError(t, err2)
_, err44 := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
MsgCount: 0,
})
require.NoError(t, err44)
o := model.Post{}
o.ChannelId = c.Id
o.UserId = u.Id
o.Message = NewTestId()
if urgent {
o.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
}
}
otmp, err3 := ss.Post().Save(&o)
require.NoError(t, err3)
o2 := model.Post{}
o2.ChannelId = c.Id
o2.UserId = model.NewId()
o2.RootId = otmp.Id
o2.Message = NewTestId()
o3 := model.Post{}
o3.ChannelId = c.Id
o3.UserId = u.Id
o3.RootId = otmp.Id
o3.Message = NewTestId()
o4 := model.Post{}
o4.ChannelId = c.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
newPosts, errIdx, err3 := ss.Post().SaveMultiple([]*model.Post{&o2, &o3, &o4})
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
olist, _ := ss.Post().Get(context.Background(), otmp.Id, opts, "", map[string]bool{})
o1 := olist.Posts[olist.Order[0]]
newPosts = append([]*model.Post{o1}, newPosts...)
require.NoError(t, err3, "couldn't save item")
require.Equal(t, -1, errIdx)
require.Len(t, newPosts, 4)
require.Equal(t, int64(2), newPosts[0].ReplyCount)
require.Equal(t, int64(2), newPosts[1].ReplyCount)
require.Equal(t, int64(2), newPosts[2].ReplyCount)
require.Equal(t, int64(0), newPosts[3].ReplyCount)
return newPosts
}
t.Run("Save replies creates a thread", func(t *testing.T) {
newPosts := makeSomePosts(false)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5 := model.Post{}
o5.ChannelId = channel.Id
o5.UserId = model.NewId()
o5.RootId = newPosts[0].Id
o5.Message = NewTestId()
_, _, err = ss.Post().SaveMultiple([]*model.Post{&o5})
require.NoError(t, err, "couldn't save item")
thread, err = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(3), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, o5.UserId}, thread.Participants)
})
t.Run("Delete a reply updates count on a thread", func(t *testing.T) {
newPosts := makeSomePosts(false)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
err = ss.Post().Delete(newPosts[1].Id, 1234, model.NewId())
require.NoError(t, err, "couldn't delete post")
thread, err = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(1), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId}, thread.Participants)
})
t.Run("Update reply should update the UpdateAt of the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.RootId = model.NewId()
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.RootId
newPosts, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost})
require.NoError(t, err)
thread1, err := ss.Thread().Get(newPosts[0].RootId)
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
require.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
replyPost2 := model.Post{}
replyPost2.ChannelId = rootPost.ChannelId
replyPost2.UserId = model.NewId()
replyPost2.Message = NewTestId()
replyPost2.RootId = rootPost.Id
replyPost3 := model.Post{}
replyPost3.ChannelId = rootPost.ChannelId
replyPost3.UserId = model.NewId()
replyPost3.Message = NewTestId()
replyPost3.RootId = rootPost.Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err)
rrootPost2, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
require.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
thread2, err := ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.Greater(t, thread2.LastReplyAt, thread1.LastReplyAt)
})
t.Run("Deleting reply should update the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
rootPost, err := ss.Post().Save(&o1)
require.NoError(t, err)
o2 := model.Post{}
o2.RootId = rootPost.Id
o2.ChannelId = rootPost.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
replyPost, err := ss.Post().Save(&o2)
require.NoError(t, err)
o3 := model.Post{}
o3.RootId = rootPost.Id
o3.ChannelId = rootPost.ChannelId
o3.UserId = o2.UserId
o3.Message = NewTestId()
replyPost2, err := ss.Post().Save(&o3)
require.NoError(t, err)
o4 := model.Post{}
o4.RootId = rootPost.Id
o4.ChannelId = rootPost.ChannelId
o4.UserId = model.NewId()
o4.Message = NewTestId()
replyPost3, err := ss.Post().Save(&o4)
require.NoError(t, err)
thread, err := ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 3)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
err = ss.Post().Delete(replyPost2.Id, 123, model.NewId())
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 2)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
err = ss.Post().Delete(replyPost.Id, 123, model.NewId())
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 1)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost3.UserId})
})
t.Run("Deleting root post should delete the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
newPosts1, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost})
require.NoError(t, err)
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = newPosts1[0].Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost})
require.NoError(t, err)
thread1, err := ss.Thread().Get(newPosts1[0].Id)
require.NoError(t, err)
require.EqualValues(t, thread1.ReplyCount, 1)
require.Len(t, thread1.Participants, 1)
err = ss.Post().PermanentDeleteByUser(rootPost.UserId)
require.NoError(t, err)
thread2, _ := ss.Thread().Get(rootPost.Id)
require.Nil(t, thread2)
})
t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: true,
}
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
require.Equal(t, int64(0), tm.LastViewed)
// No update since array has same elements.
th, e := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, e)
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, th.Participants)
opts.UpdateViewedTimestamp = true
_, e = ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err2)
require.Greater(t, m2.LastViewed, int64(0))
// Adding a new participant
_, e = ss.Thread().MaintainMembership("newuser", newPosts[0].Id, opts)
require.NoError(t, e)
th, e = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, e)
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, "newuser"}, th.Participants)
})
t.Run("Thread membership 'viewed' timestamp is updated properly for new membership", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: false,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
require.NotEqual(t, int64(0), tm.LastViewed)
})
t.Run("Updating post does not make thread unread", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
m, err := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, err)
th, err := ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(2), th.UnreadReplies)
m.LastViewed = newPosts[2].UpdateAt + 1
_, err = ss.Thread().UpdateMembership(m)
require.NoError(t, err)
th, err = ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(0), th.UnreadReplies)
editedPost := newPosts[2].Clone()
editedPost.Message = "This is an edited post"
_, err = ss.Post().Update(editedPost, newPosts[2])
require.NoError(t, err)
th, err = ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(0), th.UnreadReplies)
})
t.Run("Empty participantID should not appear in thread response", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: true,
}
m, err := ss.Thread().MaintainMembership("", newPosts[0].Id, opts)
require.NoError(t, err)
m.UserId = newPosts[0].UserId
th, err := ss.Thread().GetThreadForUser(m, true, false)
require.NoError(t, err)
for _, user := range th.Participants {
require.NotNil(t, user)
}
})
t.Run("Get unread reply counts for thread", func(t *testing.T) {
t.Skip("MM-41797")
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1)
unreads, err := ss.Thread().GetThreadUnreadReplyCount(m)
require.NoError(t, err)
require.Equal(t, int64(0), unreads)
err = ss.Thread().MarkAsRead(newPosts[0].UserId, newPosts[0].Id, newPosts[0].CreateAt)
require.NoError(t, err)
m, err = ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err)
unreads, err = ss.Thread().GetThreadUnreadReplyCount(m)
require.NoError(t, err)
require.Equal(t, int64(2), unreads)
})
testCases := []bool{true, false}
for _, isUrgent := range testCases {
t.Run("Return is urgent for user thread/s", func(t *testing.T) {
newPosts := makeSomePosts(isUrgent)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
userID := newPosts[0].UserId
_, e := ss.Thread().MaintainMembership(userID, newPosts[0].Id, opts)
require.NoError(t, e)
m, e := ss.Thread().GetMembershipForUser(userID, newPosts[0].Id)
require.NoError(t, e)
th, e := ss.Thread().GetThreadForUser(m, false, true)
require.NoError(t, e)
require.Equal(t, isUrgent, th.IsUrgent)
threads, e := ss.Thread().GetThreadsForUser(userID, "", model.GetUserThreadsOpts{IncludeIsUrgent: true})
require.NoError(t, e)
require.Equal(t, isUrgent, threads[0].IsUrgent)
})
}
}
func threadStoreCreateReply(t *testing.T, ss store.Store, channelID, postID, userID string, createAt int64) *model.Post {
t.Helper()
reply, err := ss.Post().Save(&model.Post{
ChannelId: channelID,
UserId: userID,
CreateAt: createAt,
RootId: postID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
return reply
}
func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
thread, err := ss.Thread().Get(post.Id)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := thread.LastReplyAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
thread, err = ss.Thread().Get(post.Id)
assert.NoError(t, err)
assert.Nil(t, thread, "thread should have been deleted by channel policy")
// create a new thread
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
thread, err = ss.Thread().Get(post.Id)
require.NoError(t, err)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = thread.LastReplyAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().Get(post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
thread, err = ss.Thread().Get(post.Id)
assert.NoError(t, err)
assert.Nil(t, thread, "thread should have been deleted by team policy")
}
func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t *testing.T, ss store.Store, s SqlStore) {
const limit = 1000
userID := model.NewId()
createThreadMembership := func(userID, postID string) *model.ThreadMembership {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
threadMembership, err := ss.Thread().GetMembershipForUser(userID, postID)
require.NoError(t, err)
return threadMembership
}
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
threadMembership := createThreadMembership(userID, post.Id)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := threadMembership.LastUpdated + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by channel policy")
// create a new thread membership
threadMembership = createThreadMembership(userID, post.Id)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = threadMembership.LastUpdated + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by team policy")
// create a new thread membership
createThreadMembership(userID, post.Id)
// Delete team policy and thread
err = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err)
_, err = s.GetMasterX().Exec("DELETE FROM Threads WHERE PostId='" + post.Id + "'")
require.NoError(t, err)
deleted, err := ss.Thread().DeleteOrphanedRows(1000)
require.NoError(t, err)
require.NotZero(t, deleted)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted because thread no longer exists")
}
func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) {
userID := model.NewId()
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: userID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
createThreadMembership(userID, post.Id)
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(1), teamsUnread[team1.Id].ThreadCount)
post, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: userID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
createThreadMembership(userID, post.Id)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: userID,
Message: model.NewRandomString(10),
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, model.GetMillis())
createThreadMembership(userID, post2.Id)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id, team2.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 2)
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: true,
}
_, err = ss.Thread().MaintainMembership(userID, post2.Id, opts)
require.NoError(t, err)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team2.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadUrgentMentionCount)
}
type byPostId []*model.Post
func (a byPostId) Len() int { return len(a) }
func (a byPostId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byPostId) Less(i, j int) bool { return a[i].Id < a[j].Id }
func testVarious(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string, isMention bool) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: isMention,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
viewThread := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
user1, err := ss.User().Save(&model.User{
Username: "user1" + model.NewId(),
Email: MakeEmail(),
})
require.NoError(t, err)
user2, err := ss.User().Save(&model.User{
Username: "user2" + model.NewId(),
Email: MakeEmail(),
})
require.NoError(t, err)
user1ID := user1.Id
user2ID := user2.Id
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "Team2",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team1channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Channel2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
dm1, err := ss.Channel().CreateDirectChannel(&model.User{Id: user1ID}, &model.User{Id: user2ID})
require.NoError(t, err)
gm1, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM",
Name: "gm" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
team1channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team1channel1post2, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team1channel1post3, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
team2channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team2channel1post2deleted, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
dm1post1, err := ss.Post().Save(&model.Post{
ChannelId: dm1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
gm1post1, err := ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
postNames := map[string]string{
team1channel1post1.Id: "team1channel1post1",
team1channel1post2.Id: "team1channel1post2",
team1channel1post3.Id: "team1channel1post3",
team2channel1post1.Id: "team2channel1post1",
team2channel1post2deleted.Id: "team2channel1post2deleted",
dm1post1.Id: "dm1post1",
gm1post1.Id: "gm1post1",
}
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post3.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team2channel1.Id, team2channel1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team2channel1.Id, team2channel1post2deleted.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, dm1.Id, dm1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, gm1.Id, gm1post1.Id, user2ID, model.GetMillis())
// Create thread memberships, with simulated unread mentions.
createThreadMembership(user1ID, team1channel1post1.Id, false)
createThreadMembership(user1ID, team1channel1post2.Id, false)
createThreadMembership(user1ID, team1channel1post3.Id, true)
createThreadMembership(user1ID, team2channel1post1.Id, false)
createThreadMembership(user1ID, team2channel1post2deleted.Id, false)
createThreadMembership(user1ID, dm1post1.Id, false)
createThreadMembership(user1ID, gm1post1.Id, true)
// Have user1 view a subset of the threads
viewThread(user1ID, team1channel1post1.Id)
viewThread(user2ID, team1channel1post2.Id)
viewThread(user1ID, team2channel1post1.Id)
viewThread(user1ID, dm1post1.Id)
// Add reply to a viewed thread to confirm it's unread again.
time.Sleep(2 * time.Millisecond)
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
// Actually make team2channel1post2deleted deleted
err = ss.Post().Delete(team2channel1post2deleted.Id, model.GetMillis(), user1ID)
require.NoError(t, err)
// Re-fetch posts to ensure metadata up-to-date
allPosts := []*model.Post{
team1channel1post1,
team1channel1post2,
team1channel1post3,
team2channel1post1,
team2channel1post2deleted,
dm1post1,
gm1post1,
}
for i := range allPosts {
updatedPost, err := ss.Post().GetSingle(allPosts[i].Id, true)
require.NoError(t, err)
// Fix some inconsistencies with how the post store returns posts vs. how the
// thread store returns it.
if updatedPost.RemoteId == nil {
updatedPost.RemoteId = new(string)
}
// Also, we don't populate ReplyCount for posts when querying threads, so don't
// assert same.
updatedPost.ReplyCount = 0
updatedPost.ShallowCopy(allPosts[i])
}
t.Run("GetTotalUnreadThreads", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
gm1post1, // (no unread threads in team2)
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post2deleted, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadThreads, err := ss.Thread().GetTotalUnreadThreads(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadThreads)
})
}
})
t.Run("GetTotalThreads", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
team2channel1post1, dm1post1, gm1post1,
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalThreads, err := ss.Thread().GetTotalThreads(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalThreads)
})
}
})
t.Run("GetTotalUnreadMentions", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3, gm1post1,
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadMentions, err := ss.Thread().GetTotalUnreadMentions(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadMentions)
})
}
})
t.Run("GetTotalUnreadUrgentMentions", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3,
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadUrgentMentions, err := ss.Thread().GetTotalUnreadUrgentMentions(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadUrgentMentions)
})
}
})
assertThreadPosts := func(t *testing.T, threads []*model.ThreadResponse, expectedPosts []*model.Post) {
t.Helper()
actualPosts := make([]*model.Post, 0, len(threads))
actualPostNames := make([]string, 0, len(threads))
for _, thread := range threads {
actualPosts = append(actualPosts, thread.Post)
postName, ok := postNames[thread.PostId]
require.True(t, ok, "failed to find actual %s in post names", thread.PostId)
actualPostNames = append(actualPostNames, postName)
}
sort.Strings(actualPostNames)
expectedPostNames := make([]string, 0, len(expectedPosts))
for _, post := range expectedPosts {
postName, ok := postNames[post.Id]
require.True(t, ok, "failed to find expected %s in post names", post.Id)
expectedPostNames = append(expectedPostNames, postName)
}
sort.Strings(expectedPostNames)
assert.Equal(t, expectedPostNames, actualPostNames)
// Check posts themselves
sort.Sort(byPostId(expectedPosts))
sort.Sort(byPostId(actualPosts))
if assert.Len(t, actualPosts, len(expectedPosts)) {
for i := range actualPosts {
assert.Equal(t, expectedPosts[i], actualPosts[i], "mismatch comparing expected post %s with actual post %s", postNames[expectedPosts[i].Id], postNames[actualPosts[i].Id])
}
} else {
assert.Equal(t, expectedPosts, actualPosts)
}
// Check common fields between threads and posts.
for _, thread := range threads {
assert.Equal(t, thread.DeleteAt, thread.Post.DeleteAt, "expected Thread.DeleteAt == Post.DeleteAt")
}
}
t.Run("GetThreadsForUser", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
}},
{"team1, user1, unread", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
}},
{"team1, user1, unread + deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
team2channel1post1, dm1post1, gm1post1,
}},
{"team2, user1, unread", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
gm1post1, // (no unread in team2)
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
}},
{"team2, user1, unread + deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team2channel1post2deleted, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
threads, err := ss.Thread().GetThreadsForUser(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assertThreadPosts(t, threads, testCase.ExpectedThreads)
})
}
})
}
func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) {
postingUserId := model.NewId()
userAID := model.NewId()
userBID := model.NewId()
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: "channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel2",
Name: "channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
assertThreadReplyCount := func(t *testing.T, userID string, count int64) {
t.Helper()
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, false)
require.NoError(t, err)
require.Len(t, teamsUnread, 1, "unexpected unread teams count")
assert.Equal(t, count, teamsUnread[team1.Id].ThreadCount, "unexpected thread count")
}
t.Run("empty set of channels", func(t *testing.T) {
err := ss.Thread().MarkAllAsReadByChannels(model.NewId(), []string{})
require.NoError(t, err)
})
t.Run("single channel", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
RootId: post.Id,
Message: "Reply",
})
require.NoError(t, err)
createThreadMembership(userAID, post.Id)
createThreadMembership(userBID, post.Id)
assertThreadReplyCount(t, userAID, 1)
assertThreadReplyCount(t, userBID, 1)
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 1)
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 0)
})
t.Run("multiple channels", func(t *testing.T) {
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
RootId: post1.Id,
Message: "Reply",
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: postingUserId,
RootId: post2.Id,
Message: "Reply",
})
require.NoError(t, err)
createThreadMembership(userAID, post1.Id)
createThreadMembership(userBID, post1.Id)
createThreadMembership(userAID, post2.Id)
createThreadMembership(userBID, post2.Id)
assertThreadReplyCount(t, userAID, 2)
assertThreadReplyCount(t, userBID, 2)
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id, channel2.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 2)
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id, channel2.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 0)
})
}
func testGetTopThreads(t *testing.T, ss store.Store) {
// create two users
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
t.Run("test get top team threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 2000)
// get top threads
topThreadsInTeam, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeam.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsInTeam.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].UserInformation.Id, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsInTeam.Items[0].Post.Message, post1.Message)
// require second element to be post2 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[1].PostId, post2.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsInTeam.Items[1].UserId, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].Post.Message, post2.Message)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsInTeam.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.Id, post2.Id)
})
t.Run("test get top user threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u3.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post3.Id, post3.UserId, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(post1.UserId, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post3.Id, opts)
require.NoError(t, err)
// get top threads by user
topThreadsByUser1, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post1.UserId, 12, 0, limit)
require.NoError(t, err)
topThreadsByUser2, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post2.UserId, 12, 0, limit)
require.NoError(t, err)
// require length of top threads by users to be 1,2 respectively
require.Len(t, topThreadsByUser1.Items, 1)
require.Len(t, topThreadsByUser2.Items, 2)
// require first element of topThreadsByUser1 to be post1 with 2 replyCount=2
require.Equal(t, topThreadsByUser1.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsByUser1.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser1.Items[0].Post.Message, post1.Message)
require.Equal(t, topThreadsByUser1.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsByUser1.Items[0].UserInformation.Id, post1.UserId)
// require elements of topThreadsByUser2 to be post2 and post3 respectively
require.Equal(t, topThreadsByUser2.Items[0].PostId, post2.Id)
require.Equal(t, topThreadsByUser2.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser2.Items[0].Post.Message, post2.Message)
require.Equal(t, topThreadsByUser2.Items[0].UserId, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[0].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[1].PostId, post3.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsByUser2.Items[1].Post.Message, post3.Message)
require.Equal(t, topThreadsByUser2.Items[1].UserId, post3.UserId)
require.Equal(t, topThreadsByUser2.Items[1].UserInformation.Id, post3.UserId)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsByUser1.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.Id, post3.Id)
})
t.Run("test get top threads only from given teamid", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, 2000)
// assert that getting top threads from teamid 1 doesn't have post1.Id
topThreadsTeam2, err := ss.Thread().GetTopThreadsForTeamSince(team2.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam2.Items, 1)
require.Equal(t, topThreadsTeam2.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads only from non-direct channels", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, u1.Id, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(u1.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u1.Id, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post2.Id, opts)
require.NoError(t, err)
// assert that getting top threads from teamid 1 doesn't have DMs
topThreadsTeam1, err := ss.Thread().GetTopThreadsForTeamSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam1.Items, 1)
require.Equal(t, topThreadsTeam1.Items[0].Post.Id, post2.Id)
// assert that getting top threads from user 1 doesn't contain dm threads.
topUserThreads, err := ss.Thread().GetTopThreadsForUserSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topUserThreads.Items, 1)
require.Equal(t, topUserThreads.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads doesn't exceed duration", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
// post 2 has replies after 10 ms unix time.
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
CreateAt: 1,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 10)
// get top threads
topThreadsInTeamNewer, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamNewer.Items, 1)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamNewer.Items[0].PostId, post1.Id)
// get top threads
topThreadsInTeamOlder, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 9, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamOlder.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamOlder.Items[1].PostId, post2.Id)
})
}
func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
assertThreadReplyCount := func(t *testing.T, userID, teamID string, count int64, message string) {
t.Helper()
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{teamID}, true)
require.NoError(t, err)
require.Lenf(t, teamsUnread, 1, "unexpected unread teams count: %s", message)
assert.Equalf(t, count, teamsUnread[teamID].ThreadCount, "unexpected thread count: %s", message)
}
postingUserId := model.NewId()
userAID := model.NewId()
userBID := model.NewId()
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team1" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team1channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Team1: Channel1",
Name: "team1channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team1channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Team1: Channel2",
Name: "team1channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "Team2",
Name: "team2" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Team2: Channel1",
Name: "team2channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Team2: Channel2",
Name: "team2channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team1channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
RootId: team1channel1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team1channel2post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
RootId: team1channel2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team2channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
RootId: team2channel1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team2channel2post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
RootId: team2channel2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
gm1, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM1",
Name: "gm1" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
gm1post1, err := ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: postingUserId,
RootId: gm1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
gm2, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM1",
Name: "gm1" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
gm2post1, err := ss.Post().Save(&model.Post{
ChannelId: gm2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: gm2.Id,
UserId: postingUserId,
RootId: gm2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
t.Run("empty team", func(t *testing.T) {
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), "")
require.NoError(t, err)
})
t.Run("unknown team", func(t *testing.T) {
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), model.NewId())
require.NoError(t, err)
})
t.Run("team1", func(t *testing.T) {
createThreadMembership(userAID, team1channel1post1.Id)
createThreadMembership(userBID, team1channel1post1.Id)
createThreadMembership(userAID, team1channel2post1.Id)
createThreadMembership(userBID, team1channel2post1.Id)
createThreadMembership(userAID, team2channel1post1.Id)
createThreadMembership(userBID, team2channel1post1.Id)
// Note that GMs (and similarly, DMs) don't count towards this API.
createThreadMembership(userAID, gm1.Id)
createThreadMembership(userBID, gm1.Id)
createThreadMembership(userAID, gm2.Id)
createThreadMembership(userBID, gm2.Id)
assertThreadReplyCount(t, userAID, team1.Id, 2, "expected 2 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
err = ss.Thread().MarkAllAsReadByTeam(userAID, team1.Id)
require.NoError(t, err)
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
err = ss.Thread().MarkAllAsReadByTeam(userBID, team1.Id)
require.NoError(t, err)
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 0, "expected 0 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestTokensStore(t *testing.T, ss store.Store) {
t.Run("TokensCleanup", func(t *testing.T) { testTokensCleanup(t, ss) })
}
func testTokensCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
for i := 0; i < 10; i++ {
err := ss.Token().Save(&model.Token{
Token: model.NewRandomString(model.TokenSize),
CreateAt: now - int64(i),
Type: model.TokenTypeOAuth,
Extra: "",
})
require.NoError(t, err)
}
tokens, err := ss.Token().GetAllTokensByType(model.TokenTypeOAuth)
require.NoError(t, err)
assert.Len(t, tokens, 10)
ss.Token().Cleanup(now + int64(1))
tokens, err = ss.Token().GetAllTokensByType(model.TokenTypeOAuth)
require.NoError(t, err)
assert.Len(t, tokens, 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestTrueUpReviewStatusStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("CreateTrueUpReviewStatusRecord", func(t *testing.T) { testCreateTrueUpReviewStatus(t, ss) })
t.Run("GetTrueUpReviewStatus", func(t *testing.T) { testGetTrueUpReviewStatus(t, ss) })
t.Run("Update", func(t *testing.T) { testUpdateTrueUpReviewStatus(t, ss) })
}
func testCreateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
t.Run("create true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, reviewStatus.Completed, resp.Completed)
assert.Equal(t, reviewStatus.DueDate, resp.DueDate)
})
}
func testGetTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.August, 1, 0, 0, 0, 0, time.Local)
dueDate := utils.GetNextTrueUpReviewDueDate(now).UnixMilli()
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: dueDate,
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("get true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().GetTrueUpReviewStatus(dueDate)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}
func testUpdateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.April, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: false,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("save ", func(t *testing.T) {
reviewStatus.Completed = true
resp, err := ss.TrueUpReview().Update(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUploadSessionStore(t *testing.T, ss store.Store) {
t.Run("UploadSessionStoreSaveGet", func(t *testing.T) { testUploadSessionStoreSaveGet(t, ss) })
t.Run("UploadSessionStoreUpdate", func(t *testing.T) { testUploadSessionStoreUpdate(t, ss) })
t.Run("UploadSessionStoreGetForUser", func(t *testing.T) { testUploadSessionStoreGetForUser(t, ss) })
t.Run("UploadSessionStoreDelete", func(t *testing.T) { testUploadSessionStoreDelete(t, ss) })
}
func testUploadSessionStoreSaveGet(t *testing.T, ss store.Store) {
var session *model.UploadSession
t.Run("saving nil session should fail", func(t *testing.T) {
us, err := ss.UploadSession().Save(nil)
require.Error(t, err)
require.Nil(t, us)
})
t.Run("saving empty session should fail", func(t *testing.T) {
session = &model.UploadSession{}
us, err := ss.UploadSession().Save(session)
require.Error(t, err)
require.Nil(t, us)
})
t.Run("saving valid session should succeed", func(t *testing.T) {
session = &model.UploadSession{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
})
t.Run("getting non-existing session should fail", func(t *testing.T) {
us, err := ss.UploadSession().Get(context.Background(), "fake")
require.Error(t, err)
require.Nil(t, us)
})
t.Run("getting existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Get(context.Background(), session.Id)
require.NoError(t, err)
require.NotNil(t, us)
require.Equal(t, session, us)
})
}
func testUploadSessionStoreUpdate(t *testing.T, ss store.Store) {
session := &model.UploadSession{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
t.Run("updating nil session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(nil)
require.Error(t, err)
})
t.Run("updating invalid session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(&model.UploadSession{})
require.Error(t, err)
})
t.Run("updating non-existing session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(&model.UploadSession{})
require.Error(t, err)
})
t.Run("updating existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
us.FileOffset = 512
err = ss.UploadSession().Update(us)
require.NoError(t, err)
updated, err := ss.UploadSession().Get(context.Background(), us.Id)
require.NoError(t, err)
require.NotNil(t, us)
require.Equal(t, us, updated)
})
}
func testUploadSessionStoreGetForUser(t *testing.T, ss store.Store) {
userId := model.NewId()
sessions := []*model.UploadSession{
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test0",
FileSize: 1024,
Path: "/tmp/test0",
},
{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test1",
FileSize: 1024,
Path: "/tmp/test1",
},
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test2",
FileSize: 1024,
Path: "/tmp/test2",
},
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test3",
FileSize: 1024,
Path: "/tmp/test3",
},
}
t.Run("should return no sessions", func(t *testing.T) {
us, err := ss.UploadSession().GetForUser(userId)
require.NoError(t, err)
require.NotNil(t, us)
require.Empty(t, us)
})
for i := 0; i < len(sessions); i++ {
us, err := ss.UploadSession().Save(sessions[i])
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
// We need this to make sure the ordering is consistent.
time.Sleep(2 * time.Millisecond)
}
t.Run("should return existing sessions", func(t *testing.T) {
us, err := ss.UploadSession().GetForUser(userId)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
require.Len(t, us, 3)
require.Equal(t, sessions[0], us[0])
require.Equal(t, sessions[2], us[1])
require.Equal(t, sessions[3], us[2])
})
}
func testUploadSessionStoreDelete(t *testing.T, ss store.Store) {
session := &model.UploadSession{
Id: model.NewId(),
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
t.Run("deleting invalid id should fail", func(t *testing.T) {
err := ss.UploadSession().Delete("invalidId")
require.Error(t, err)
})
t.Run("deleting existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
err = ss.UploadSession().Delete(session.Id)
require.NoError(t, err)
us, err = ss.UploadSession().Get(context.Background(), us.Id)
require.Error(t, err)
require.Nil(t, us)
require.IsType(t, &store.ErrNotFound{}, err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUserAccessTokenStore(t *testing.T, ss store.Store) {
t.Run("UserAccessTokenSaveGetDelete", func(t *testing.T) { testUserAccessTokenSaveGetDelete(t, ss) })
t.Run("UserAccessTokenDisableEnable", func(t *testing.T) { testUserAccessTokenDisableEnable(t, ss) })
t.Run("UserAccessTokenSearch", func(t *testing.T) { testUserAccessTokenSearch(t, ss) })
}
func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) {
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: model.NewId(),
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, nErr := ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
result, terr := ss.UserAccessToken().Get(uat.Id)
require.NoError(t, terr)
require.Equal(t, result.Token, uat.Token, "received incorrect token after save")
received, err2 := ss.UserAccessToken().GetByToken(uat.Token)
require.NoError(t, err2)
require.Equal(t, received.Token, uat.Token, "received incorrect token after save")
_, nErr = ss.UserAccessToken().GetByToken("notarealtoken")
require.Error(t, nErr, "should have failed on bad token")
received2, err2 := ss.UserAccessToken().GetByUser(uat.UserId, 0, 100)
require.NoError(t, err2)
require.Equal(t, 1, len(received2), "received incorrect number of tokens after save")
result2, err := ss.UserAccessToken().GetAll(0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(result2), "received incorrect number of tokens after save")
nErr = ss.UserAccessToken().Delete(uat.Id)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, err, "should error - session should be deleted")
_, nErr = ss.UserAccessToken().GetByToken(s1.Token)
require.Error(t, nErr, "should error - access token should be deleted")
s2 := &model.Session{}
s2.UserId = uat.UserId
s2.Token = uat.Token
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, nErr = ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
nErr = ss.UserAccessToken().DeleteAllForUser(uat.UserId)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s2.Token)
require.Error(t, err, "should error - session should be deleted")
_, nErr = ss.UserAccessToken().GetByToken(s2.Token)
require.Error(t, nErr, "should error - access token should be deleted")
}
func testUserAccessTokenDisableEnable(t *testing.T, ss store.Store) {
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: model.NewId(),
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, nErr := ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
nErr = ss.UserAccessToken().UpdateTokenDisable(uat.Id)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, err, "should error - session should be deleted")
s2 := &model.Session{}
s2.UserId = uat.UserId
s2.Token = uat.Token
_, err = ss.Session().Save(s2)
require.NoError(t, err)
nErr = ss.UserAccessToken().UpdateTokenEnable(uat.Id)
require.NoError(t, nErr)
}
func testUserAccessTokenSearch(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: u1.Id,
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
_, nErr := ss.Session().Save(s1)
require.NoError(t, nErr)
_, nErr = ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
received, nErr := ss.UserAccessToken().Search(uat.Id)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
received, nErr = ss.UserAccessToken().Search(uat.UserId)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
received, nErr = ss.UserAccessToken().Search(u1.Username)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
)
func cleanupStatusStore(t *testing.T, s SqlStore) {
_, execerr := s.GetMasterX().Exec(`DELETE FROM Status`)
require.NoError(t, execerr)
}
func TestUserStore(t *testing.T, ss store.Store, s SqlStore) {
users, err := ss.User().GetAll()
require.NoError(t, err, "failed cleaning up test users")
for _, u := range users {
err := ss.User().PermanentDelete(u.Id)
require.NoError(t, err, "failed cleaning up test user %s", u.Username)
}
t.Run("IsEmpty", func(t *testing.T) { testIsEmpty(t, ss) })
t.Run("Count", func(t *testing.T) { testCount(t, ss) })
t.Run("AnalyticsActiveCount", func(t *testing.T) { testUserStoreAnalyticsActiveCount(t, ss, s) })
t.Run("AnalyticsActiveCountForPeriod", func(t *testing.T) { testUserStoreAnalyticsActiveCountForPeriod(t, ss, s) })
t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) })
t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) })
t.Run("AnalyticsGetGuestCount", func(t *testing.T) { testUserStoreAnalyticsGetGuestCount(t, ss) })
t.Run("AnalyticsGetExternalUsers", func(t *testing.T) { testUserStoreAnalyticsGetExternalUsers(t, ss) })
t.Run("Save", func(t *testing.T) { testUserStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testUserStoreUpdate(t, ss) })
t.Run("UpdateUpdateAt", func(t *testing.T) { testUserStoreUpdateUpdateAt(t, ss) })
t.Run("UpdateFailedPasswordAttempts", func(t *testing.T) { testUserStoreUpdateFailedPasswordAttempts(t, ss) })
t.Run("Get", func(t *testing.T) { testUserStoreGet(t, ss) })
t.Run("GetAllUsingAuthService", func(t *testing.T) { testGetAllUsingAuthService(t, ss) })
t.Run("GetAllProfiles", func(t *testing.T) { testUserStoreGetAllProfiles(t, ss) })
t.Run("GetProfiles", func(t *testing.T) { testUserStoreGetProfiles(t, ss) })
t.Run("GetProfilesInChannel", func(t *testing.T) { testUserStoreGetProfilesInChannel(t, ss) })
t.Run("GetProfilesInChannelByStatus", func(t *testing.T) { testUserStoreGetProfilesInChannelByStatus(t, ss, s) })
t.Run("GetProfilesInChannelByAdmin", func(t *testing.T) { testUserStoreGetProfilesInChannelByAdmin(t, ss, s) })
t.Run("GetProfilesWithoutTeam", func(t *testing.T) { testUserStoreGetProfilesWithoutTeam(t, ss) })
t.Run("GetAllProfilesInChannel", func(t *testing.T) { testUserStoreGetAllProfilesInChannel(t, ss) })
t.Run("GetProfilesNotInChannel", func(t *testing.T) { testUserStoreGetProfilesNotInChannel(t, ss) })
t.Run("GetProfilesByIds", func(t *testing.T) { testUserStoreGetProfilesByIds(t, ss) })
t.Run("GetProfileByGroupChannelIdsForUser", func(t *testing.T) { testUserStoreGetProfileByGroupChannelIdsForUser(t, ss) })
t.Run("GetProfilesByUsernames", func(t *testing.T) { testUserStoreGetProfilesByUsernames(t, ss) })
t.Run("GetSystemAdminProfiles", func(t *testing.T) { testUserStoreGetSystemAdminProfiles(t, ss) })
t.Run("GetByEmail", func(t *testing.T) { testUserStoreGetByEmail(t, ss) })
t.Run("GetByAuthData", func(t *testing.T) { testUserStoreGetByAuthData(t, ss) })
t.Run("GetByUsername", func(t *testing.T) { testUserStoreGetByUsername(t, ss) })
t.Run("GetForLogin", func(t *testing.T) { testUserStoreGetForLogin(t, ss) })
t.Run("UpdatePassword", func(t *testing.T) { testUserStoreUpdatePassword(t, ss) })
t.Run("Delete", func(t *testing.T) { testUserStoreDelete(t, ss) })
t.Run("UpdateAuthData", func(t *testing.T) { testUserStoreUpdateAuthData(t, ss) })
t.Run("ResetAuthDataToEmailForUsers", func(t *testing.T) { testUserStoreResetAuthDataToEmailForUsers(t, ss) })
t.Run("UserUnreadCount", func(t *testing.T) { testUserUnreadCount(t, ss) })
t.Run("UpdateMfaSecret", func(t *testing.T) { testUserStoreUpdateMfaSecret(t, ss) })
t.Run("UpdateMfaActive", func(t *testing.T) { testUserStoreUpdateMfaActive(t, ss) })
t.Run("GetRecentlyActiveUsersForTeam", func(t *testing.T) { testUserStoreGetRecentlyActiveUsersForTeam(t, ss, s) })
t.Run("GetNewUsersForTeam", func(t *testing.T) { testUserStoreGetNewUsersForTeam(t, ss) })
t.Run("Search", func(t *testing.T) { testUserStoreSearch(t, ss) })
t.Run("SearchNotInChannel", func(t *testing.T) { testUserStoreSearchNotInChannel(t, ss) })
t.Run("SearchInChannel", func(t *testing.T) { testUserStoreSearchInChannel(t, ss) })
t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) })
t.Run("SearchWithoutTeam", func(t *testing.T) { testUserStoreSearchWithoutTeam(t, ss) })
t.Run("SearchInGroup", func(t *testing.T) { testUserStoreSearchInGroup(t, ss) })
t.Run("SearchNotInGroup", func(t *testing.T) { testUserStoreSearchNotInGroup(t, ss) })
t.Run("GetProfilesNotInTeam", func(t *testing.T) { testUserStoreGetProfilesNotInTeam(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) })
t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) })
t.Run("GetTeamGroupUsers", func(t *testing.T) { testUserStoreGetTeamGroupUsers(t, ss) })
t.Run("GetChannelGroupUsers", func(t *testing.T) { testUserStoreGetChannelGroupUsers(t, ss) })
t.Run("PromoteGuestToUser", func(t *testing.T) { testUserStorePromoteGuestToUser(t, ss) })
t.Run("DemoteUserToGuest", func(t *testing.T) { testUserStoreDemoteUserToGuest(t, ss) })
t.Run("DeactivateGuests", func(t *testing.T) { testDeactivateGuests(t, ss) })
t.Run("ResetLastPictureUpdate", func(t *testing.T) { testUserStoreResetLastPictureUpdate(t, ss) })
t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, ss) })
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, ss) })
t.Run("GetFirstSystemAdminID", func(t *testing.T) { testUserStoreGetFirstSystemAdminID(t, ss) })
}
func testUserStoreSave(t *testing.T, ss store.Store) {
teamId := model.NewId()
maxUsersPerTeam := 50
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
_, err = ss.User().Save(&u1)
require.Error(t, err, "shouldn't be able to update user from save")
u2 := model.User{
Email: u1.Email,
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be unique email")
u2.Email = MakeEmail()
u2.Username = u1.Username
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be unique username")
u2.Username = ""
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be non-empty username")
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
NotifyProps: make(map[string]string, 1),
}
maxPostSize := ss.Post().GetMaxPostSize()
u3.NotifyProps[model.AutoResponderMessageNotifyProp] = strings.Repeat("a", maxPostSize+1)
_, err = ss.User().Save(&u3)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
for i := 0; i < 49; i++ {
u := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u)
require.NoError(t, err, "couldn't save item")
defer func() { require.NoError(t, ss.User().PermanentDelete(u.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
}
u2.Id = ""
u2.Email = MakeEmail()
u2.Username = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err, "couldn't save item")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)
require.Error(t, nErr, "should be the limit")
}
func testUserStoreUpdate(t *testing.T, ss store.Store) {
u1 := &model.User{
Email: MakeEmail(),
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
AuthService: "ldap",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, err = ss.User().Update(u1, false)
require.NoError(t, err)
missing := &model.User{}
_, err = ss.User().Update(missing, false)
require.Error(t, err, "Update should have failed because of missing key")
newId := &model.User{
Id: model.NewId(),
}
_, err = ss.User().Update(newId, false)
require.Error(t, err, "Update should have failed because id change")
u2.Email = MakeEmail()
_, err = ss.User().Update(u2, false)
require.Error(t, err, "Update should have failed because you can't modify AD/LDAP fields")
u3 := &model.User{
Email: MakeEmail(),
AuthService: "gitlab",
}
oldEmail := u3.Email
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u3.Id}, -1)
require.NoError(t, nErr)
u3.Email = MakeEmail()
userUpdate, err := ss.User().Update(u3, false)
require.NoError(t, err, "Update should not have failed")
assert.Equal(t, oldEmail, userUpdate.New.Email, "Email should not have been updated as the update is not trusted")
u3.Email = MakeEmail()
userUpdate, err = ss.User().Update(u3, true)
require.NoError(t, err, "Update should not have failed")
assert.NotEqual(t, oldEmail, userUpdate.New.Email, "Email should have been updated as the update is trusted")
err = ss.User().UpdateLastPictureUpdate(u1.Id)
require.NoError(t, err, "Update should not have failed")
// Test UpdateNotifyProps
u1, err = ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
props := u1.NotifyProps
props["hello"] = "world"
err = ss.User().UpdateNotifyProps(u1.Id, props)
require.NoError(t, err)
ss.User().InvalidateProfileCacheForUser(u1.Id)
uNew, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.Equal(t, props, uNew.NotifyProps)
u4 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
NotifyProps: make(map[string]string, 1),
}
maxPostSize := ss.Post().GetMaxPostSize()
u4.NotifyProps[model.AutoResponderMessageNotifyProp] = strings.Repeat("a", maxPostSize+1)
_, err = ss.User().Update(&u4, false)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
err = ss.User().UpdateNotifyProps(u4.Id, u4.NotifyProps)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
}
func testUserStoreUpdateUpdateAt(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
// Ensure UpdateAt has a change to be different below.
time.Sleep(2 * time.Millisecond)
_, err = ss.User().UpdateUpdateAt(u1.Id)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Less(t, u1.UpdateAt, user.UpdateAt, "UpdateAt not updated correctly")
}
func testUserStoreUpdateFailedPasswordAttempts(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().UpdateFailedPasswordAttempts(u1.Id, 3)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Equal(t, 3, user.FailedAttempts, "FailedAttempts not updated correctly")
}
func testUserStoreGet(t *testing.T, ss store.Store) {
u1 := &model.User{
Email: MakeEmail(),
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, _ := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
})
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u2.Id,
Username: u2.Username,
Description: "bot description",
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u2.IsBot = true
u2.BotDescription = "bot description"
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u2.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
t.Run("fetch empty id", func(t *testing.T) {
_, err := ss.User().Get(context.Background(), "")
require.Error(t, err)
})
t.Run("fetch user 1", func(t *testing.T) {
actual, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Equal(t, u1, actual)
require.False(t, actual.IsBot)
})
t.Run("fetch user 2, also a bot", func(t *testing.T) {
actual, err := ss.User().Get(context.Background(), u2.Id)
require.NoError(t, err)
require.Equal(t, u2, actual)
require.True(t, actual.IsBot)
require.Equal(t, "bot description", actual.BotDescription)
})
}
func testGetAllUsingAuthService(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthService: "service2",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
t.Run("get by unknown auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("unknown")
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("get by auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("service")
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2}, users)
})
t.Run("get by other auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("service2")
require.NoError(t, err)
assert.Equal(t, []*model.User{u3}, users)
})
}
func sanitized(user *model.User) *model.User {
clonedUser := user.DeepCopy()
clonedUser.Sanitize(map[string]bool{})
return clonedUser
}
func testUserStoreGetAllProfiles(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
Roles: "system_user some-other-role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
u5, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u5" + model.NewId(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
u6, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u6" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u6.Id)) }()
u7, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u7" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u7.Id)) }()
t.Run("get offset 0, limit 100", func(t *testing.T) {
options := &model.UserGetOptions{Page: 0, PerPage: 100}
actual, userErr := ss.User().GetAllProfiles(options)
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
sanitized(u6),
sanitized(u7),
}, actual)
})
t.Run("get offset 0, limit 1", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 1,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
}, actual)
})
t.Run("get all", func(t *testing.T) {
actual, userErr := ss.User().GetAll()
require.NoError(t, userErr)
require.Equal(t, []*model.User{
u1,
u2,
u3,
u4,
u5,
u6,
u7,
}, actual)
})
t.Run("etag changes for all after user creation", func(t *testing.T) {
etag := ss.User().GetEtagForAllProfiles()
uNew := &model.User{}
uNew.Email = MakeEmail()
_, userErr := ss.User().Save(uNew)
require.NoError(t, userErr)
defer func() { require.NoError(t, ss.User().PermanentDelete(uNew.Id)) }()
updatedEtag := ss.User().GetEtagForAllProfiles()
require.NotEqual(t, etag, updatedEtag)
})
t.Run("filter to system_admin role", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Role: "system_admin",
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u5),
sanitized(u6),
}, actual)
})
t.Run("filter to system_admin role, inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Role: "system_admin",
Inactive: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
}, actual)
})
t.Run("filter to inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Inactive: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
sanitized(u7),
}, actual)
})
t.Run("filter to active", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Active: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
}, actual)
})
t.Run("try to filter to active and inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Inactive: true,
Active: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
sanitized(u7),
}, actual)
})
u8, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u8" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_user_manager system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u8.Id)) }()
u9, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u9" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_manager system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u9.Id)) }()
u10, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u10" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_read_only_admin system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u10.Id)) }()
t.Run("filter by system_user_manager role", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_user_manager"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u8),
}, actual)
})
t.Run("filter by multiple system roles", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_manager", "system_user_manager", "system_read_only_admin", "system_admin"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u10),
sanitized(u5),
sanitized(u6),
sanitized(u8),
sanitized(u9),
}, actual)
})
t.Run("filter by system_user only", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_user"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u7),
}, actual)
})
}
func testUserStoreGetProfiles(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
u5, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u5" + model.NewId(),
DeleteAt: model.GetMillis(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u5.Id}, -1)
require.NoError(t, nErr)
t.Run("get page 0, perPage 100", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
}, actual)
})
t.Run("get page 0, perPage 1", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
require.Equal(t, []*model.User{sanitized(u1)}, actual)
})
t.Run("get unknown team id", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: "123",
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Equal(t, []*model.User{}, actual)
})
t.Run("etag changes for all after user creation", func(t *testing.T) {
etag := ss.User().GetEtagForProfiles(teamId)
uNew := &model.User{}
uNew.Email = MakeEmail()
_, err := ss.User().Save(uNew)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(uNew.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: uNew.Id}, -1)
require.NoError(t, nErr)
updatedEtag := ss.User().GetEtagForProfiles(teamId)
require.NotEqual(t, etag, updatedEtag)
})
t.Run("filter to system_admin role", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Role: "system_admin",
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u4),
}, actual)
})
t.Run("filter to inactive", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Inactive: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u5),
}, actual)
})
t.Run("filter to active", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Active: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
}, actual)
})
t.Run("try to filter to active and inactive", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Inactive: true,
Active: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u5),
}, actual)
})
}
func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
u4.DeleteAt = 1
_, err = ss.User().Update(u4, true)
require.NoError(t, err)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get all users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3), sanitized(u4)}, users)
})
t.Run("get only active users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Active: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3)}, users)
})
t.Run("get inactive users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Inactive: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u4)}, users)
})
t.Run("get in channel 1, offset 1, limit 2", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 1,
PerPage: 1,
})
require.NoError(t, err)
users_p2, err2 := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 2,
PerPage: 1,
})
require.NoError(t, err2)
users = append(users, users_p2...)
assert.Equal(t, []*model.User{sanitized(u2), sanitized(u3)}, users)
})
t.Run("get in channel 2, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c2.Id,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1)}, users)
})
t.Run("Filter by channel members and channel admins", func(t *testing.T) {
// save admin for c1
user2Admin, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "bbb" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user2Admin.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_admin",
})
require.NoError(t, nErr)
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
ChannelRoles: []string{model.ChannelAdminRoleId},
Page: 0,
PerPage: 5,
})
require.NoError(t, err)
assert.Equal(t, user2Admin.Id, users[0].Id)
})
}
func testUserStoreGetProfilesInChannelByAdmin(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "aaa" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user1.Id}, -1)
require.NoError(t, nErr)
user2Admin, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "bbb" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
require.NoError(t, nErr)
user3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "ccc" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user3.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel by admin",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user2Admin.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_admin",
})
require.NoError(t, nErr)
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get users in admin, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByAdmin(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Len(t, users, 3)
require.Equal(t, user2Admin.Username, users[0].Username)
require.Equal(t, user1.Username, users[1].Username)
require.Equal(t, user3.Username, users[2].Username)
})
}
func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
u4.DeleteAt = 1
_, err = ss.User().Update(u4, true)
require.NoError(t, err)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u1.Id,
Status: model.StatusDnd,
}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u2.Id,
Status: model.StatusAway,
}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u3.Id,
Status: model.StatusOnline,
}))
t.Run("get all users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3), sanitized(u4)}, users)
})
t.Run("get active in channel 1 by status, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByStatus(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Active: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3), sanitized(u2), sanitized(u1)}, users)
})
t.Run("get inactive users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Inactive: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u4)}, users)
})
t.Run("get in channel 2 by status, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByStatus(&model.UserGetOptions{
InChannelId: c2.Id,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1)}, users)
})
}
func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
DeleteAt: 1,
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get, page 0, per_page 100", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u2), sanitized(u3)}, users)
})
t.Run("get, page 1, per_page 1", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 1, PerPage: 1})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
t.Run("get, page 2, per_page 1", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 2, PerPage: 1})
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("get, page 0, per_page 100, inactive", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100, Inactive: true})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
t.Run("get, page 0, per_page 100, role", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100, Role: "system_admin"})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
}
func testUserStoreGetAllProfilesInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("all profiles in channel 1, no caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c1.Id, false)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
u2.Id: sanitized(u2),
u3.Id: sanitized(u3),
}, profiles)
})
t.Run("all profiles in channel 2, no caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, false)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
t.Run("all profiles in channel 2, caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, true)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
t.Run("all profiles in channel 2, caching [repeated]", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, true)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
ss.User().InvalidateProfilesInChannelCacheByUser(u1.Id)
ss.User().InvalidateProfilesInChannelCache(c2.Id)
}
func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
t.Run("get team 1, channel 1, offset 0, limit 100", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c1.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 100", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, profiles)
})
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get team 1, channel 1, offset 0, limit 100, after update", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c1.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 100, after update", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
sanitized(u3),
}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 0, setting group constrained when it's not", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, true, 0, 100, nil)
require.NoError(t, err)
assert.Empty(t, profiles)
})
// create a group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString("n_" + model.NewId()),
DisplayName: "dn_" + model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
})
require.NoError(t, err)
// add two members to the group
for _, u := range []*model.User{u1, u2} {
_, err = ss.Group().UpsertMember(group.Id, u.Id)
require.NoError(t, err)
}
// associate the group with the channel
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: group.Id,
SyncableId: c2.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
t.Run("get team 1, channel 2, offset 0, limit 0, setting group constrained", func(t *testing.T) {
profiles, err := ss.User().GetProfilesNotInChannel(teamId, c2.Id, true, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
}, profiles)
})
}
func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
t.Run("get u1 by id, no caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id}, nil, false)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get u1 by id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get u1, u2, u3 by id, no caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id}, nil, false)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2, u3}, users)
})
t.Run("get u1, u2, u3 by id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2, u3}, users)
})
t.Run("get unknown id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{"123"}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("should only return users with UpdateAt greater than the since time", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id, u4.Id}, &store.UserGetByIdsOpts{
Since: u2.CreateAt,
}, true)
require.NoError(t, err)
// u3 comes from the cache, and u4 does not
assert.Equal(t, []*model.User{u3, u4}, users)
})
}
func testUserStoreGetProfileByGroupChannelIdsForUser(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
gc1, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, nErr)
for _, uId := range []string{u1.Id, u2.Id, u3.Id} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc1.Id,
UserId: uId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
gc2, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, nErr)
for _, uId := range []string{u1.Id, u3.Id, u4.Id} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc2.Id,
UserId: uId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
testCases := []struct {
Name string
UserId string
ChannelIds []string
ExpectedUserIdsByChannel map[string][]string
EnsureChannelsNotInResults []string
}{
{
Name: "Get group 1 as user 1",
UserId: u1.Id,
ChannelIds: []string{gc1.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u2.Id, u3.Id},
},
EnsureChannelsNotInResults: []string{},
},
{
Name: "Get groups 1 and 2 as user 1",
UserId: u1.Id,
ChannelIds: []string{gc1.Id, gc2.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u2.Id, u3.Id},
gc2.Id: {u3.Id, u4.Id},
},
EnsureChannelsNotInResults: []string{},
},
{
Name: "Get groups 1 and 2 as user 2",
UserId: u2.Id,
ChannelIds: []string{gc1.Id, gc2.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u1.Id, u3.Id},
},
EnsureChannelsNotInResults: []string{gc2.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
res, err := ss.User().GetProfileByGroupChannelIdsForUser(tc.UserId, tc.ChannelIds)
require.NoError(t, err)
for channelId, expectedUsers := range tc.ExpectedUserIdsByChannel {
users, ok := res[channelId]
require.True(t, ok)
var userIds []string
for _, user := range users {
userIds = append(userIds, user.Id)
}
require.ElementsMatch(t, expectedUsers, userIds)
}
for _, channelId := range tc.EnsureChannelsNotInResults {
_, ok := res[channelId]
require.False(t, ok)
}
})
}
}
func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) {
teamId := model.NewId()
team2Id := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team2Id, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get by u1 and u2 usernames, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2}, users)
})
t.Run("get by u1 username, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get by u1 and u3 usernames, no team id", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u3}, users)
})
t.Run("get by u1 and u3 usernames, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get by u1 and u3 usernames, team id 2", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{team2Id}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u3}, users)
})
}
func testUserStoreGetSystemAdminProfiles(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId,
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId,
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("all system admin profiles", func(t *testing.T) {
result, userError := ss.User().GetSystemAdminProfiles()
require.NoError(t, userError)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
u3.Id: sanitized(u3),
}, result)
})
}
func testUserStoreGetByEmail(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
assert.Equal(t, u1, u)
})
t.Run("get u2 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u2.Email)
require.NoError(t, err)
assert.Equal(t, u2, u)
})
t.Run("get u3 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u3.Email)
require.NoError(t, err)
assert.Equal(t, u3, u)
})
t.Run("get by empty email", func(t *testing.T) {
_, err := ss.User().GetByEmail("")
require.Error(t, err)
})
t.Run("get by unknown", func(t *testing.T) {
_, err := ss.User().GetByEmail("unknown")
require.Error(t, err)
})
}
func testUserStoreGetByAuthData(t *testing.T, ss store.Store) {
teamId := model.NewId()
auth1 := model.NewId()
auth3 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthData: &auth1,
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthData: &auth3,
AuthService: "service2",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get by u1 auth", func(t *testing.T) {
u, err := ss.User().GetByAuth(u1.AuthData, u1.AuthService)
require.NoError(t, err)
assert.Equal(t, u1, u)
})
t.Run("get by u3 auth", func(t *testing.T) {
u, err := ss.User().GetByAuth(u3.AuthData, u3.AuthService)
require.NoError(t, err)
assert.Equal(t, u3, u)
})
t.Run("get by u1 auth, unknown service", func(t *testing.T) {
_, err := ss.User().GetByAuth(u1.AuthData, "unknown")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get by unknown auth, u1 service", func(t *testing.T) {
unknownAuth := ""
_, err := ss.User().GetByAuth(&unknownAuth, u1.AuthService)
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
})
t.Run("get by unknown auth, unknown service", func(t *testing.T) {
unknownAuth := ""
_, err := ss.User().GetByAuth(&unknownAuth, "unknown")
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
})
}
func testUserStoreGetByUsername(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u1.Username)
require.NoError(t, err)
assert.Equal(t, u1, result)
})
t.Run("get u2 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u2.Username)
require.NoError(t, err)
assert.Equal(t, u2, result)
})
t.Run("get u3 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u3.Username)
require.NoError(t, err)
assert.Equal(t, u3, result)
})
t.Run("get by empty username", func(t *testing.T) {
_, err := ss.User().GetByUsername("")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get by unknown", func(t *testing.T) {
_, err := ss.User().GetByUsername("unknown")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testUserStoreGetForLogin(t *testing.T, ss store.Store) {
teamId := model.NewId()
auth := model.NewId()
auth2 := model.NewId()
auth3 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthService: model.UserAuthServiceGitlab,
AuthData: &auth,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
AuthService: model.UserAuthServiceLdap,
AuthData: &auth2,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthService: model.UserAuthServiceLdap,
AuthData: &auth3,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by username, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u1.Username, true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by username, check for case issues", func(t *testing.T) {
user, err := ss.User().GetForLogin(strings.ToUpper(u1.Username), true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by username, allow only email", func(t *testing.T) {
_, err := ss.User().GetForLogin(u1.Username, false, true)
require.Error(t, err)
require.Equal(t, "user not found", err.Error())
})
t.Run("get u1 by email, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u1.Email, true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by email, check for case issues", func(t *testing.T) {
user, err := ss.User().GetForLogin(strings.ToUpper(u1.Email), true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by email, allow only username", func(t *testing.T) {
_, err := ss.User().GetForLogin(u1.Email, true, false)
require.Error(t, err)
require.Equal(t, "user not found", err.Error())
})
t.Run("get u2 by username, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u2.Username, true, true)
require.NoError(t, err)
assert.Equal(t, u2, user)
})
t.Run("get u2 by email, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u2.Email, true, true)
require.NoError(t, err)
assert.Equal(t, u2, user)
})
t.Run("get u2 by username, allow neither", func(t *testing.T) {
_, err := ss.User().GetForLogin(u2.Username, false, false)
require.Error(t, err)
require.Equal(t, "sign in with username and email are disabled", err.Error())
})
}
func testUserStoreUpdatePassword(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
hashedPassword := model.HashPassword("newpwd")
err = ss.User().UpdatePassword(u1.Id, hashedPassword)
require.NoError(t, err)
user, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
require.Equal(t, user.Password, hashedPassword, "Password was not updated correctly")
}
func testUserStoreDelete(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().PermanentDelete(u1.Id)
require.NoError(t, err)
}
func testUserStoreUpdateAuthData(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
service := "someservice"
authData := model.NewId()
_, err = ss.User().UpdateAuthData(u1.Id, service, &authData, "", true)
require.NoError(t, err)
user, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
require.Equal(t, service, user.AuthService, "AuthService was not updated correctly")
require.Equal(t, authData, *user.AuthData, "AuthData was not updated correctly")
require.Equal(t, "", user.Password, "Password was not cleared properly")
}
func testUserStoreResetAuthDataToEmailForUsers(t *testing.T, ss store.Store) {
user := &model.User{}
user.Username = "user1" + model.NewId()
user.Email = MakeEmail()
_, err := ss.User().Save(user)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
resetAuthDataToID := func() {
_, err = ss.User().UpdateAuthData(
user.Id, model.UserAuthServiceSaml, model.NewString("some-id"), "", false)
require.NoError(t, err)
}
resetAuthDataToID()
// dry run
numAffected, err := ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
// real run
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, false)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
user, appErr := ss.User().Get(context.Background(), user.Id)
require.NoError(t, appErr)
require.Equal(t, *user.AuthData, user.Email)
resetAuthDataToID()
// with specific user IDs
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, []string{model.NewId()}, false, true)
require.NoError(t, err)
require.Equal(t, 0, numAffected)
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, []string{user.Id}, false, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
// delete user
user.DeleteAt = model.GetMillisForTime(time.Now())
ss.User().Update(user, true)
// without deleted user
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, true)
require.NoError(t, err)
require.Equal(t, 0, numAffected)
// with deleted user
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, true, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
}
func testUserUnreadCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{}
c1.TeamId = teamId
c1.DisplayName = "Unread Messages"
c1.Name = "unread-messages-" + model.NewId()
c1.Type = model.ChannelTypeOpen
c2 := model.Channel{}
c2.TeamId = teamId
c2.DisplayName = "Unread Direct"
c2.Name = "unread-direct-" + model.NewId()
c2.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Username = "user1" + model.NewId()
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = "user2" + model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3 := &model.User{}
u3.Email = MakeEmail()
u3.Username = "user3" + model.NewId()
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().Save(&c1, -1)
require.NoError(t, nErr, "couldn't save item")
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
m3 := model.ChannelMember{}
m3.ChannelId = c1.Id
m3.UserId = u3.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
m1.ChannelId = c2.Id
m2.ChannelId = c2.Id
_, nErr = ss.Channel().SaveDirectChannel(&c2, &m1, &m2)
require.NoError(t, nErr, "couldn't save direct channel")
p1 := model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = "this is a message for @" + u2.Username + " and " + "@" + u3.Username
// Post one message with mention to open channel
_, nErr = ss.Post().Save(&p1)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u2.Id, u3.Id}, false, false)
require.NoError(t, nErr)
// Post 2 messages without mention to direct channel
p2 := model.Post{}
p2.ChannelId = c2.Id
p2.UserId = u1.Id
p2.Message = "first message"
_, nErr = ss.Post().Save(&p2)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false)
require.NoError(t, nErr)
p3 := model.Post{}
p3.ChannelId = c2.Id
p3.UserId = u1.Id
p3.Message = "second message"
_, nErr = ss.Post().Save(&p3)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false)
require.NoError(t, nErr)
badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id, false)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(3), badge, "should have 3 unread messages")
badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, false)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread message")
// Increment root mentions by 1
nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u3.Id}, true, false)
require.NoError(t, nErr)
// CRT is enabled, only root mentions are counted
badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, true)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread message with CRT")
badge, unreadCountErr = ss.User().GetUnreadCountForChannel(u2.Id, c1.Id)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread messages for that channel")
badge, unreadCountErr = ss.User().GetUnreadCountForChannel(u2.Id, c2.Id)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(2), badge, "should have 2 unread messages for that channel")
}
func testUserStoreUpdateMfaSecret(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
err = ss.User().UpdateMfaSecret(u1.Id, "12345")
require.NoError(t, err)
// should pass, no update will occur though
err = ss.User().UpdateMfaSecret("junk", "12345")
require.NoError(t, err)
}
func testUserStoreUpdateMfaActive(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
time.Sleep(time.Millisecond)
err = ss.User().UpdateMfaActive(u1.Id, true)
require.NoError(t, err)
err = ss.User().UpdateMfaActive(u1.Id, false)
require.NoError(t, err)
// should pass, no update will occur though
err = ss.User().UpdateMfaActive("junk", true)
require.NoError(t, err)
}
func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
millis := model.GetMillis()
u3.LastActivityAt = millis
u2.LastActivityAt = millis - 1
u1.LastActivityAt = millis - 1
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u1.LastActivityAt, ActiveChannel: ""}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u2.LastActivityAt, ActiveChannel: ""}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u3.LastActivityAt, ActiveChannel: ""}))
t.Run("get team 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
sanitized(u1),
sanitized(u2),
}, users)
})
t.Run("get team 1, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
t.Run("get team 1, offset 2, limit 1", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 2, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
}, users)
})
}
func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
teamId2 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Yuka",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Leia",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Ali",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)
require.NoError(t, nErr)
t.Run("get team 1, offset 0, limit 100", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
sanitized(u2),
sanitized(u1),
}, result)
})
t.Run("get team 1, offset 0, limit 1", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 0, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
}, result)
})
t.Run("get team 1, offset 2, limit 1", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 2, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
}, result)
})
t.Run("get team 2, offset 0, limit 100", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId2, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u4),
}, result)
})
}
func assertUsers(t *testing.T, expected, actual []*model.User) {
expectedUsernames := make([]string, 0, len(expected))
for _, user := range expected {
expectedUsernames = append(expectedUsernames, user.Username)
}
actualUsernames := make([]string, 0, len(actual))
for _, user := range actual {
actualUsernames = append(actualUsernames, user.Username)
}
if assert.Equal(t, expectedUsernames, actualUsernames) {
assert.Equal(t, expected, actual)
}
}
func testUserStoreSearch(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
Roles: "system_user system_admin",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
Roles: "system_user system_user_manager",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
Roles: "system_guest",
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
t1id := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u1.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u2.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u3.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1)
require.NoError(t, nErr)
testCases := []struct {
Description string
TeamId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, team 1",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, team 1 with team guest and team admin filters without sys admin filter",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamGuestRoleId, model.TeamAdminRoleId},
},
[]*model.User{u3},
},
{
"search jimb, team 1 with team admin filter and sys admin filter",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u1},
},
{
"search jim, team 1 with team admin filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u2},
},
{
"search jim, team 1 with team admin and team guest filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamAdminRoleId, model.TeamGuestRoleId},
},
[]*model.User{u2, u3},
},
{
"search jim, team 1 with team admin and system admin filters",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u2, u1},
},
{
"search jim, team 1 with system guest filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemGuestRoleId},
TeamRoles: []string{},
},
[]*model.User{u3},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().Search(
testCase.TeamId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
ch1 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(&ch1, -1)
require.NoError(t, nErr)
ch2 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c2, nErr := ss.Channel().Save(&ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
testCases := []struct {
Description string
TeamId string
ChannelId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, channel 1",
tid,
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, channel 1",
tid,
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, channel 1, no team id",
"",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, channel 1, junk team id",
"junk",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, channel 2",
tid,
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, channel 2",
tid,
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u3},
},
{
"search jimb, channel 2, no team id",
"",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, channel 2, junk team id",
"junk",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, channel 1",
tid,
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"search jim, channel 1, limit 1",
tid,
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: 1,
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInChannel(
testCase.TeamId,
testCase.ChannelId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchInChannel(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
Roles: "system_user system_admin",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
Roles: "system_user",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
Roles: "system_user",
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
ch1 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(&ch1, -1)
require.NoError(t, nErr)
ch2 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c2, nErr := ss.Channel().Save(&ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: true,
SchemeUser: true,
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: false,
SchemeUser: true,
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: false,
SchemeUser: true,
})
require.NoError(t, nErr)
testCases := []struct {
Description string
ChannelId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, channel 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, channel 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, allow inactive, channel 1, limit 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
{
"search jimb, channel 2",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, channel 2",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, allow inactive, channel 1 with system admin filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
},
[]*model.User{u1},
},
{
"search jim, allow inactive, channel 1 with system admin and system user filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId},
},
[]*model.User{u1, u3},
},
{
"search jim, allow inactive, channel 1 with channel user filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId},
},
[]*model.User{u3},
},
{
"search jim, allow inactive, channel 1 with channel user and channel admin filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId, model.ChannelAdminRoleId},
},
[]*model.User{u3},
},
{
"search jim, allow inactive, channel 2 with channel user filter",
c2.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId},
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchInChannel(
testCase.ChannelId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInTeam(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4 := &model.User{
Username: "simon" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(u4)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
u5 := &model.User{
Username: "yu" + model.NewId(),
FirstName: "En",
LastName: "Yu",
Nickname: "enyu",
Email: MakeEmail(),
}
_, err = ss.User().Save(u5)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
u6 := &model.User{
Username: "underscore" + model.NewId(),
FirstName: "Du_",
LastName: "_DE",
Nickname: "lodash",
Email: MakeEmail(),
}
_, err = ss.User().Save(u6)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u6.Id)) }()
teamId1 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// u4 is not in team 1
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u5.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u6.Id}, -1)
require.NoError(t, nErr)
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
u4.AuthData = nilAuthData
u5.AuthData = nilAuthData
u6.AuthData = nilAuthData
testCases := []struct {
Description string
TeamId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search simo, team 1",
teamId1,
"simo",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u4},
},
{
"search jimb, team 1",
teamId1,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, team 1",
teamId1,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search simo, team 2",
teamId2,
"simo",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, team2",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, team 2",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, allow inactive, team 2, limit 1",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInTeam(
testCase.TeamId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchWithoutTeam(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
testCases := []struct {
Description string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"empty string",
"",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"jim",
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"PLT-8354",
"* ",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"jim, limit 1",
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: 1,
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchWithoutTeam(
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchInGroup(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := model.NewString("")
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u3.Id)
require.NoError(t, err)
u3.DeleteAt = 1
_, err = ss.User().Update(u3, true)
require.NoError(t, err)
testCases := []struct {
Description string
GroupId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, group 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, group 1, allow inactive",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, group 1, limit 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
{
"search jimb, group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchInGroup(
testCase.GroupId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInGroup(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := model.NewString("")
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u3.Id)
require.NoError(t, err)
u3.DeleteAt = 1
_, err = ss.User().Update(u3, true)
require.NoError(t, err)
testCases := []struct {
Description string
GroupId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, not in group 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, not in group 1",
g1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2},
},
{
"search jimb, not in group 3, allow inactive",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jim, not in group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInGroup(
testCase.GroupId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testCount(t *testing.T, ss store.Store) {
// Regular
teamId := model.NewId()
channelId := model.NewId()
regularUser := &model.User{}
regularUser.Email = MakeEmail()
regularUser.Roles = model.SystemUserRoleId
_, err := ss.User().Save(regularUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(regularUser.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: regularUser.Id, SchemeAdmin: false, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: regularUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
guestUser := &model.User{}
guestUser.Email = MakeEmail()
guestUser.Roles = model.SystemGuestRoleId
_, err = ss.User().Save(guestUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guestUser.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: guestUser.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: guestUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
teamAdmin := &model.User{}
teamAdmin.Email = MakeEmail()
teamAdmin.Roles = model.SystemUserRoleId
_, err = ss.User().Save(teamAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(teamAdmin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: teamAdmin.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: teamAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
sysAdmin := &model.User{}
sysAdmin.Email = MakeEmail()
sysAdmin.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
_, err = ss.User().Save(sysAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: sysAdmin.Id, SchemeAdmin: false, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: sysAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
// Deleted
deletedUser := &model.User{}
deletedUser.Email = MakeEmail()
deletedUser.DeleteAt = model.GetMillis()
_, err = ss.User().Save(deletedUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedUser.Id)) }()
// Bot
botUser, err := ss.User().Save(&model.User{
Email: MakeEmail(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(botUser.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: botUser.Id,
Username: botUser.Username,
OwnerId: regularUser.Id,
})
require.NoError(t, nErr)
botUser.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(botUser.Id)) }()
testCases := []struct {
Description string
Options model.UserCountOptions
Expected int64
}{
{
"No bot accounts no deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: false,
IncludeDeleted: false,
TeamId: "",
},
4,
},
{
"Include bot accounts no deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: false,
TeamId: "",
},
5,
},
{
"Include delete accounts no bots and no team id",
model.UserCountOptions{
IncludeBotAccounts: false,
IncludeDeleted: true,
TeamId: "",
},
5,
},
{
"Include bot accounts and deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: "",
},
6,
},
{
"Include bot accounts, deleted accounts, exclude regular users with no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
ExcludeRegularUsers: true,
TeamId: "",
},
1,
},
{
"Include bot accounts and deleted accounts with existing team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
},
4,
},
{
"Include bot accounts and deleted accounts with fake team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: model.NewId(),
},
0,
},
{
"Include bot accounts and deleted accounts with existing team id and view restrictions allowing team",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}},
},
4,
},
{
"Include bot accounts and deleted accounts with existing team id and view restrictions not allowing current team",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}},
},
0,
},
{
"Filter by system admins only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId},
},
1,
},
{
"Filter by system users only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemUserRoleId},
},
2,
},
{
"Filter by system guests only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemGuestRoleId},
},
1,
},
{
"Filter by system admins and system users",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId},
},
3,
},
{
"Filter by system admins, system user and system guests",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId, model.SystemGuestRoleId},
},
4,
},
{
"Filter by team admins",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamAdminRoleId},
},
1,
},
{
"Filter by team members",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamUserRoleId},
},
1,
},
{
"Filter by team guests",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamGuestRoleId},
},
1,
},
{
"Filter by team guests and any system role",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamGuestRoleId},
Roles: []string{model.SystemAdminRoleId},
},
2,
},
{
"Filter by channel members",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelUserRoleId},
},
1,
},
{
"Filter by channel members and system admins",
model.UserCountOptions{
ChannelId: channelId,
Roles: []string{model.SystemAdminRoleId},
ChannelRoles: []string{model.ChannelUserRoleId},
},
2,
},
{
"Filter by channel members and system admins and channel admins",
model.UserCountOptions{
ChannelId: channelId,
Roles: []string{model.SystemAdminRoleId},
ChannelRoles: []string{model.ChannelUserRoleId, model.ChannelAdminRoleId},
},
3,
},
{
"Filter by channel guests",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelGuestRoleId},
},
1,
},
{
"Filter by channel guests and any system role",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelGuestRoleId},
Roles: []string{model.SystemAdminRoleId},
},
2,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
count, err := ss.User().Count(testCase.Options)
require.NoError(t, err)
require.Equal(t, testCase.Expected, count)
})
}
}
func testUserStoreGetFirstSystemAdminID(t *testing.T, ss store.Store) {
sysAdmin := &model.User{}
sysAdmin.Email = MakeEmail()
sysAdmin.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
sysAdmin, err := ss.User().Save(sysAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin.Id)) }()
// We need the second system admin to be created after the first one
// our granulirity is ms
time.Sleep(1 * time.Millisecond)
sysAdmin2 := &model.User{}
sysAdmin2.Email = MakeEmail()
sysAdmin2.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
sysAdmin2, err = ss.User().Save(sysAdmin2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin2.Id)) }()
returnedId, err := ss.User().GetFirstSystemAdminID()
require.NoError(t, err)
require.Equal(t, sysAdmin.Id, returnedId)
}
func testUserStoreAnalyticsActiveCount(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
// Create 5 users statuses u0, u1, u2, u3, u4.
// u4 is also a bot
u0, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u0" + model.NewId(),
})
require.NoError(t, err)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() {
require.NoError(t, ss.User().PermanentDelete(u0.Id))
require.NoError(t, ss.User().PermanentDelete(u1.Id))
require.NoError(t, ss.User().PermanentDelete(u2.Id))
require.NoError(t, ss.User().PermanentDelete(u3.Id))
require.NoError(t, ss.User().PermanentDelete(u4.Id))
}()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u4.Id,
Username: u4.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
millis := model.GetMillis()
millisTwoDaysAgo := model.GetMillis() - (2 * DayMilliseconds)
millisTwoMonthsAgo := model.GetMillis() - (2 * MonthMilliseconds)
// u0 last activity status is two months ago.
// u1 last activity status is two days ago.
// u2, u3, u4 last activity is within last day
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u0.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOffline, LastActivityAt: millis}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOffline, LastActivityAt: millis}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u4.Id, Status: model.StatusOffline, LastActivityAt: millis}))
// Daily counts (without bots)
count, err := ss.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(2), count)
// Daily counts (with bots)
count, err = ss.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
// Monthly counts (without bots)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
// Monthly counts - (with bots)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(4), count)
// Monthly counts - (with bots, excluding deleted)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: false})
require.NoError(t, err)
assert.Equal(t, int64(4), count)
}
func testUserStoreAnalyticsActiveCountForPeriod(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
// Create 5 users statuses u0, u1, u2, u3, u4.
// u4 is also a bot
u0, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u0" + model.NewId(),
})
require.NoError(t, err)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() {
require.NoError(t, ss.User().PermanentDelete(u0.Id))
require.NoError(t, ss.User().PermanentDelete(u1.Id))
require.NoError(t, ss.User().PermanentDelete(u2.Id))
require.NoError(t, ss.User().PermanentDelete(u3.Id))
require.NoError(t, ss.User().PermanentDelete(u4.Id))
}()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u4.Id,
Username: u4.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
millis := model.GetMillis()
millisTwoDaysAgo := model.GetMillis() - (2 * DayMilliseconds)
millisTwoMonthsAgo := model.GetMillis() - (2 * MonthMilliseconds)
// u0 last activity status is two months ago.
// u1 last activity status is one month ago
// u2 last activity is two days ago
// u2 last activity is one day ago
// u3 last activity is within last day
// u4 last activity is within last day
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u0.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo + MonthMilliseconds}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo + DayMilliseconds}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u4.Id, Status: model.StatusOffline, LastActivityAt: millis}))
// Two months to two days (without bots)
count, nerr := ss.User().AnalyticsActiveCountForPeriod(millisTwoMonthsAgo, millisTwoDaysAgo, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two months to two days (without bots)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoMonthsAgo, millisTwoDaysAgo, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two days to present - (with bots)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoDaysAgo, millis, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: false})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two days to present - (with bots, excluding deleted)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoDaysAgo, millis, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
}
func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
count, err := ss.User().AnalyticsGetInactiveUsersCount()
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.DeleteAt = model.GetMillis()
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
newCount, err := ss.User().AnalyticsGetInactiveUsersCount()
require.NoError(t, err)
require.Equal(t, count, newCount-1, "Expected 1 more inactive users but found otherwise.")
}
func testUserStoreAnalyticsGetSystemAdminCount(t *testing.T, ss store.Store) {
countBefore, err := ss.User().AnalyticsGetSystemAdminCount()
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
_, nErr := ss.User().Save(&u1)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr = ss.User().Save(&u2)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
result, err := ss.User().AnalyticsGetSystemAdminCount()
require.NoError(t, err)
require.Equal(t, countBefore+1, result, "Did not get the expected number of system admins.")
}
func testUserStoreAnalyticsGetGuestCount(t *testing.T, ss store.Store) {
countBefore, err := ss.User().AnalyticsGetGuestCount()
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2.Roles = "system_user"
u3 := model.User{}
u3.Email = MakeEmail()
u3.Username = model.NewId()
u3.Roles = "system_guest"
_, nErr := ss.User().Save(&u1)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr = ss.User().Save(&u2)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.User().Save(&u3)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
result, err := ss.User().AnalyticsGetGuestCount()
require.NoError(t, err)
require.Equal(t, countBefore+1, result, "Did not get the expected number of guests.")
}
func testUserStoreAnalyticsGetExternalUsers(t *testing.T, ss store.Store) {
localHostDomain := "mattermost.com"
result, err := ss.User().AnalyticsGetExternalUsers(localHostDomain)
require.NoError(t, err)
assert.False(t, result)
u1 := model.User{}
u1.Email = "a@mattermost.com"
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = "b@example.com"
u2.Username = model.NewId()
u2.Roles = "system_user"
u3 := model.User{}
u3.Email = "c@test.com"
u3.Username = model.NewId()
u3.Roles = "system_guest"
_, err = ss.User().Save(&u1)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, err = ss.User().Save(&u2)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, err = ss.User().Save(&u3)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
result, err = ss.User().AnalyticsGetExternalUsers(localHostDomain)
require.NoError(t, err)
assert.True(t, result)
}
func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "Team",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, err)
teamId := team.Id
teamId2 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u2.Id}, -1)
require.NoError(t, nErr)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
var etag1, etag2, etag3 string
t.Run("etag for profiles not in team 1", func(t *testing.T) {
etag1 = ss.User().GetEtagForProfilesNotInTeam(teamId)
})
t.Run("get not in team 1, offset 0, limit 100000", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u2),
sanitized(u3),
}, users)
})
t.Run("get not in team 1, offset 1, limit 1", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 1, 1, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
t.Run("get not in team 2, offset 0, limit 100", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId2, false, 0, 100, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
// Add u2 to team 1
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u2.UpdateAt, err = ss.User().UpdateUpdateAt(u2.Id)
require.NoError(t, err)
t.Run("etag for profiles not in team 1 after update", func(t *testing.T) {
etag2 = ss.User().GetEtagForProfilesNotInTeam(teamId)
require.NotEqual(t, etag2, etag1, "etag should have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after update", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
e := ss.Team().RemoveMember(teamId, u1.Id)
require.NoError(t, e)
e = ss.Team().RemoveMember(teamId, u2.Id)
require.NoError(t, e)
u1.UpdateAt, err = ss.User().UpdateUpdateAt(u1.Id)
require.NoError(t, err)
u2.UpdateAt, err = ss.User().UpdateUpdateAt(u2.Id)
require.NoError(t, err)
t.Run("etag for profiles not in team 1 after second update", func(t *testing.T) {
etag3 = ss.User().GetEtagForProfilesNotInTeam(teamId)
require.NotEqual(t, etag1, etag3, "etag should have changed")
require.NotEqual(t, etag2, etag3, "etag should have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after second update", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
t.Run("etag for profiles not in team 1 after addition to team", func(t *testing.T) {
etag4 := ss.User().GetEtagForProfilesNotInTeam(teamId)
require.Equal(t, etag3, etag4, "etag should not have changed")
})
// Add u3 to team 2
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u3.Id}, -1)
require.NoError(t, nErr)
u3.UpdateAt, err = ss.User().UpdateUpdateAt(u3.Id)
require.NoError(t, err)
// GetEtagForProfilesNotInTeam produces a new etag every time a member, not
// in the team, gets a new UpdateAt value. In the case that an older member
// in the set joins a different team, their UpdateAt value changes, thus
// creating a new etag (even though the user set doesn't change). A hashing
// solution, which only uses UserIds, would solve this issue.
t.Run("etag for profiles not in team 1 after u3 added to team 2", func(t *testing.T) {
t.Skip()
etag4 := ss.User().GetEtagForProfilesNotInTeam(teamId)
require.Equal(t, etag3, etag4, "etag should not have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after second update, setting group constrained when it's not", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, true, 0, 100000, nil)
require.NoError(t, userErr)
assert.Empty(t, users)
})
// create a group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString("n_" + model.NewId()),
DisplayName: "dn_" + model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
})
require.NoError(t, err)
// add two members to the group
for _, u := range []*model.User{u1, u2} {
_, err = ss.Group().UpsertMember(group.Id, u.Id)
require.NoError(t, err)
}
// associate the group with the team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: group.Id,
SyncableId: teamId,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
t.Run("get not in team 1, offset 0, limit 100000 after second update, setting group constrained", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, true, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
}, users)
})
}
func testUserStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user system_admin system_post_all",
}
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user custom_role system_admin another_custom_role",
}
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user",
}
u4 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "custom_only",
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, err = ss.User().Save(&u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, err = ss.User().Save(&u4)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
require.NoError(t, ss.User().ClearAllCustomRoleAssignments())
r1, err := ss.User().GetByUsername(u1.Username)
require.NoError(t, err)
assert.Equal(t, u1.Roles, r1.Roles)
r2, err1 := ss.User().GetByUsername(u2.Username)
require.NoError(t, err1)
assert.Equal(t, "system_user system_admin", r2.Roles)
r3, err2 := ss.User().GetByUsername(u3.Username)
require.NoError(t, err2)
assert.Equal(t, u3.Roles, r3.Roles)
r4, err3 := ss.User().GetByUsername(u4.Username)
require.NoError(t, err3)
assert.Equal(t, "", r4.Roles)
}
func testUserStoreGetAllAfter(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user system_admin system_post_all",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u2.Id,
Username: u2.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u2.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u2.Id)) }()
expected := []*model.User{u1, u2}
if strings.Compare(u2.Id, u1.Id) < 0 {
expected = []*model.User{u2, u1}
}
t.Run("get after lowest possible id", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, strings.Repeat("0", 26))
require.NoError(t, err)
assert.Equal(t, expected, actual)
})
t.Run("get after first user", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, expected[0].Id)
require.NoError(t, err)
assert.Equal(t, []*model.User{expected[1]}, actual)
})
t.Run("get after second user", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, expected[1].Id)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, actual)
})
}
func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
// Set up all the objects needed
t1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, err)
ch1 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
cPub1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
cPub2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
ch3 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
cPriv, nErr := ss.Channel().Save(ch3, -1)
require.NoError(t, nErr)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: u2.Id,
TeamId: t1.Id,
}, 100)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u2.Id,
ChannelId: cPub1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u2.Id,
ChannelId: cPub2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: u3.Id,
TeamId: t1.Id,
DeleteAt: model.GetMillis(),
}, 100)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u3.Id,
ChannelId: cPub2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u3.Id,
ChannelId: cPriv.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
cDM := &model.Channel{
Name: model.NewId() + "__" + model.NewId(),
Type: model.ChannelTypeDirect,
}
cm1 := &model.ChannelMember{
UserId: u3.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
UserId: u2.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cDM, nErr = ss.Channel().SaveDirectChannel(cDM, cm1, cm2)
require.NoError(t, nErr)
// Getting all users
res1List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 100)
require.NoError(t, err)
assert.Len(t, res1List, 3)
for _, user := range res1List {
switch user.Id {
case u2.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub1.Id, cPub2.Id, cDM.Id})
case u3.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub2.Id, cDM.Id})
}
}
// Testing pagination
res2List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 1)
require.NoError(t, err)
assert.Len(t, res2List, 1)
res2List, err = ss.User().GetUsersBatchForIndexing(res2List[0].CreateAt, res2List[0].Id, 2)
require.NoError(t, err)
assert.Len(t, res2List, 2)
res2List, err = ss.User().GetUsersBatchForIndexing(res2List[1].CreateAt, res2List[1].Id, 2)
require.NoError(t, err)
assert.Len(t, res2List, 0)
}
func testUserStoreGetTeamGroupUsers(t *testing.T, ss store.Store) {
// create team
id := model.NewId()
team, err := ss.Team().Save(&model.Team{
DisplayName: "dn_" + id,
Name: "n-" + id,
Email: id + "@test.com",
Type: model.TeamInvite,
})
require.NoError(t, err)
require.NotNil(t, team)
// create users
var testUsers []*model.User
for i := 0; i < 3; i++ {
id = model.NewId()
user, userErr := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
})
require.NoError(t, userErr)
require.NotNil(t, user)
testUsers = append(testUsers, user)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
}
require.Len(t, testUsers, 3, "testUsers length doesn't meet required length")
userGroupA, userGroupB, userNoGroup := testUsers[0], testUsers[1], testUsers[2]
// add non-group-member to the team (to prove that the query isn't just returning all members)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userNoGroup.Id,
}, 999)
require.NoError(t, nErr)
// create groups
var testGroups []*model.Group
for i := 0; i < 2; i++ {
id = model.NewId()
var group *model.Group
group, err = ss.Group().Create(&model.Group{
Name: model.NewString("n_" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + id),
})
require.NoError(t, err)
require.NotNil(t, group)
testGroups = append(testGroups, group)
}
require.Len(t, testGroups, 2, "testGroups length doesn't meet required length")
groupA, groupB := testGroups[0], testGroups[1]
// add members to groups
_, err = ss.Group().UpsertMember(groupA.Id, userGroupA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(groupB.Id, userGroupB.Id)
require.NoError(t, err)
// association one group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupA.Id,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
var users []*model.User
requireNUsers := func(n int) {
users, err = ss.User().GetTeamGroupUsers(team.Id)
require.NoError(t, err)
require.NotNil(t, users)
require.Len(t, users, n)
}
// team not group constrained returns users
requireNUsers(1)
// update team to be group-constrained
team.GroupConstrained = model.NewBool(true)
team, err = ss.Team().Update(team)
require.NoError(t, err)
// still returns user (being group-constrained has no effect)
requireNUsers(1)
// associate other group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupB.Id,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
// should return users from all groups
// 2 users now that both groups have been associated to the team
requireNUsers(2)
// add team membership of allowed user
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userGroupA.Id,
}, 999)
require.NoError(t, nErr)
// ensure allowed member still returned by query
requireNUsers(2)
// delete team membership of allowed user
err = ss.Team().RemoveMember(team.Id, userGroupA.Id)
require.NoError(t, err)
// ensure removed allowed member still returned by query
requireNUsers(2)
}
func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) {
// create channel
id := model.NewId()
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "dn_" + id,
Name: "n-" + id,
Type: model.ChannelTypePrivate,
}, 999)
require.NoError(t, nErr)
require.NotNil(t, channel)
// create users
var testUsers []*model.User
for i := 0; i < 3; i++ {
id = model.NewId()
user, userErr := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
})
require.NoError(t, userErr)
require.NotNil(t, user)
testUsers = append(testUsers, user)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
}
require.Len(t, testUsers, 3, "testUsers length doesn't meet required length")
userGroupA, userGroupB, userNoGroup := testUsers[0], testUsers[1], testUsers[2]
// add non-group-member to the channel (to prove that the query isn't just returning all members)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: userNoGroup.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// create groups
var testGroups []*model.Group
for i := 0; i < 2; i++ {
id = model.NewId()
var group *model.Group
group, err = ss.Group().Create(&model.Group{
Name: model.NewString("n_" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + id),
})
require.NoError(t, err)
require.NotNil(t, group)
testGroups = append(testGroups, group)
}
require.Len(t, testGroups, 2, "testGroups length doesn't meet required length")
groupA, groupB := testGroups[0], testGroups[1]
// add members to groups
_, err = ss.Group().UpsertMember(groupA.Id, userGroupA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(groupB.Id, userGroupB.Id)
require.NoError(t, err)
// association one group to channel
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupA.Id,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
var users []*model.User
requireNUsers := func(n int) {
users, err = ss.User().GetChannelGroupUsers(channel.Id)
require.NoError(t, err)
require.NotNil(t, users)
require.Len(t, users, n)
}
// channel not group constrained returns users
requireNUsers(1)
// update team to be group-constrained
channel.GroupConstrained = model.NewBool(true)
_, nErr = ss.Channel().Update(channel)
require.NoError(t, nErr)
// still returns user (being group-constrained has no effect)
requireNUsers(1)
// associate other group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupB.Id,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
// should return users from all groups
// 2 users now that both groups have been associated to the team
requireNUsers(2)
// add team membership of allowed user
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: userGroupA.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// ensure allowed member still returned by query
requireNUsers(2)
// delete team membership of allowed user
err = ss.Channel().RemoveMember(channel.Id, userGroupA.Id)
require.NoError(t, err)
// ensure removed allowed member still returned by query
requireNUsers(2)
}
func testUserStorePromoteGuestToUser(t *testing.T, ss store.Store) {
// create users
t.Run("Must do nothing with regular user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
require.True(t, user.UpdateAt < updatedUser.UpdateAt)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must do nothing with admin user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user system_admin", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with guest user without teams or channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
})
t.Run("Must work with guest user with teams but no channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
})
t.Run("Must work with guest user with teams and channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with guest user with teams and channels and custom role", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest custom_role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user custom_role", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must no change any other user guest role", func(t *testing.T) {
id := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
teamId1 := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId1,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
id = model.NewId()
user2, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2.Id)) }()
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user1.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user1.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId1, user1.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user1.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
notUpdatedUser, err := ss.User().Get(context.Background(), user2.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", notUpdatedUser.Roles)
notUpdatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId2, user2.Id)
require.NoError(t, nErr)
require.True(t, notUpdatedTeamMember.SchemeGuest)
require.False(t, notUpdatedTeamMember.SchemeUser)
notUpdatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user2.Id)
require.NoError(t, nErr)
require.True(t, notUpdatedChannelMember.SchemeGuest)
require.False(t, notUpdatedChannelMember.SchemeUser)
})
}
func testUserStoreDemoteUserToGuest(t *testing.T, ss store.Store) {
// create users
t.Run("Must do nothing with guest", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
require.True(t, user.UpdateAt < updatedUser.UpdateAt)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, updatedUser.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, updatedUser.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must demote properly an admin user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with user without teams or channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
})
t.Run("Must work with user with teams but no channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
})
t.Run("Must work with user with teams and channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with user with teams and channels and custom role", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user custom_role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest custom_role", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must no change any other user role", func(t *testing.T) {
id := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
teamId1 := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId1,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
id = model.NewId()
user2, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2.Id)) }()
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user1.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId1, user1.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user1.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
notUpdatedUser, err := ss.User().Get(context.Background(), user2.Id)
require.NoError(t, err)
require.Equal(t, "system_user", notUpdatedUser.Roles)
notUpdatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId2, user2.Id)
require.NoError(t, nErr)
require.False(t, notUpdatedTeamMember.SchemeGuest)
require.True(t, notUpdatedTeamMember.SchemeUser)
notUpdatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user2.Id)
require.NoError(t, nErr)
require.False(t, notUpdatedChannelMember.SchemeGuest)
require.True(t, notUpdatedChannelMember.SchemeUser)
})
}
func testDeactivateGuests(t *testing.T, ss store.Store) {
// create users
t.Run("Must disable all guests and no regular user or already deactivated users", func(t *testing.T) {
guest1Random := model.NewId()
guest1, err := ss.User().Save(&model.User{
Email: guest1Random + "@test.com",
Username: "un_" + guest1Random,
Nickname: "nn_" + guest1Random,
FirstName: "f_" + guest1Random,
LastName: "l_" + guest1Random,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest1.Id)) }()
guest2Random := model.NewId()
guest2, err := ss.User().Save(&model.User{
Email: guest2Random + "@test.com",
Username: "un_" + guest2Random,
Nickname: "nn_" + guest2Random,
FirstName: "f_" + guest2Random,
LastName: "l_" + guest2Random,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest2.Id)) }()
guest3Random := model.NewId()
guest3, err := ss.User().Save(&model.User{
Email: guest3Random + "@test.com",
Username: "un_" + guest3Random,
Nickname: "nn_" + guest3Random,
FirstName: "f_" + guest3Random,
LastName: "l_" + guest3Random,
Password: "Password1",
Roles: "system_guest",
DeleteAt: 10,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest3.Id)) }()
regularUserRandom := model.NewId()
regularUser, err := ss.User().Save(&model.User{
Email: regularUserRandom + "@test.com",
Username: "un_" + regularUserRandom,
Nickname: "nn_" + regularUserRandom,
FirstName: "f_" + regularUserRandom,
LastName: "l_" + regularUserRandom,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(regularUser.Id)) }()
ids, err := ss.User().DeactivateGuests()
require.NoError(t, err)
assert.ElementsMatch(t, []string{guest1.Id, guest2.Id}, ids)
u, err := ss.User().Get(context.Background(), guest1.Id)
require.NoError(t, err)
assert.NotEqual(t, u.DeleteAt, int64(0))
u, err = ss.User().Get(context.Background(), guest2.Id)
require.NoError(t, err)
assert.NotEqual(t, u.DeleteAt, int64(0))
u, err = ss.User().Get(context.Background(), guest3.Id)
require.NoError(t, err)
assert.Equal(t, u.DeleteAt, int64(10))
u, err = ss.User().Get(context.Background(), regularUser.Id)
require.NoError(t, err)
assert.Equal(t, u.DeleteAt, int64(0))
})
}
func testUserStoreResetLastPictureUpdate(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().UpdateLastPictureUpdate(u1.Id)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.NotZero(t, user.LastPictureUpdate)
assert.NotZero(t, user.UpdateAt)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
err = ss.User().ResetLastPictureUpdate(u1.Id)
require.NoError(t, err)
ss.User().InvalidateProfileCacheForUser(u1.Id)
user2, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.True(t, user2.UpdateAt > user.UpdateAt)
assert.Zero(t, user2.LastPictureUpdate)
}
func testGetKnownUsers(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
ch3 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c3, nErr := ss.Channel().Save(ch3, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c3.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get know users sharing no channels", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u4.Id)
require.NoError(t, err)
assert.Empty(t, userIds)
})
t.Run("get know users sharing one channel", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u3.Id)
require.NoError(t, err)
assert.Len(t, userIds, 1)
assert.Equal(t, userIds[0], u1.Id)
})
t.Run("get know users sharing multiple channels", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u1.Id)
require.NoError(t, err)
assert.Len(t, userIds, 2)
assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id})
})
}
func testIsEmpty(t *testing.T, ss store.Store) {
ok, err := ss.User().IsEmpty(false)
require.NoError(t, err)
require.True(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.True(t, ok)
u := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u, err = ss.User().Save(u)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.False(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.False(t, ok)
b := &model.Bot{
UserId: u.Id,
OwnerId: model.NewId(),
Username: model.NewId(),
}
_, err = ss.Bot().Save(b)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.False(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.True(t, ok)
err = ss.User().PermanentDelete(u.Id)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.True(t, ok)
}
func testGetUsersWithInvalidEmails(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: "ben@invalid.mattermost.com",
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
users, err := ss.User().GetUsersWithInvalidEmails(0, 50, "localhost,simulator.amazonses.com")
require.NoError(t, err)
assert.Len(t, users, 1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUserTermsOfServiceStore(t *testing.T, ss store.Store) {
t.Run("TestSaveUserTermsOfService", func(t *testing.T) { testSaveUserTermsOfService(t, ss) })
t.Run("TestGetByUserTermsOfService", func(t *testing.T) { testGetByUserTermsOfService(t, ss) })
t.Run("TestDeleteUserTermsOfService", func(t *testing.T) { testDeleteUserTermsOfService(t, ss) })
}
func testSaveUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
savedUserTermsOfService, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
assert.Equal(t, userTermsOfService.UserId, savedUserTermsOfService.UserId)
assert.Equal(t, userTermsOfService.TermsOfServiceId, savedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, savedUserTermsOfService.CreateAt)
// Check we can save a new terms of service id (MM-41611)
newUserTermsOfService := &model.UserTermsOfService{
UserId: userTermsOfService.UserId,
TermsOfServiceId: model.NewId(),
}
savedUserTermsOfService, err = ss.UserTermsOfService().Save(newUserTermsOfService)
require.NoError(t, err)
assert.Equal(t, newUserTermsOfService.UserId, savedUserTermsOfService.UserId)
assert.Equal(t, newUserTermsOfService.TermsOfServiceId, savedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, savedUserTermsOfService.CreateAt)
}
func testGetByUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
_, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
fetchedUserTermsOfService, err := ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
require.NoError(t, err)
assert.Equal(t, userTermsOfService.UserId, fetchedUserTermsOfService.UserId)
assert.Equal(t, userTermsOfService.TermsOfServiceId, fetchedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, fetchedUserTermsOfService.CreateAt)
}
func testDeleteUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
_, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
_, err = ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
require.NoError(t, err)
err = ss.UserTermsOfService().Delete(userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
require.NoError(t, err)
_, err = ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
var nfErr *store.ErrNotFound
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"github.com/mattermost/mattermost-server/v6/model"
)
// This function has a copy of it in app/helper_test
// NewTestId is used for testing as a replacement for model.NewId(). It is a [A-Z0-9] string 26
// characters long. It replaces every odd character with a digit.
func NewTestId() string {
newId := []byte(model.NewId())
for i := 1; i < len(newId); i = i + 2 {
newId[i] = 48 + newId[i-1]%10
}
return string(newId)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestWebhookStore(t *testing.T, ss store.Store) {
t.Run("SaveIncoming", func(t *testing.T) { testWebhookStoreSaveIncoming(t, ss) })
t.Run("UpdateIncoming", func(t *testing.T) { testWebhookStoreUpdateIncoming(t, ss) })
t.Run("GetIncoming", func(t *testing.T) { testWebhookStoreGetIncoming(t, ss) })
t.Run("GetIncomingList", func(t *testing.T) { testWebhookStoreGetIncomingList(t, ss) })
t.Run("GetIncomingListByUser", func(t *testing.T) { testWebhookStoreGetIncomingListByUser(t, ss) })
t.Run("GetIncomingByTeam", func(t *testing.T) { testWebhookStoreGetIncomingByTeam(t, ss) })
t.Run("GetIncomingByTeamByUser", func(t *testing.T) { TestWebhookStoreGetIncomingByTeamByUser(t, ss) })
t.Run("GetIncomingByTeamByChannel", func(t *testing.T) { testWebhookStoreGetIncomingByChannel(t, ss) })
t.Run("DeleteIncoming", func(t *testing.T) { testWebhookStoreDeleteIncoming(t, ss) })
t.Run("DeleteIncomingByChannel", func(t *testing.T) { testWebhookStoreDeleteIncomingByChannel(t, ss) })
t.Run("DeleteIncomingByUser", func(t *testing.T) { testWebhookStoreDeleteIncomingByUser(t, ss) })
t.Run("SaveOutgoing", func(t *testing.T) { testWebhookStoreSaveOutgoing(t, ss) })
t.Run("GetOutgoing", func(t *testing.T) { testWebhookStoreGetOutgoing(t, ss) })
t.Run("GetOutgoingList", func(t *testing.T) { testWebhookStoreGetOutgoingList(t, ss) })
t.Run("GetOutgoingListByUser", func(t *testing.T) { testWebhookStoreGetOutgoingListByUser(t, ss) })
t.Run("GetOutgoingByChannel", func(t *testing.T) { testWebhookStoreGetOutgoingByChannel(t, ss) })
t.Run("GetOutgoingByChannelByUser", func(t *testing.T) { testWebhookStoreGetOutgoingByChannelByUser(t, ss) })
t.Run("GetOutgoingByTeam", func(t *testing.T) { testWebhookStoreGetOutgoingByTeam(t, ss) })
t.Run("GetOutgoingByTeamByUser", func(t *testing.T) { testWebhookStoreGetOutgoingByTeamByUser(t, ss) })
t.Run("DeleteOutgoing", func(t *testing.T) { testWebhookStoreDeleteOutgoing(t, ss) })
t.Run("DeleteOutgoingByChannel", func(t *testing.T) { testWebhookStoreDeleteOutgoingByChannel(t, ss) })
t.Run("DeleteOutgoingByUser", func(t *testing.T) { testWebhookStoreDeleteOutgoingByUser(t, ss) })
t.Run("UpdateOutgoing", func(t *testing.T) { testWebhookStoreUpdateOutgoing(t, ss) })
t.Run("CountIncoming", func(t *testing.T) { testWebhookStoreCountIncoming(t, ss) })
t.Run("CountOutgoing", func(t *testing.T) { testWebhookStoreCountOutgoing(t, ss) })
}
func testWebhookStoreSaveIncoming(t *testing.T, ss store.Store) {
o1 := buildIncomingWebhook()
_, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Webhook().SaveIncoming(o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testWebhookStoreUpdateIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
previousUpdatedAt := o1.UpdateAt
o1.DisplayName = "TestHook"
time.Sleep(10 * time.Millisecond)
webhook, err := ss.Webhook().UpdateIncoming(o1)
require.NoError(t, err)
require.NotEqual(t, webhook.UpdateAt, previousUpdatedAt, "should have updated the UpdatedAt of the hook")
require.Equal(t, "TestHook", webhook.DisplayName, "display name is not updated")
}
func testWebhookStoreGetIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, false)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
webhook, err = ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
_, err = ss.Webhook().GetIncoming("123", false)
require.Error(t, err, "Missing id should have failed")
_, err = ss.Webhook().GetIncoming("123", true)
require.Error(t, err, "Missing id should have failed")
_, err = ss.Webhook().GetIncoming("123", true)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr), "Should have set the status as not found for missing id")
}
func testWebhookStoreGetIncomingList(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
var err error
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
hooks, err := ss.Webhook().GetIncomingList(0, 1000)
require.NoError(t, err)
found := false
for _, hook := range hooks {
if hook.Id == o1.Id {
found = true
}
}
require.True(t, found, "missing webhook")
hooks, err = ss.Webhook().GetIncomingList(0, 1)
require.NoError(t, err)
require.Len(t, hooks, 1, "only 1 should be returned")
}
func testWebhookStoreGetIncomingListByUser(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
o1, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
t.Run("GetIncomingListByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingListByUser(o1.UserId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetIncomingListByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingListByUser("123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetIncomingByTeam(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
hooks, err := ss.Webhook().GetIncomingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt, "invalid returned webhook")
hooks, err = ss.Webhook().GetIncomingByTeam("123", 0, 100)
require.NoError(t, err)
require.Empty(t, hooks, "no webhooks should have returned")
}
func TestWebhookStoreGetIncomingByTeamByUser(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
o2 := buildIncomingWebhook()
o2.TeamId = o1.TeamId //Set both to the same team
o2, err = ss.Webhook().SaveIncoming(o2)
require.NoError(t, err)
t.Run("GetIncomingByTeamByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetIncomingByTeamByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeamByUser(o1.TeamId, o1.UserId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 1)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt)
})
t.Run("GetIncomingByTeamByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeamByUser(o2.TeamId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 0)
})
}
func testWebhookStoreGetIncomingByChannel(t *testing.T, ss store.Store) {
o1 := buildIncomingWebhook()
o1, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhooks, err := ss.Webhook().GetIncomingByChannel(o1.ChannelId)
require.NoError(t, err)
require.Equal(t, webhooks[0].CreateAt, o1.CreateAt, "invalid returned webhook")
webhooks, err = ss.Webhook().GetIncomingByChannel("123")
require.NoError(t, err)
require.Empty(t, webhooks, "no webhooks should have returned")
}
func testWebhookStoreDeleteIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().DeleteIncoming(o1.Id, model.GetMillis())
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err)
}
func testWebhookStoreDeleteIncomingByChannel(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteIncomingByChannel(o1.ChannelId)
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteIncomingByUser(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteIncomingByUser(o1.UserId)
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err, "Missing id should have failed")
}
func buildIncomingWebhook() *model.IncomingWebhook {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
return o1
}
func testWebhookStoreSaveOutgoing(t *testing.T, ss store.Store) {
o1 := model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
_, err := ss.Webhook().SaveOutgoing(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Webhook().SaveOutgoing(&o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testWebhookStoreGetOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
_, err = ss.Webhook().GetOutgoing("123")
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreGetOutgoingListByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
t.Run("GetOutgoingListByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingListByUser(o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetOutgoingListByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingListByUser("123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetOutgoingList(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = model.NewId()
o2.CreatorId = model.NewId()
o2.TeamId = model.NewId()
o2.CallbackURLs = []string{"http://nowhere.com/"}
o2, _ = ss.Webhook().SaveOutgoing(o2)
r1, err := ss.Webhook().GetOutgoingList(0, 1000)
require.NoError(t, err)
hooks := r1
found1 := false
found2 := false
for _, hook := range hooks {
if hook.CreateAt != o1.CreateAt {
found1 = true
}
if hook.CreateAt != o2.CreateAt {
found2 = true
}
}
require.True(t, found1, "missing hook1")
require.True(t, found2, "missing hook2")
result, err := ss.Webhook().GetOutgoingList(0, 2)
require.NoError(t, err)
require.Len(t, result, 2, "wrong number of hooks returned")
}
func testWebhookStoreGetOutgoingByChannel(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
r1, err := ss.Webhook().GetOutgoingByChannel(o1.ChannelId, 0, 100)
require.NoError(t, err)
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned webhook")
result, err := ss.Webhook().GetOutgoingByChannel("123", -1, -1)
require.NoError(t, err)
require.Empty(t, result, "no webhooks should have returned")
}
func testWebhookStoreGetOutgoingByChannelByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = o1.ChannelId
o2.CreatorId = model.NewId()
o2.TeamId = model.NewId()
o2.CallbackURLs = []string{"http://nowhere.com/"}
_, err = ss.Webhook().SaveOutgoing(o2)
require.NoError(t, err)
t.Run("GetOutgoingByChannelByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannel(o1.ChannelId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetOutgoingByChannelByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannelByUser(o1.ChannelId, o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetOutgoingByChannelByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannelByUser(o1.ChannelId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetOutgoingByTeam(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
r1, err := ss.Webhook().GetOutgoingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned webhook")
result, err := ss.Webhook().GetOutgoingByTeam("123", -1, -1)
require.NoError(t, err)
require.Empty(t, result, "no webhooks should have returned")
}
func testWebhookStoreGetOutgoingByTeamByUser(t *testing.T, ss store.Store) {
var err error
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err = ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = model.NewId()
o2.CreatorId = model.NewId()
o2.TeamId = o1.TeamId
o2.CallbackURLs = []string{"http://nowhere.com/"}
o2, err = ss.Webhook().SaveOutgoing(o2)
require.NoError(t, err)
t.Run("GetOutgoingByTeamByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetOutgoingByTeamByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeamByUser(o1.TeamId, o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 1)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt)
})
t.Run("GetOutgoingByTeamByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeamByUser(o2.TeamId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 0)
})
}
func testWebhookStoreDeleteOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().DeleteOutgoing(o1.Id, model.GetMillis())
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteOutgoingByChannel(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteOutgoingByChannel(o1.ChannelId)
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteOutgoingByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteOutgoingByUser(o1.CreatorId)
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreUpdateOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
o1, _ = ss.Webhook().SaveOutgoing(o1)
o1.Token = model.NewId()
o1.Username = "another-test-user-name"
_, err := ss.Webhook().UpdateOutgoing(o1)
require.NoError(t, err)
}
func testWebhookStoreCountIncoming(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
_, _ = ss.Webhook().SaveIncoming(o1)
c, err := ss.Webhook().AnalyticsIncomingCount("")
require.NoError(t, err)
require.NotEqual(t, 0, c, "should have at least 1 incoming hook")
}
func testWebhookStoreCountOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
_, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
r, err := ss.Webhook().AnalyticsOutgoingCount("")
require.NoError(t, err)
require.NotEqual(t, 0, r, "should have at least 1 outgoing hook")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package timerlayer
import (
"context"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type TimerLayer struct {
store.Store
Metrics einterfaces.MetricsInterface
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *TimerLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *TimerLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *TimerLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *TimerLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *TimerLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *TimerLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *TimerLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *TimerLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *TimerLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *TimerLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *TimerLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *TimerLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *TimerLayer) Job() store.JobStore {
return s.JobStore
}
func (s *TimerLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *TimerLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *TimerLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *TimerLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *TimerLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *TimerLayer) Post() store.PostStore {
return s.PostStore
}
func (s *TimerLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *TimerLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *TimerLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *TimerLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *TimerLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *TimerLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *TimerLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *TimerLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *TimerLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *TimerLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *TimerLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *TimerLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *TimerLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *TimerLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *TimerLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *TimerLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *TimerLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *TimerLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *TimerLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *TimerLayer) User() store.UserStore {
return s.UserStore
}
func (s *TimerLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *TimerLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *TimerLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type TimerLayerAuditStore struct {
store.AuditStore
Root *TimerLayer
}
type TimerLayerBotStore struct {
store.BotStore
Root *TimerLayer
}
type TimerLayerChannelStore struct {
store.ChannelStore
Root *TimerLayer
}
type TimerLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *TimerLayer
}
type TimerLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *TimerLayer
}
type TimerLayerCommandStore struct {
store.CommandStore
Root *TimerLayer
}
type TimerLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *TimerLayer
}
type TimerLayerComplianceStore struct {
store.ComplianceStore
Root *TimerLayer
}
type TimerLayerDraftStore struct {
store.DraftStore
Root *TimerLayer
}
type TimerLayerEmojiStore struct {
store.EmojiStore
Root *TimerLayer
}
type TimerLayerFileInfoStore struct {
store.FileInfoStore
Root *TimerLayer
}
type TimerLayerGroupStore struct {
store.GroupStore
Root *TimerLayer
}
type TimerLayerJobStore struct {
store.JobStore
Root *TimerLayer
}
type TimerLayerLicenseStore struct {
store.LicenseStore
Root *TimerLayer
}
type TimerLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *TimerLayer
}
type TimerLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *TimerLayer
}
type TimerLayerOAuthStore struct {
store.OAuthStore
Root *TimerLayer
}
type TimerLayerPluginStore struct {
store.PluginStore
Root *TimerLayer
}
type TimerLayerPostStore struct {
store.PostStore
Root *TimerLayer
}
type TimerLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *TimerLayer
}
type TimerLayerPostPriorityStore struct {
store.PostPriorityStore
Root *TimerLayer
}
type TimerLayerPreferenceStore struct {
store.PreferenceStore
Root *TimerLayer
}
type TimerLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *TimerLayer
}
type TimerLayerReactionStore struct {
store.ReactionStore
Root *TimerLayer
}
type TimerLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *TimerLayer
}
type TimerLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *TimerLayer
}
type TimerLayerRoleStore struct {
store.RoleStore
Root *TimerLayer
}
type TimerLayerSchemeStore struct {
store.SchemeStore
Root *TimerLayer
}
type TimerLayerSessionStore struct {
store.SessionStore
Root *TimerLayer
}
type TimerLayerSharedChannelStore struct {
store.SharedChannelStore
Root *TimerLayer
}
type TimerLayerStatusStore struct {
store.StatusStore
Root *TimerLayer
}
type TimerLayerSystemStore struct {
store.SystemStore
Root *TimerLayer
}
type TimerLayerTeamStore struct {
store.TeamStore
Root *TimerLayer
}
type TimerLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *TimerLayer
}
type TimerLayerThreadStore struct {
store.ThreadStore
Root *TimerLayer
}
type TimerLayerTokenStore struct {
store.TokenStore
Root *TimerLayer
}
type TimerLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *TimerLayer
}
type TimerLayerUploadSessionStore struct {
store.UploadSessionStore
Root *TimerLayer
}
type TimerLayerUserStore struct {
store.UserStore
Root *TimerLayer
}
type TimerLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *TimerLayer
}
type TimerLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *TimerLayer
}
type TimerLayerWebhookStore struct {
store.WebhookStore
Root *TimerLayer
}
func (s *TimerLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
start := time.Now()
result, err := s.AuditStore.Get(user_id, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerAuditStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.AuditStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerAuditStore) Save(audit *model.Audit) error {
start := time.Now()
err := s.AuditStore.Save(audit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Get(userID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.GetAll(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) PermanentDelete(userID string) error {
start := time.Now()
err := s.BotStore.PermanentDelete(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Save(bot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Update(bot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AnalyticsDeletedTypeCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AnalyticsTypeCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Autocomplete", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AutocompleteInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AutocompleteInTeamForSearch", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.ChannelStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) ClearCaches() {
start := time.Now()
s.ChannelStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerChannelStore) ClearMembersForUserCache() {
start := time.Now()
s.ChannelStore.ClearMembersForUserCache()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearMembersForUserCache", success, elapsed)
}
}
func (s *TimerLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
start := time.Now()
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearSidebarOnTeamLeave", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CountPostsAfter", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
start := time.Now()
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CountUrgentPostsAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateDirectChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateInitialSidebarCategories", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateSidebarCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) Delete(channelID string, timestamp int64) error {
start := time.Now()
err := s.ChannelStore.Delete(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
start := time.Now()
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteSidebarCategory", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
start := time.Now()
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteSidebarChannelsByPreferences", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Get(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetAll(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersById(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersById", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersNotifyPropsForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelsCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllDirectChannelsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByNameIncludeDeleted", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelCounts", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelMembersForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelMembersTimezones", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelUnread", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithCursor", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithTeamDataByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetDeleted", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetDeletedByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetFileCount(channelID string) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetFileCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetFileCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetGuestCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
start := time.Now()
result := s.ChannelStore.GetMemberCountFromCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCountFromCache", success, elapsed)
}
return result
}
func (s *TimerLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCountsByGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersByChannelIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUserWithCursor", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUserWithPagination", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersInfoByChannelIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMoreChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPinnedPostCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPinnedPosts(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPinnedPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPrivateChannelsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPublicChannelsByIdsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPublicChannelsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategories", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategoriesForTeamForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategoryOrder", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamChannels(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamForChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamMembersForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GroupSyncedChannelCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GroupSyncedChannelCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
start := time.Now()
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.IncrementMentionCount", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
start := time.Now()
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateAllChannelMembersForUser", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateCacheForChannelMembersNotifyProps", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateChannel(id string) {
start := time.Now()
s.ChannelStore.InvalidateChannel(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateChannel", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
start := time.Now()
s.ChannelStore.InvalidateChannelByName(teamID, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateChannelByName", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateGuestCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateGuestCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateGuestCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateMemberCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateMemberCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateMemberCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidatePinnedPostCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidatePinnedPostCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
start := time.Now()
result := s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.IsUserInChannelUseCache", success, elapsed)
}
return result
}
func (s *TimerLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
start := time.Now()
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.MigrateChannelMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) PermanentDelete(channelID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDelete(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteMembersByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
start := time.Now()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PostCountsByDuration", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
start := time.Now()
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveAllDeactivatedMembers", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) RemoveMember(channelID string, userID string) error {
start := time.Now()
err := s.ChannelStore.RemoveMember(channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveMember", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
start := time.Now()
err := s.ChannelStore.RemoveMembers(channelID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveMembers", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) ResetAllChannelSchemes() error {
start := time.Now()
err := s.ChannelStore.ResetAllChannelSchemes()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ResetAllChannelSchemes", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Restore(channelID string, timestamp int64) error {
start := time.Now()
err := s.ChannelStore.Restore(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Restore", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveDirectChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.SaveMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.SaveMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchAllChannels", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchArchivedInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchForUserInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchGroupChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchMore", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
start := time.Now()
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SetDeleteAt", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) SetShared(channelId string, shared bool) error {
start := time.Now()
err := s.ChannelStore.SetShared(channelId, shared)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SetShared", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Update(channel)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateLastViewedAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateLastViewedAtPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMemberNotifyProps", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
start := time.Now()
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMembersRole", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarCategories", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarCategoryOrder", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarChannelCategoryOnMove", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarChannelsByPreferences", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
start := time.Now()
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UserBelongsToChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetChannelsLeftSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetUsersInChannelDuring", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
start := time.Now()
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.LogJoinEvent", success, elapsed)
}
return err
}
func (s *TimerLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
start := time.Now()
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.LogLeaveEvent", success, elapsed)
}
return err
}
func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerClusterDiscoveryStore) Cleanup() error {
start := time.Now()
err := s.ClusterDiscoveryStore.Cleanup()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.Delete(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.Exists(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Exists", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
start := time.Now()
err := s.ClusterDiscoveryStore.Save(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
start := time.Now()
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.SetLastPingAt", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.AnalyticsCommandCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) Delete(commandID string, timestamp int64) error {
start := time.Now()
err := s.CommandStore.Delete(commandID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) Get(id string) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.GetByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.GetByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.GetByTrigger", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
start := time.Now()
err := s.CommandStore.PermanentDeleteByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.PermanentDeleteByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.CommandStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Save(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Update(hook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) Cleanup() {
start := time.Now()
s.CommandWebhookStore.Cleanup()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Cleanup", success, elapsed)
}
}
func (s *TimerLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
start := time.Now()
result, err := s.CommandWebhookStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
start := time.Now()
result, err := s.CommandWebhookStore.Save(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) TryUse(id string, limit int) error {
start := time.Now()
err := s.CommandWebhookStore.TryUse(id, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.TryUse", success, elapsed)
}
return err
}
func (s *TimerLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
start := time.Now()
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.ComplianceExport", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerComplianceStore) Get(id string) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
start := time.Now()
result, err := s.ComplianceStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
start := time.Now()
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.MessageExport", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Save(compliance)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Update(compliance)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
start := time.Now()
err := s.DraftStore.Delete(userID, channelID, rootID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.GetDraftsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Save(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Update(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
start := time.Now()
err := s.EmojiStore.Delete(emoji, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetList(offset, limit, sort)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetList", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetMultipleByName(names)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetMultipleByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Save(emoji)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
start := time.Now()
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.AttachToPost", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) ClearCaches() {
start := time.Now()
s.FileInfoStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerFileInfoStore) CountAll() (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.CountAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.CountAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
start := time.Now()
result, err := s.FileInfoStore.DeleteForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.DeleteForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetByIds(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetByPath(path)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetByPath", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
start := time.Now()
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetFilesBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetFromMaster(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetFromMaster", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetStorageUsage", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetUptoNSizeFileTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetWithOptions", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
start := time.Now()
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.InvalidateFileInfosForPostCache", success, elapsed)
}
}
func (s *TimerLayerFileInfoStore) PermanentDelete(fileID string) error {
start := time.Now()
err := s.FileInfoStore.PermanentDelete(fileID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDeleteByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Save(info)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
start := time.Now()
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) SetContent(fileID string, content string) error {
start := time.Now()
err := s.FileInfoStore.SetContent(fileID, content)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.SetContent", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Upsert(info)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Upsert", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
start := time.Now()
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.AdminRoleGroupsForSyncableMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersToAdd", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersToRemove", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountChannelMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountGroupsByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountGroupsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountTeamMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Create(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Create", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CreateGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.CreateWithUserIds(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CreateWithUserIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Delete(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Delete(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.DeleteMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.DistinctGroupMemberCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DistinctGroupMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
start := time.Now()
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DistinctGroupMemberCountForSource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Get(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Get(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetAllBySource(groupSource)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetAllBySource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetAllGroupSyncablesByGroupId", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByIDs(groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByIDs", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByName(name, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByRemoteID", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroups", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsAssociatedToChannelsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.GetMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberCount(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberCountWithRestrictions", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsers(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersSortedPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetNonMemberUsersPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupChannelCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupChannelCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupChannelCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCountBySource(source)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCountBySource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCountWithAllowReference()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCountWithAllowReference", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupMemberCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupMemberCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupTeamCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupTeamCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
start := time.Now()
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.PermanentDeleteMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
start := time.Now()
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.PermittedSyncableAdmins", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Restore(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Restore(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Restore", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersToAdd", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersToRemove(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersToRemove", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Update(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpdateGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.UpsertMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpsertMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpsertMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
start := time.Now()
err := s.JobStore.Cleanup(expiryTime, batchSize)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerJobStore) Delete(id string) (string, error) {
start := time.Now()
result, err := s.JobStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Get(id string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByStatus(status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByType(jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypeAndStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypePage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypesPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllPage(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
start := time.Now()
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetCountByStatusAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetNewestJobByStatusAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetNewestJobByStatusesAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Save(job *model.Job) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.Save(job)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
start := time.Now()
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateOptimistically", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.UpdateStatus(id, status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
start := time.Now()
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateStatusOptimistically", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.Save(license)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
start := time.Now()
result, err := s.LinkMetadataStore.Get(url, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LinkMetadataStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
start := time.Now()
result, err := s.LinkMetadataStore.Save(linkMetadata)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LinkMetadataStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
start := time.Now()
err := s.NotifyAdminStore.DeleteBefore(trial, now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.DeleteBefore", success, elapsed)
}
return err
}
func (s *TimerLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.Get(trial)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.GetDataByUserIdAndFeature", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.Save(data)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
start := time.Now()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) DeleteApp(id string) error {
start := time.Now()
err := s.OAuthStore.DeleteApp(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.DeleteApp", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessData(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessDataByRefreshToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessDataByUserForApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetApp(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAppByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetApps(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetApps", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAuthData(code)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAuthorizedApps", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetPreviousAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
start := time.Now()
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.PermanentDeleteAuthDataByUser", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAccessData(token string) error {
start := time.Now()
err := s.OAuthStore.RemoveAccessData(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAccessData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAllAccessData() error {
start := time.Now()
err := s.OAuthStore.RemoveAllAccessData()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAllAccessData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAuthData(code string) error {
start := time.Now()
err := s.OAuthStore.RemoveAuthData(code)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAuthData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
start := time.Now()
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAuthDataByClientId", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.SaveAccessData(accessData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.SaveApp(app)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
start := time.Now()
result, err := s.OAuthStore.SaveAuthData(authData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.UpdateAccessData(accessData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.UpdateAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.UpdateApp(app)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.UpdateApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
start := time.Now()
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.CompareAndDelete", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
start := time.Now()
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.CompareAndSet", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) Delete(pluginID string, key string) error {
start := time.Now()
err := s.PluginStore.Delete(pluginID, key)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) DeleteAllExpired() error {
start := time.Now()
err := s.PluginStore.DeleteAllExpired()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.DeleteAllExpired", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
start := time.Now()
err := s.PluginStore.DeleteAllForPlugin(PluginID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.DeleteAllForPlugin", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
start := time.Now()
result, err := s.PluginStore.Get(pluginID, key)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
start := time.Now()
result, err := s.PluginStore.List(pluginID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.List", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
start := time.Now()
result, err := s.PluginStore.SaveOrUpdate(keyVal)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.SaveOrUpdate", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
start := time.Now()
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.SetWithOptions", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsPostCount(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsPostCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsPostCountsByDay", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsUserCountsWithPostsByDay", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) ClearCaches() {
start := time.Now()
s.PostStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
start := time.Now()
err := s.PostStore.Delete(postID, timestamp, deleteByID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.PostStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
start := time.Now()
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetDirectPostParentsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetEditHistoryForPost(postId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetEditHistoryForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
start := time.Now()
result := s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetEtag", success, elapsed)
}
return result
}
func (s *TimerLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPostsForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPostsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetMaxPostSize() int {
start := time.Now()
result := s.PostStore.GetMaxPostSize()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetMaxPostSize", success, elapsed)
}
return result
}
func (s *TimerLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
start := time.Now()
result, err := s.PostStore.GetNthRecentPostTime(n)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetNthRecentPostTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetOldest() (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetOldest()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetOldest", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
start := time.Now()
result, err := s.PostStore.GetOldestEntityCreationTime()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetOldestEntityCreationTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
start := time.Now()
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetParentsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostAfterTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
start := time.Now()
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostIdAfterTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
start := time.Now()
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostIdBeforeTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
start := time.Now()
result, err := s.PostStore.GetPostReminderMetadata(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostReminderMetadata", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
start := time.Now()
result, err := s.PostStore.GetPostReminders(now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostReminders", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
start := time.Now()
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsBefore", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsByIds(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsByThread(threadID, since)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsByThread", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsCreatedAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsSinceForSync", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
start := time.Now()
result, err := s.PostStore.GetRecentSearchesForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRecentSearchesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
start := time.Now()
result, err := s.PostStore.GetRepliesForExport(parentID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRepliesForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetSingle(id, inclDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetSingle", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
start := time.Now()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetTopDMsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
start := time.Now()
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.HasAutoResponsePostByUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
start := time.Now()
s.PostStore.InvalidateLastPostTimeCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.InvalidateLastPostTimeCache", success, elapsed)
}
}
func (s *TimerLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
start := time.Now()
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.LogRecentSearch", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Overwrite(post)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Overwrite", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.OverwriteMultiple", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) PermanentDeleteByChannel(channelID string) error {
start := time.Now()
err := s.PostStore.PermanentDeleteByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.PostStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Save(post *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Save(post)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SaveMultiple", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.Search(teamID, userID, params)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
start := time.Now()
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SearchPostsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
start := time.Now()
err := s.PostStore.SetPostReminder(reminder)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SetPostReminder", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Update(newPost, oldPost)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
start := time.Now()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
start := time.Now()
result, err := s.PostPriorityStore.GetForPost(postId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
start := time.Now()
result, err := s.PostPriorityStore.GetForPosts(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
start := time.Now()
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.CleanupFlagsBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) Delete(userID string, category string, name string) error {
start := time.Now()
err := s.PreferenceStore.Delete(userID, category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteCategory(userID string, category string) error {
start := time.Now()
err := s.PreferenceStore.DeleteCategory(userID, category)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteCategory", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
start := time.Now()
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteCategoryAndName", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
start := time.Now()
result, err := s.PreferenceStore.Get(userID, category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetAll(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetCategory(userID, category)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetCategoryAndName", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.PreferenceStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) Save(preferences model.Preferences) error {
start := time.Now()
err := s.PreferenceStore.Save(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) Clear(notices []string) error {
start := time.Now()
err := s.ProductNoticesStore.Clear(notices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.Clear", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
start := time.Now()
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.ClearOldNotices", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
start := time.Now()
result, err := s.ProductNoticesStore.GetViews(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.GetViews", success, elapsed)
}
return result, err
}
func (s *TimerLayerProductNoticesStore) View(userID string, notices []string) error {
start := time.Now()
err := s.ProductNoticesStore.View(userID, notices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.View", success, elapsed)
}
return err
}
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.BulkGetForPosts(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.BulkGetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.Delete(reaction)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
start := time.Now()
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteAllWithEmojiName", success, elapsed)
}
return err
}
func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetForPostSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.Save(reaction)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Get(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.GetAll(filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Save(rc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
start := time.Now()
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.SetLastPingAt", success, elapsed)
}
return err
}
func (s *TimerLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Update(rc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.UpdateTopics", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.AddChannels", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.AddTeams", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) Delete(id string) error {
start := time.Now()
err := s.RetentionPolicyStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelPoliciesCountForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelPoliciesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetCount() (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamPoliciesCountForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamPoliciesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Patch(patch)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Patch", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.RemoveChannels", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.RemoveTeams", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Save(policy)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.AllChannelSchemeRoles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.AllChannelSchemeRoles", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
start := time.Now()
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelHigherScopedPermissions", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelRolesUnderTeamRole", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) Delete(roleID string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Delete(roleID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) Get(roleID string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Get(roleID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetAll() ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetByName(ctx, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetByNames(names)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) PermanentDeleteAll() error {
start := time.Now()
err := s.RoleStore.PermanentDeleteAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.PermanentDeleteAll", success, elapsed)
}
return err
}
func (s *TimerLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Save(role)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) CountByScope(scope string) (int64, error) {
start := time.Now()
result, err := s.SchemeStore.CountByScope(scope)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.CountByScope", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
start := time.Now()
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.CountWithoutPermission", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Delete(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Get(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.GetByName(schemeName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) PermanentDeleteAll() error {
start := time.Now()
err := s.SchemeStore.PermanentDeleteAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.PermanentDeleteAll", success, elapsed)
}
return err
}
func (s *TimerLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Save(scheme)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) AnalyticsSessionCount() (int64, error) {
start := time.Now()
result, err := s.SessionStore.AnalyticsSessionCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.AnalyticsSessionCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
start := time.Now()
err := s.SessionStore.Cleanup(expiryTime, batchSize)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessions(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessions", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessionsExpired", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessionsWithActiveDeviceIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
start := time.Now()
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.PermanentDeleteSessionsByUser", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Remove(sessionIDOrToken string) error {
start := time.Now()
err := s.SessionStore.Remove(sessionIDOrToken)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Remove", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) RemoveAllSessions() error {
start := time.Now()
err := s.SessionStore.RemoveAllSessions()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.RemoveAllSessions", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.Save(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
start := time.Now()
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateDeviceId", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
start := time.Now()
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateExpiredNotify", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
start := time.Now()
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateExpiresAt", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
start := time.Now()
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateLastActivityAt", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateProps(session *model.Session) error {
start := time.Now()
err := s.SessionStore.UpdateProps(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateProps", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
start := time.Now()
result, err := s.SessionStore.UpdateRoles(userID, roles)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateRoles", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Delete(channelId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.Delete(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.DeleteRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Get(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAllCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAllCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemote(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemoteByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemoteForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemotes(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemotes", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemotesStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetSingleUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetUsersForSync(filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetUsersForSync", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetUsersForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetUsersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.HasChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.HasChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.HasRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Save(sc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveAttachment(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveRemote(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveUser(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Update(sc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
start := time.Now()
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateAttachmentLastSyncAt", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.UpdateRemote(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
start := time.Now()
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateRemoteCursor", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
start := time.Now()
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateUserLastSyncAt", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
start := time.Now()
result, err := s.SharedChannelStore.UpsertAttachment(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpsertAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) Get(userID string) (*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.Get(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.GetByIds(userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.GetByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
start := time.Now()
result, err := s.StatusStore.GetTotalActiveUsersCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.GetTotalActiveUsersCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) ResetAll() error {
start := time.Now()
err := s.StatusStore.ResetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.ResetAll", success, elapsed)
}
return err
}
func (s *TimerLayerStatusStore) SaveOrUpdate(status *model.Status) error {
start := time.Now()
err := s.StatusStore.SaveOrUpdate(status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.SaveOrUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.UpdateExpiredDNDStatuses", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
start := time.Now()
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.UpdateLastActivityAt", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) Get() (model.StringMap, error) {
start := time.Now()
result, err := s.SystemStore.Get()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) GetByName(name string) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.GetByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.InsertIfExists(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.InsertIfExists", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.PermanentDeleteByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.PermanentDeleteByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) Save(system *model.System) error {
start := time.Now()
err := s.SystemStore.Save(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) SaveOrUpdate(system *model.System) error {
start := time.Now()
err := s.SystemStore.SaveOrUpdate(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.SaveOrUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
start := time.Now()
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.SaveOrUpdateWithWarnMetricHandling", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) Update(system *model.System) error {
start := time.Now()
err := s.SystemStore.Update(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
start := time.Now()
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.AnalyticsGetTeamCountForScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
start := time.Now()
result, err := s.TeamStore.AnalyticsTeamCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.AnalyticsTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.TeamStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) ClearCaches() {
start := time.Now()
s.TeamStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerTeamStore) Get(id string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetActiveMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAll() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
start := time.Now()
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllPrivateTeamListing()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllPrivateTeamListing", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllTeamListing()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllTeamListing", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByEmptyInviteID()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByEmptyInviteID", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByInviteId(inviteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByInviteId", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByName(name string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByNames(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
start := time.Now()
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetChannelUnreadsForAllTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
start := time.Now()
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetChannelUnreadsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
start := time.Now()
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetCommonTeamIDsForTwoUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetMany(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMembersByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
start := time.Now()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetNewTeamMembersSince", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamMembersForExport(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamMembersForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsByScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsByUserId(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsByUserId", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsForUserWithPagination", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTotalMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
start := time.Now()
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetUserTeamIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
start := time.Now()
result, err := s.TeamStore.GroupSyncedTeamCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GroupSyncedTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
start := time.Now()
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.InvalidateAllTeamIdsForUser", success, elapsed)
}
}
func (s *TimerLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
start := time.Now()
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.MigrateTeamMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) PermanentDelete(teamID string) error {
start := time.Now()
err := s.TeamStore.PermanentDelete(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
start := time.Now()
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveAllMembersByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveAllMembersByUser(userID string) error {
start := time.Now()
err := s.TeamStore.RemoveAllMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveAllMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveMember(teamID string, userID string) error {
start := time.Now()
err := s.TeamStore.RemoveMember(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveMember", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
start := time.Now()
err := s.TeamStore.RemoveMembers(teamID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveMembers", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) ResetAllTeamSchemes() error {
start := time.Now()
err := s.TeamStore.ResetAllTeamSchemes()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ResetAllTeamSchemes", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Save(team)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SaveMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SaveMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchAll(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
start := time.Now()
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchAllPaged", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchOpen(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchOpen", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchPrivate(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchPrivate", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Update(team)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
start := time.Now()
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateLastTeamIconUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.UpdateMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
start := time.Now()
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMembersRole", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.UpdateMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
start := time.Now()
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UserBelongsToTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.GetLatest", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.Save(termsOfService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
start := time.Now()
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipForUser", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) Get(id string) (*model.Thread, error) {
start := time.Now()
result, err := s.ThreadStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetMembershipForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetMembershipsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
start := time.Now()
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTeamsUnreadForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadFollowers", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadUnreadReplyCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalThreads", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadMentions", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadThreads", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadUrgentMentions", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MaintainMembership", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsRead", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsReadByChannels", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsReadByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
start := time.Now()
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAsRead", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.UpdateMembership(membership)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.UpdateMembership", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) Cleanup(expiryTime int64) {
start := time.Now()
s.TokenStore.Cleanup(expiryTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Cleanup", success, elapsed)
}
}
func (s *TimerLayerTokenStore) Delete(token string) error {
start := time.Now()
err := s.TokenStore.Delete(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
start := time.Now()
result, err := s.TokenStore.GetAllTokensByType(tokenType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.GetAllTokensByType", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) GetByToken(token string) (*model.Token, error) {
start := time.Now()
result, err := s.TokenStore.GetByToken(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.GetByToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
start := time.Now()
err := s.TokenStore.RemoveAllTokensByType(tokenType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.RemoveAllTokensByType", success, elapsed)
}
return err
}
func (s *TimerLayerTokenStore) Save(recovery *model.Token) error {
start := time.Now()
err := s.TokenStore.Save(recovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.CreateTrueUpReviewStatusRecord", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.GetTrueUpReviewStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Delete(id string) error {
start := time.Now()
err := s.UploadSessionStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.Get(ctx, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.GetForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.GetForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.Save(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Update(session *model.UploadSession) error {
start := time.Now()
err := s.UploadSessionStore.Update(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsActiveCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsActiveCountForPeriod", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetExternalUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetGuestCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetGuestCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetInactiveUsersCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetSystemAdminCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
start := time.Now()
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AutocompleteUsersInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.UserStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) ClearCaches() {
start := time.Now()
s.UserStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.Count(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Count", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) DeactivateGuests() ([]string, error) {
start := time.Now()
result, err := s.UserStore.DeactivateGuests()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.DeactivateGuests", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.DemoteUserToGuest(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.DemoteUserToGuest", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.Get(ctx, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAll() ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllNotInAuthService(authServices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllNotInAuthService", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllProfiles(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllProfilesInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllUsingAuthService(authService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllUsingAuthService", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAnyUnreadPostCountForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByAuth(authData, authService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByAuth", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByEmail(email string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByEmail(email)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByEmail", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByUsername(username string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByUsername(username)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByUsername", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetChannelGroupUsers(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetChannelGroupUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetEtagForAllProfiles() string {
start := time.Now()
result := s.UserStore.GetEtagForAllProfiles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForAllProfiles", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetEtagForProfiles(teamID string) string {
start := time.Now()
result := s.UserStore.GetEtagForProfiles(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForProfiles", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
start := time.Now()
result := s.UserStore.GetEtagForProfilesNotInTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForProfilesNotInTeam", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetFirstSystemAdminID() (string, error) {
start := time.Now()
result, err := s.UserStore.GetFirstSystemAdminID()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetFirstSystemAdminID", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetForLogin", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
start := time.Now()
result, err := s.UserStore.GetKnownUsers(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetKnownUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetMany(ctx, ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetNewUsersForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfileByGroupChannelIdsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfileByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfiles(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesByUsernames", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannel(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannelByAdmin", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannelByStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesNotInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesWithoutTeam(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesWithoutTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetRecentlyActiveUsersForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetSystemAdminProfiles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetSystemAdminProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetTeamGroupUsers(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetTeamGroupUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUnreadCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUnreadCountForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
start := time.Now()
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUsersBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUsersWithInvalidEmails", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) InferSystemInstallDate() (int64, error) {
start := time.Now()
result, err := s.UserStore.InferSystemInstallDate()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InferSystemInstallDate", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) InsertUsers(users []*model.User) error {
start := time.Now()
err := s.UserStore.InsertUsers(users)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InsertUsers", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) InvalidateProfileCacheForUser(userID string) {
start := time.Now()
s.UserStore.InvalidateProfileCacheForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfileCacheForUser", success, elapsed)
}
}
func (s *TimerLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
start := time.Now()
s.UserStore.InvalidateProfilesInChannelCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfilesInChannelCache", success, elapsed)
}
}
func (s *TimerLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
start := time.Now()
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfilesInChannelCacheByUser", success, elapsed)
}
}
func (s *TimerLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
start := time.Now()
result, err := s.UserStore.IsEmpty(excludeBots)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.IsEmpty", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) PermanentDelete(userID string) error {
start := time.Now()
err := s.UserStore.PermanentDelete(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) PromoteGuestToUser(userID string) error {
start := time.Now()
err := s.UserStore.PromoteGuestToUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.PromoteGuestToUser", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
start := time.Now()
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ResetAuthDataToEmailForUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) ResetLastPictureUpdate(userID string) error {
start := time.Now()
err := s.UserStore.ResetLastPictureUpdate(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ResetLastPictureUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) Save(user *model.User) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.Save(user)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.Search(teamID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchInChannel(channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchInGroup(groupID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchInGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchWithoutTeam(term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchWithoutTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
start := time.Now()
result, err := s.UserStore.Update(user, allowRoleUpdate)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
start := time.Now()
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
start := time.Now()
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateFailedPasswordAttempts", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateLastPictureUpdate(userID string) error {
start := time.Now()
err := s.UserStore.UpdateLastPictureUpdate(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateLastPictureUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateMfaActive(userID string, active bool) error {
start := time.Now()
err := s.UserStore.UpdateMfaActive(userID, active)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateMfaActive", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
start := time.Now()
err := s.UserStore.UpdateMfaSecret(userID, secret)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateMfaSecret", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
start := time.Now()
err := s.UserStore.UpdateNotifyProps(userID, props)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateNotifyProps", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdatePassword(userID string, newPassword string) error {
start := time.Now()
err := s.UserStore.UpdatePassword(userID, newPassword)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdatePassword", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.UpdateUpdateAt(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateUpdateAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
start := time.Now()
result, err := s.UserStore.VerifyEmail(userID, email)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.VerifyEmail", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Delete(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.Delete(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
start := time.Now()
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.DeleteAllForUser", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Get(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetByToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Save(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Search(term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.UpdateTokenDisable", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.UpdateTokenEnable", success, elapsed)
}
return err
}
func (s *TimerLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
start := time.Now()
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
start := time.Now()
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
start := time.Now()
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.AnalyticsIncomingCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.AnalyticsOutgoingCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) ClearCaches() {
start := time.Now()
s.WebhookStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
start := time.Now()
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.DeleteIncoming", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
start := time.Now()
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.DeleteOutgoing", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByTeamByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingList(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingList", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingListByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoing(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByChannelByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByTeamByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingList", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingListByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) InvalidateWebhookCache(webhook string) {
start := time.Now()
s.WebhookStore.InvalidateWebhookCache(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.InvalidateWebhookCache", success, elapsed)
}
}
func (s *TimerLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteIncomingByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteIncomingByUser", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteOutgoingByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteOutgoingByUser", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.SaveIncoming(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.SaveIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.SaveOutgoing(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.SaveOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.UpdateIncoming(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.UpdateIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.UpdateOutgoing(hook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.UpdateOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayer) Close() {
s.Store.Close()
}
func (s *TimerLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *TimerLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *TimerLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *TimerLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *TimerLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *TimerLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *TimerLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *TimerLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLayer {
newStore := TimerLayer{
Store: childStore,
Metrics: metrics,
}
newStore.AuditStore = &TimerLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &TimerLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &TimerLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &TimerLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &TimerLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &TimerLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &TimerLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &TimerLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &TimerLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &TimerLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &TimerLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &TimerLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &TimerLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &TimerLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &TimerLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &TimerLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &TimerLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &TimerLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &TimerLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &TimerLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &TimerLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &TimerLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &TimerLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &TimerLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &TimerLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &TimerLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &TimerLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &TimerLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &TimerLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &TimerLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &TimerLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &TimerLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &TimerLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &TimerLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"encoding/json"
"io"
"testing"
)
// AssertLog asserts that a JSON-encoded buffer of logs contains one with the given level and message.
func AssertLog(t *testing.T, logs io.Reader, level, message string) {
dec := json.NewDecoder(logs)
for {
var log struct {
Level string
Msg string
}
if err := dec.Decode(&log); err == io.EOF {
break
} else if err != nil {
t.Logf("Error decoding log entry: %s", err)
continue
}
if log.Level == level && log.Msg == message {
return
}
}
t.Fatalf("failed to find %s log message: %s", level, message)
}
// AssertNoLog asserts that a JSON-encoded buffer of logs does not contains one with the given level and message.
func AssertNoLog(t *testing.T, logs io.Reader, level, message string) {
dec := json.NewDecoder(logs)
for {
var log struct {
Level string
Msg string
}
if err := dec.Decode(&log); err == io.EOF {
break
} else if err != nil {
t.Logf("Error decoding log entry: %s", err)
continue
}
if log.Level == level && log.Msg == message {
t.Fatalf("found %s log message: %s", level, message)
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
type FakeClusterInterface struct {
clusterMessageHandler einterfaces.ClusterMessageHandler
mut sync.RWMutex
messages []*model.ClusterMessage
}
func (c *FakeClusterInterface) StartInterNodeCommunication() {}
func (c *FakeClusterInterface) StopInterNodeCommunication() {}
func (c *FakeClusterInterface) RegisterClusterMessageHandler(event model.ClusterEvent, crm einterfaces.ClusterMessageHandler) {
c.clusterMessageHandler = crm
}
func (c *FakeClusterInterface) HealthScore() int {
return 0
}
func (c *FakeClusterInterface) GetClusterId() string { return "" }
func (c *FakeClusterInterface) IsLeader() bool { return false }
func (c *FakeClusterInterface) GetMyClusterInfo() *model.ClusterInfo { return nil }
func (c *FakeClusterInterface) GetClusterInfos() []*model.ClusterInfo { return nil }
func (c *FakeClusterInterface) SendClusterMessage(message *model.ClusterMessage) {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = append(c.messages, message)
}
func (c *FakeClusterInterface) SendClusterMessageToNode(nodeID string, message *model.ClusterMessage) error {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = append(c.messages, message)
return nil
}
func (c *FakeClusterInterface) NotifyMsg(buf []byte) {}
func (c *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model.AppError) {
return nil, nil
}
func (c *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) {
return []string{}, nil
}
func (c *FakeClusterInterface) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
return make(map[string][]string), nil
}
func (c *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
return nil
}
func (c *FakeClusterInterface) SendClearRoleCacheMessage() {
if c.clusterMessageHandler != nil {
c.clusterMessageHandler(&model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForRoles,
})
}
}
func (c *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
return nil, nil
}
func (c *FakeClusterInterface) GetMessages() []*model.ClusterMessage {
c.mut.RLock()
defer c.mut.RUnlock()
return c.messages
}
func (c *FakeClusterInterface) SelectMessages(filterCond func(message *model.ClusterMessage) bool) []*model.ClusterMessage {
c.mut.RLock()
defer c.mut.RUnlock()
filteredMessages := []*model.ClusterMessage{}
for _, msg := range c.messages {
if filterCond(msg) {
filteredMessages = append(filteredMessages, msg)
}
}
return filteredMessages
}
func (c *FakeClusterInterface) ClearMessages() {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
type MainHelper struct {
Settings *model.SqlSettings
Store store.Store
SearchEngine *searchengine.Broker
SQLStore *sqlstore.SqlStore
ClusterInterface *FakeClusterInterface
status int
testResourcePath string
replicas []string
}
type HelperOptions struct {
EnableStore bool
EnableResources bool
WithReadReplica bool
}
func NewMainHelper() *MainHelper {
return NewMainHelperWithOptions(&HelperOptions{
EnableStore: true,
EnableResources: true,
})
}
func NewMainHelperWithOptions(options *HelperOptions) *MainHelper {
var mainHelper MainHelper
flag.Parse()
utils.TranslationsPreInit()
if options != nil {
if options.EnableStore && !testing.Short() {
mainHelper.setupStore(options.WithReadReplica)
}
if options.EnableResources {
mainHelper.setupResources()
}
}
return &mainHelper
}
func (h *MainHelper) Main(m *testing.M) {
if h.testResourcePath != "" {
prevDir, err := os.Getwd()
if err != nil {
panic("Failed to get current working directory: " + err.Error())
}
err = os.Chdir(h.testResourcePath)
if err != nil {
panic(fmt.Sprintf("Failed to set current working directory to %s: %s", h.testResourcePath, err.Error()))
}
defer func() {
err := os.Chdir(prevDir)
if err != nil {
panic(fmt.Sprintf("Failed to restore current working directory to %s: %s", prevDir, err.Error()))
}
}()
}
h.status = m.Run()
}
func (h *MainHelper) setupStore(withReadReplica bool) {
driverName := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if driverName == "" {
driverName = model.DatabaseDriverPostgres
}
h.Settings = storetest.MakeSqlSettings(driverName, withReadReplica)
h.replicas = h.Settings.DataSourceReplicas
config := &model.Config{}
config.SetDefaults()
h.SearchEngine = searchengine.NewBroker(config)
h.ClusterInterface = &FakeClusterInterface{}
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.Store = searchlayer.NewSearchLayer(&TestStore{
h.SQLStore,
}, h.SearchEngine, config)
}
func (h *MainHelper) ToggleReplicasOff() {
if h.SQLStore.GetLicense() == nil {
panic("expecting a license to use this")
}
h.Settings.DataSourceReplicas = []string{}
lic := h.SQLStore.GetLicense()
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.SQLStore.UpdateLicense(lic)
}
func (h *MainHelper) ToggleReplicasOn() {
if h.SQLStore.GetLicense() == nil {
panic("expecting a license to use this")
}
h.Settings.DataSourceReplicas = h.replicas
lic := h.SQLStore.GetLicense()
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.SQLStore.UpdateLicense(lic)
}
func (h *MainHelper) setupResources() {
var err error
h.testResourcePath, err = SetupTestResources()
if err != nil {
panic("failed to setup test resources: " + err.Error())
}
}
// PreloadMigrations preloads the migrations and roles into the database
// so that they are not run again when the migrations happen every time
// the server is started.
// This change is forward-compatible with new migrations and only new migrations
// will get executed.
// Only if the schema of either roles or systems table changes, this will break.
// In that case, just update the migrations or comment this out for the time being.
// In the worst case, only an optimization is lost.
//
// Re-generate the files with:
// pg_dump -a -h localhost -U mmuser -d <> --no-comments --inserts -t roles -t systems
// mysqldump -u root -p <> --no-create-info --extended-insert=FALSE Systems Roles
// And keep only the permission related rows in the systems table output.
func (h *MainHelper) PreloadMigrations() {
var buf []byte
var err error
basePath := os.Getenv("MM_SERVER_PATH")
if basePath == "" {
basePath = "mattermost-server/server"
}
relPath := "channels/testlib/testdata"
switch *h.Settings.DriverName {
case model.DatabaseDriverPostgres:
finalPath := filepath.Join(basePath, relPath, "postgres_migration_warmup.sql")
buf, err = os.ReadFile(finalPath)
if err != nil {
panic(fmt.Errorf("cannot read file: %v", err))
}
case model.DatabaseDriverMysql:
finalPath := filepath.Join(basePath, relPath, "mysql_migration_warmup.sql")
buf, err = os.ReadFile(finalPath)
if err != nil {
panic(fmt.Errorf("cannot read file: %v", err))
}
}
handle := h.SQLStore.GetMasterX()
_, err = handle.Exec(string(buf))
if err != nil {
panic(errors.Wrap(err, "Error preloading migrations. Check if you have &multiStatements=true in your DSN if you are using MySQL. Or perhaps the schema changed? If yes, then update the warmup files accordingly"))
}
}
func (h *MainHelper) Close() error {
if h.SQLStore != nil {
h.SQLStore.Close()
}
if h.Settings != nil {
storetest.CleanupSqlSettings(h.Settings)
}
if h.testResourcePath != "" {
os.RemoveAll(h.testResourcePath)
}
if r := recover(); r != nil {
log.Fatalln(r)
}
os.Exit(h.status)
return nil
}
func (h *MainHelper) GetSQLSettings() *model.SqlSettings {
if h.Settings == nil {
panic("MainHelper not initialized with database access.")
}
return h.Settings
}
func (h *MainHelper) GetStore() store.Store {
if h.Store == nil {
panic("MainHelper not initialized with store.")
}
return h.Store
}
func (h *MainHelper) GetSQLStore() *sqlstore.SqlStore {
if h.SQLStore == nil {
panic("MainHelper not initialized with sql store.")
}
return h.SQLStore
}
func (h *MainHelper) GetClusterInterface() *FakeClusterInterface {
if h.ClusterInterface == nil {
panic("MainHelper not initialized with cluster interface.")
}
return h.ClusterInterface
}
func (h *MainHelper) GetSearchEngine() *searchengine.Broker {
if h.SearchEngine == nil {
panic("MainHelper not initialized with search engine")
}
return h.SearchEngine
}
func (h *MainHelper) SetReplicationLagForTesting(seconds int) error {
if dn := h.SQLStore.DriverName(); dn != model.DatabaseDriverMysql {
return fmt.Errorf("method not implemented for %q database driver, only %q is supported", dn, model.DatabaseDriverMysql)
}
err := h.execOnEachReplica("STOP SLAVE SQL_THREAD FOR CHANNEL ''")
if err != nil {
return err
}
err = h.execOnEachReplica(fmt.Sprintf("CHANGE MASTER TO MASTER_DELAY = %d", seconds))
if err != nil {
return err
}
err = h.execOnEachReplica("START SLAVE SQL_THREAD FOR CHANNEL ''")
if err != nil {
return err
}
return nil
}
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
for _, replica := range h.SQLStore.ReplicaXs {
_, err := replica.Exec(query, args...)
if err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
)
const (
resourceTypeFile = iota
resourceTypeFolder
)
const (
actionCopy = iota
actionSymlink
)
const root = "___mattermost-server"
type testResourceDetails struct {
src string
dest string
resType int8
action int8
}
func findFile(path string) string {
return fileutils.FindPath(path, fileutils.CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
func findDir(dir string) (string, bool) {
if dir == root {
srcPath := findFile("go.mod")
if srcPath == "" {
return "./", false
}
return path.Dir(srcPath), true
}
found := fileutils.FindPath(dir, fileutils.CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
func getTestResourcesToSetup() []testResourceDetails {
var srcPath string
var found bool
var testResourcesToSetup = []testResourceDetails{
{root, "mattermost-server", resourceTypeFolder, actionSymlink},
{"go.mod", "go.mod", resourceTypeFile, actionSymlink},
{"i18n", "i18n", resourceTypeFolder, actionSymlink},
{"templates", "templates", resourceTypeFolder, actionSymlink},
{"tests", "tests", resourceTypeFolder, actionSymlink},
{"fonts", "fonts", resourceTypeFolder, actionSymlink},
{"channels/utils/policies-roles-mapping.json", "channels/utils/policies-roles-mapping.json", resourceTypeFile, actionSymlink},
}
// Finding resources and setting full path to source to be used for further processing
for i, testResource := range testResourcesToSetup {
if testResource.resType == resourceTypeFile {
srcPath = findFile(testResource.src)
if srcPath == "" {
panic(fmt.Sprintf("Failed to find file %s", testResource.src))
}
testResourcesToSetup[i].src = srcPath
} else if testResource.resType == resourceTypeFolder {
srcPath, found = findDir(testResource.src)
if !found {
panic(fmt.Sprintf("Failed to find folder %s", testResource.src))
}
testResourcesToSetup[i].src = srcPath
} else {
panic(fmt.Sprintf("Invalid resource type: %d", testResource.resType))
}
}
return testResourcesToSetup
}
func CopyFile(src, dst string) error {
fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{DriverName: "local", Directory: ""})
if err != nil {
return errors.Wrapf(err, "failed to copy file %s to %s", src, dst)
}
if err = fileBackend.CopyFile(src, dst); err != nil {
return errors.Wrapf(err, "failed to copy file %s to %s", src, dst)
}
return nil
}
func SetupTestResources() (string, error) {
testResourcesToSetup := getTestResourcesToSetup()
tempDir, err := os.MkdirTemp("", "testlib")
if err != nil {
return "", errors.Wrap(err, "failed to create temporary directory")
}
pluginsDir := path.Join(tempDir, "plugins")
err = os.Mkdir(pluginsDir, 0700)
if err != nil {
return "", errors.Wrapf(err, "failed to create plugins directory %s", pluginsDir)
}
clientDir := path.Join(tempDir, "client")
err = os.Mkdir(clientDir, 0700)
if err != nil {
return "", errors.Wrapf(err, "failed to create client directory %s", clientDir)
}
err = setupConfig(path.Join(tempDir, "config"))
if err != nil {
return "", errors.Wrap(err, "failed to setup config")
}
var resourceDestInTemp string
// Setting up test resources in temp.
// Action in each resource tells whether it needs to be copied or just symlinked
for _, testResource := range testResourcesToSetup {
resourceDestInTemp = filepath.Join(tempDir, testResource.dest)
if testResource.action == actionCopy {
if testResource.resType == resourceTypeFile {
if err = CopyFile(testResource.src, resourceDestInTemp); err != nil {
return "", err
}
} else if testResource.resType == resourceTypeFolder {
err = utils.CopyDir(testResource.src, resourceDestInTemp)
if err != nil {
return "", errors.Wrapf(err, "failed to copy folder %s to %s", testResource.src, resourceDestInTemp)
}
}
} else if testResource.action == actionSymlink {
destDir := path.Dir(resourceDestInTemp)
if destDir != "." {
err = os.MkdirAll(destDir, os.ModePerm)
if err != nil {
return "", errors.Wrapf(err, "failed to make dir %s", destDir)
}
}
err = os.Symlink(testResource.src, resourceDestInTemp)
if err != nil {
return "", errors.Wrapf(err, "failed to symlink %s to %s", testResource.src, resourceDestInTemp)
}
} else {
return "", errors.Wrapf(err, "Invalid action: %d", testResource.action)
}
}
return tempDir, nil
}
func setupConfig(configDir string) error {
var err error
var config model.Config
config.SetDefaults()
err = os.Mkdir(configDir, 0700)
if err != nil {
return errors.Wrapf(err, "failed to create config directory %s", configDir)
}
buf, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %v", err)
}
configJSON := path.Join(configDir, "config.json")
err = os.WriteFile(configJSON, buf, 0644)
if err != nil {
return errors.Wrapf(err, "failed to write config to %s", configJSON)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"net/http"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
)
type TestStore struct {
store.Store
}
func (s *TestStore) Close() {
// Don't propagate to the underlying store, since this instance is persistent.
}
func GetMockStoreForSetupFunctions() *mocks.Store {
mockStore := mocks.Store{}
systemStore := mocks.SystemStore{}
systemStore.On("GetByName", "FirstAdminSetupComplete").Return(&model.System{Name: "FirstAdminSetupComplete", Value: "true"}, nil)
systemStore.On("GetByName", "RemainingSchemaMigrations").Return(&model.System{Name: "RemainingSchemaMigrations", Value: "true"}, nil)
systemStore.On("GetByName", "ContentExtractionConfigDefaultTrueMigrationComplete").Return(&model.System{Name: "ContentExtractionConfigDefaultTrueMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "UpgradedFromTE").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "ContentExtractionConfigMigrationComplete").Return(&model.System{Name: "ContentExtractionConfigMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "AsymmetricSigningKey").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "PostActionCookieSecret").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: strconv.FormatInt(model.GetMillis(), 10)}, nil)
systemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
systemStore.On("GetByName", "AdvancedPermissionsMigrationComplete").Return(&model.System{Name: "AdvancedPermissionsMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "EmojisPermissionsMigrationComplete").Return(&model.System{Name: "EmojisPermissionsMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "GuestRolesCreationMigrationComplete").Return(&model.System{Name: "GuestRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "SystemConsoleRolesCreationMigrationComplete").Return(&model.System{Name: "SystemConsoleRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "PlaybookRolesCreationMigrationComplete").Return(&model.System{Name: "PlaybookRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "PostPriorityConfigDefaultTrueMigrationComplete").Return(&model.System{Name: "PostPriorityConfigDefaultTrueMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyEmojiPermissionsSplit).Return(&model.System{Name: model.MigrationKeyEmojiPermissionsSplit, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyWebhookPermissionsSplit).Return(&model.System{Name: model.MigrationKeyWebhookPermissionsSplit, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyListJoinPublicPrivateTeams).Return(&model.System{Name: model.MigrationKeyListJoinPublicPrivateTeams, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyRemovePermanentDeleteUser).Return(&model.System{Name: model.MigrationKeyRemovePermanentDeleteUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddBotPermissions).Return(&model.System{Name: model.MigrationKeyAddBotPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyApplyChannelManageDeleteToChannelUser).Return(&model.System{Name: model.MigrationKeyApplyChannelManageDeleteToChannelUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyRemoveChannelManageDeleteFromTeamUser).Return(&model.System{Name: model.MigrationKeyRemoveChannelManageDeleteFromTeamUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyViewMembersNewPermission).Return(&model.System{Name: model.MigrationKeyViewMembersNewPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageGuestsPermissions).Return(&model.System{Name: model.MigrationKeyAddManageGuestsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyChannelModerationsPermissions).Return(&model.System{Name: model.MigrationKeyChannelModerationsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddUseGroupMentionsPermission).Return(&model.System{Name: model.MigrationKeyAddUseGroupMentionsPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSystemConsolePermissions).Return(&model.System{Name: model.MigrationKeyAddSystemConsolePermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddConvertChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddConvertChannelPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSystemRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddSystemRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddBillingPermissions).Return(&model.System{Name: model.MigrationKeyAddBillingPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddDownloadComplianceExportResults).Return(&model.System{Name: model.MigrationKeyAddDownloadComplianceExportResults, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSiteSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddSiteSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddExperimentalSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddExperimentalSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddAuthenticationSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddAuthenticationSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddComplianceSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddExperimentalSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddEnvironmentSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddEnvironmentSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddReportingSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddReportingSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddTestEmailAncillaryPermission).Return(&model.System{Name: model.MigrationKeyAddTestEmailAncillaryPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddAboutSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddAboutSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddIntegrationsSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddIntegrationsSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageSharedChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSharedChannelPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageSecureConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSecureConnectionsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddPlaybooksPermissions).Return(&model.System{Name: model.MigrationKeyAddPlaybooksPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissionRestore).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)
userStore := mocks.UserStore{}
userStore.On("Count", mock.AnythingOfType("model.UserCountOptions")).Return(int64(1), nil)
userStore.On("DeactivateGuests").Return(nil, nil)
userStore.On("ClearCaches").Return(nil)
postStore := mocks.PostStore{}
postStore.On("GetMaxPostSize").Return(4000)
statusStore := mocks.StatusStore{}
statusStore.On("ResetAll").Return(nil)
channelStore := mocks.ChannelStore{}
channelStore.On("ClearCaches").Return(nil)
schemeStore := mocks.SchemeStore{}
schemeStore.On("GetAllPage", model.SchemeScopeTeam, mock.Anything, 100).Return([]*model.Scheme{}, nil)
teamStore := mocks.TeamStore{}
roleStore := mocks.RoleStore{}
roleStore.On("GetAll").Return([]*model.Role{}, nil)
sessionStore := mocks.SessionStore{}
oAuthStore := mocks.OAuthStore{}
groupStore := mocks.GroupStore{}
mockStore.On("System").Return(&systemStore)
mockStore.On("User").Return(&userStore)
mockStore.On("Post").Return(&postStore)
mockStore.On("Status").Return(&statusStore)
mockStore.On("Channel").Return(&channelStore)
mockStore.On("Team").Return(&teamStore)
mockStore.On("Role").Return(&roleStore)
mockStore.On("Scheme").Return(&schemeStore)
mockStore.On("Close").Return(nil)
mockStore.On("DropAllTables").Return(nil)
mockStore.On("MarkSystemRanUnitTests").Return(nil)
mockStore.On("Session").Return(&sessionStore)
mockStore.On("OAuth").Return(&oAuthStore)
mockStore.On("Group").Return(&groupStore)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
return &mockStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func CheckOrigin(r *http.Request, allowedOrigins string) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
if allowedOrigins == "*" {
return true
}
for _, allowed := range strings.Split(allowedOrigins, " ") {
if allowed == origin {
return true
}
}
return false
}
func OriginChecker(allowedOrigins string) func(*http.Request) bool {
return func(r *http.Request) bool {
return CheckOrigin(r, allowedOrigins)
}
}
func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) {
RenderWebError(config, w, r, err.StatusCode, url.Values{
"message": []string{err.Message},
}, s)
}
func RenderWebError(config *model.Config, w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) {
queryString := params.Encode()
subpath, _ := GetSubpathFromConfig(config)
h := crypto.SHA256
sum := h.New()
sum.Write([]byte(path.Join(subpath, "error") + "?" + queryString))
signature, err := s.Sign(rand.Reader, sum.Sum(nil), h)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
destination := path.Join(subpath, "error") + "?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature)
if status >= 300 && status < 400 {
http.Redirect(w, r, destination, status)
return
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
fmt.Fprintln(w, `<!DOCTYPE html><html><head></head>`)
fmt.Fprintln(w, `<body onload="window.location = '`+template.HTMLEscapeString(template.JSEscapeString(destination))+`'">`)
fmt.Fprintln(w, `<noscript><meta http-equiv="refresh" content="0; url=`+template.HTMLEscapeString(destination)+`"></noscript>`)
fmt.Fprintln(w, `<!-- web error message -->`)
fmt.Fprintln(w, `<a href="`+template.HTMLEscapeString(destination)+`" style="color: #c0c0c0;">...</a>`)
fmt.Fprintln(w, `</body></html>`)
}
func RenderMobileAuthComplete(w http.ResponseWriter, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width: 64px; height: 64px; fill: #3c763d">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path stroke="green" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</svg>
<h2> `+i18n.T("api.oauth.auth_complete")+` </h2>
<p id="redirecting-message"> `+i18n.T("api.oauth.redirecting_back")+` </p>
<p id="close-tab-message" style="display: none"> `+i18n.T("api.oauth.close_browser")+` </p>
<p> `+i18n.T("api.oauth.click_redirect", model.StringInterface{"Link": link})+` </p>
<meta http-equiv="refresh" content="2; url=`+link+`">
<script>
window.onload = function() {
setTimeout(function() {
document.getElementById('redirecting-message').style.display = 'none';
document.getElementById('close-tab-message').style.display = 'block';
}, 2000);
}
</script>
`)
}
func RenderMobileError(config *model.Config, w http.ResponseWriter, err *model.AppError, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
var invalidSchemes = map[string]bool{
"data": true,
"javascript": true,
"vbscript": true,
}
u, redirectErr := url.Parse(redirectURL)
if redirectErr != nil || invalidSchemes[u.Scheme] {
link = *config.ServiceSettings.SiteURL
}
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" style="width: 64px; height: 64px; fill: #ccc">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</svg>
<h2> `+i18n.T("error")+` </h2>
<p> `+err.Message+` </p>
<a href="`+link+`">
`+i18n.T("api.back_to_app", map[string]any{"SiteName": config.TeamSettings.SiteName})+`
</a>
`)
}
func RenderMobileMessage(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
<style>
body {
color: #333;
background-color: #fff;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857143;
}
a {
color: #337ab7;
text-decoration: none;
}
a:focus, a:hover {
color: #23527c;
text-decoration: underline;
}
h2 {
font-size: 30px;
margin: 20px 0 10px 0;
font-weight: 500;
line-height: 1.1
}
p {
margin: 0 0 10px;
}
.message-container {
color: #555;
display: table-cell;
padding: 5em 0;
text-align: left;
vertical-align: top;
}
</style>
</head>
<body>
<!-- mobile app message -->
<div class="message-container">
`+message+`
</div>
</body>
</html>
`)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func sanitizePath(p string) string {
dir := strings.ReplaceAll(filepath.Dir(filepath.Clean(p)), "..", "")
base := filepath.Base(p)
if strings.Count(base, ".") == len(base) {
return ""
}
return filepath.Join(dir, base)
}
// UnzipToPath extracts a given zip archive into a given path.
// It returns a list of extracted paths.
func UnzipToPath(zipFile io.ReaderAt, size int64, outPath string) ([]string, error) {
rd, err := zip.NewReader(zipFile, size)
if err != nil {
return nil, fmt.Errorf("failed to create reader: %w", err)
}
paths := make([]string, len(rd.File))
for i, f := range rd.File {
filePath := sanitizePath(f.Name)
if filePath == "" {
return nil, fmt.Errorf("invalid filepath `%s`", f.Name)
}
path := filepath.Join(outPath, filePath)
paths[i] = path
if f.FileInfo().IsDir() {
if err := os.Mkdir(path, 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
continue
}
if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
}
outFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
defer outFile.Close()
file, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
if _, err := io.Copy(outFile, file); err != nil {
return nil, fmt.Errorf("failed to write to file: %w", err)
}
}
return paths, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
var backoffTimeouts = []time.Duration{50 * time.Millisecond, 100 * time.Millisecond, 200 * time.Millisecond, 200 * time.Millisecond, 400 * time.Millisecond, 400 * time.Millisecond}
// ProgressiveRetry executes a BackoffOperation and waits an increasing time before retrying the operation.
func ProgressiveRetry(operation func() error) error {
var err error
for attempts := 0; attempts < len(backoffTimeouts); attempts++ {
err = operation()
if err == nil {
return nil
}
time.Sleep(backoffTimeouts[attempts])
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"testing"
"github.com/stretchr/testify/require"
)
func CreateTestGif(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create gif: %v", err)
return buffer.Bytes()
}
func CreateTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
var buffer bytes.Buffer
img := gif.GIF{
Image: make([]*image.Paletted, frames),
Delay: make([]int, frames),
}
for i := 0; i < frames; i++ {
img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
img.Delay[i] = 0
}
err := gif.EncodeAll(&buffer, &img)
require.NoErrorf(t, err, "failed to create animated gif: %v", err)
return buffer.Bytes()
}
func CreateTestJpeg(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create jpeg: %v", err)
return buffer.Bytes()
}
func CreateTestPng(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)))
require.NoErrorf(t, err, "failed to create png: %v", err)
return buffer.Bytes()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// CopyFile will copy a file from src path to dst path.
// Overwrites any existing files at dst.
// Permissions are copied from file at src to the new file at dst.
func CopyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return
}
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
stat, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, stat.Mode())
if err != nil {
return
}
return
}
// CopyDir will copy a directory and all contained files and directories.
// src must exist and dst must not exist.
// Permissions are preserved when possible. Symlinks are skipped.
func CopyDir(src string, dst string) (err error) {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
stat, err := os.Stat(src)
if err != nil {
return
}
if !stat.IsDir() {
return fmt.Errorf("source must be a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
return fmt.Errorf("destination already exists")
}
err = os.MkdirAll(dst, stat.Mode())
if err != nil {
return
}
items, err := os.ReadDir(src)
if err != nil {
return
}
for _, item := range items {
srcPath := filepath.Join(src, item.Name())
dstPath := filepath.Join(dst, item.Name())
if item.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return
}
} else {
info, ierr := item.Info()
if ierr != nil {
continue
}
if info.Mode()&os.ModeSymlink != 0 {
continue
}
err = CopyFile(srcPath, dstPath)
if err != nil {
return
}
}
}
return
}
var SizeLimitExceeded = errors.New("Size limit exceeded")
type LimitedReaderWithError struct {
limitedReader *io.LimitedReader
}
func NewLimitedReaderWithError(reader io.Reader, maxBytes int64) *LimitedReaderWithError {
return &LimitedReaderWithError{
limitedReader: &io.LimitedReader{R: reader, N: maxBytes + 1},
}
}
func (l *LimitedReaderWithError) Read(p []byte) (int, error) {
n, err := l.limitedReader.Read(p)
if l.limitedReader.N <= 0 && err == io.EOF {
return n, SizeLimitExceeded
}
return n, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package fileutils
import (
"os"
"path/filepath"
)
func CommonBaseSearchPaths() []string {
paths := []string{
".",
"..",
"../..",
"../../..",
"../../../..",
}
// this enables the server to be used in tests from a different repository
if mmPath := os.Getenv("MM_SERVER_PATH"); mmPath != "" {
paths = append(paths, mmPath)
}
return paths
}
func findPath(path string, baseSearchPaths []string, workingDirFirst bool, filter func(os.FileInfo) bool) string {
if filepath.IsAbs(path) {
if _, err := os.Stat(path); err == nil {
return path
}
return ""
}
searchPaths := []string{}
if workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
// Attempt to search relative to the location of the running binary either before
// or after searching relative to the working directory, depending on `workingDirFirst`.
var binaryDir string
if exe, err := os.Executable(); err == nil {
if exe, err = filepath.EvalSymlinks(exe); err == nil {
if exe, err = filepath.Abs(exe); err == nil {
binaryDir = filepath.Dir(exe)
}
}
}
if binaryDir != "" {
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(
searchPaths,
filepath.Join(binaryDir, baseSearchPath),
)
}
}
if !workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
for _, parent := range searchPaths {
found, err := filepath.Abs(filepath.Join(parent, path))
if err != nil {
continue
} else if fileInfo, err := os.Stat(found); err == nil {
if filter != nil {
if filter(fileInfo) {
return found
}
} else {
return found
}
}
}
return ""
}
func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
return findPath(path, baseSearchPaths, true, filter)
}
// FindFile looks for the given file in nearby ancestors relative to the current working
// directory as well as the directory of the executable.
func FindFile(path string) string {
return FindPath(path, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
// fileutils.FindDir looks for the given directory in nearby ancestors relative to the current working
// directory as well as the directory of the executable, falling back to `./` if not found.
func FindDir(dir string) (string, bool) {
found := FindPath(dir, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
// FindDirRelBinary looks for the given directory in nearby ancestors relative to the
// directory of the executable, then relative to the working directory, falling back to `./` if not found.
func FindDirRelBinary(dir string) (string, bool) {
found := findPath(dir, CommonBaseSearchPaths(), false, func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"fmt"
)
func HashSha256(text string) string {
hash := sha256.New()
hash.Write([]byte(text))
return fmt.Sprintf("%x", hash.Sum(nil))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
// this functions loads translations from filesystem if they are not
// loaded already and assigns english while loading server config
func TranslationsPreInit() error {
translationsDir := "i18n"
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
translationsDir = filepath.Join(mattermostPath, "i18n")
}
i18nDirectory, found := fileutils.FindDirRelBinary(translationsDir)
if !found {
return fmt.Errorf("unable to find i18n directory at %q", translationsDir)
}
return i18n.TranslationsPreInit(i18nDirectory)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This is a modified version, the original copyright was: Copyright (c) 2011
// The Go Authors.
package imgutils
// This contains a portion of Go's image/go library, modified to count the number of frames in a gif without loading
// the entire image into memory.
import (
"bufio"
"compress/lzw"
"encoding/binary"
"errors"
"fmt"
"io"
)
var (
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
)
// If the io.Reader does not also have ReadByte, then decode will introduce its own buffering.
type reader interface {
io.Reader
io.ByteReader
}
// Masks etc.
const (
// Fields.
fColorTable = 1 << 7
fColorTableBitsMask = 7
// Graphic control flags.
gcTransparentColorSet = 1 << 0
gcDisposalMethodMask = 7 << 2
)
// Disposal Methods.
const (
DisposalNone = 0x01
DisposalBackground = 0x02
DisposalPrevious = 0x03
)
// Section indicators.
const (
sExtension = 0x21
sImageDescriptor = 0x2C
sTrailer = 0x3B
)
// Extensions.
const (
eText = 0x01 // Plain Text
eGraphicControl = 0xF9 // Graphic Control
eComment = 0xFE // Comment
eApplication = 0xFF // Application
)
func readFull(r io.Reader, b []byte) error {
_, err := io.ReadFull(r, b)
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return err
}
func readByte(r io.ByteReader) (byte, error) {
b, err := r.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return b, err
}
// decoder is the type used to decode a GIF file.
type decoder struct {
r reader
// From header.
vers string
width int
height int
loopCount int
delayTime int
backgroundIndex byte
disposalMethod byte
// From image descriptor.
imageFields byte
// From graphics control.
transparentIndex byte
hasTransparentIndex bool
// Computed.
hasGlobalColorTable bool
// Used when decoding.
imageCount int
tmp [1024]byte // must be at least 768 so we can read color table
}
// blockReader parses the block structure of GIF image data, which comprises
// (n, (n bytes)) blocks, with 1 <= n <= 255. It is the reader given to the
// LZW decoder, which is thus immune to the blocking. After the LZW decoder
// completes, there will be a 0-byte block remaining (0, ()), which is
// consumed when checking that the blockReader is exhausted.
//
// To avoid the allocation of a bufio.Reader for the lzw Reader, blockReader
// implements io.ReadByte and buffers blocks into the decoder's "tmp" buffer.
type blockReader struct {
d *decoder
i, j uint8 // d.tmp[i:j] contains the buffered bytes
err error
}
func (b *blockReader) fill() {
if b.err != nil {
return
}
b.j, b.err = readByte(b.d.r)
if b.j == 0 && b.err == nil {
b.err = io.EOF
}
if b.err != nil {
return
}
b.i = 0
b.err = readFull(b.d.r, b.d.tmp[:b.j])
if b.err != nil {
b.j = 0
}
}
func (b *blockReader) ReadByte() (byte, error) {
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
c := b.d.tmp[b.i]
b.i++
return c, nil
}
// blockReader must implement io.Reader, but its Read shouldn't ever actually
// be called in practice. The compress/lzw package will only call ReadByte.
func (b *blockReader) Read(p []byte) (int, error) {
if len(p) == 0 || b.err != nil {
return 0, b.err
}
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
n := copy(p, b.d.tmp[b.i:b.j])
b.i += uint8(n)
return n, nil
}
// close primarily detects whether or not a block terminator was encountered
// after reading a sequence of data sub-blocks. It allows at most one trailing
// sub-block worth of data. I.e., if some number of bytes exist in one sub-block
// following the end of LZW data, the very next sub-block must be the block
// terminator. If the very end of LZW data happened to fill one sub-block, at
// most one more sub-block of length 1 may exist before the block-terminator.
// These accommodations allow us to support GIFs created by less strict encoders.
// See https://golang.org/issue/16146.
func (b *blockReader) close() error {
if b.err == io.EOF {
// A clean block-sequence terminator was encountered while reading.
return nil
} else if b.err != nil {
// Some other error was encountered while reading.
return b.err
}
if b.i == b.j {
// We reached the end of a sub block reading LZW data. We'll allow at
// most one more sub block of data with a length of 1 byte.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
} else if b.j > 1 {
return errTooMuch
}
}
// Part of a sub-block remains buffered. We expect that the next attempt to
// buffer a sub-block will reach the block terminator.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
}
return errTooMuch
}
// decode reads a GIF image from r and stores the result in d.
func (d *decoder) decode(r io.Reader, configOnly bool) error {
// Add buffering if r does not provide ReadByte.
if rr, ok := r.(reader); ok {
d.r = rr
} else {
d.r = bufio.NewReader(r)
}
d.loopCount = -1
err := d.readHeaderAndScreenDescriptor()
if err != nil {
return err
}
if configOnly {
return nil
}
for {
c, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading frames: %v", err)
}
switch c {
case sExtension:
if err = d.readExtension(); err != nil {
return err
}
case sImageDescriptor:
if err = d.readImageDescriptor(); err != nil {
return err
}
case sTrailer:
if d.imageCount == 0 {
return fmt.Errorf("gif: missing image data")
}
return nil
default:
return fmt.Errorf("gif: unknown block type: 0x%.2x", c)
}
}
}
func (d *decoder) readHeaderAndScreenDescriptor() error {
err := readFull(d.r, d.tmp[:13])
if err != nil {
return fmt.Errorf("gif: reading header: %v", err)
}
d.vers = string(d.tmp[:6])
if d.vers != "GIF87a" && d.vers != "GIF89a" {
return fmt.Errorf("gif: can't recognize format %q", d.vers)
}
d.width = int(d.tmp[6]) + int(d.tmp[7])<<8
d.height = int(d.tmp[8]) + int(d.tmp[9])<<8
if fields := d.tmp[10]; fields&fColorTable != 0 {
d.backgroundIndex = d.tmp[11]
// readColorTable overwrites the contents of d.tmp, but that's OK.
if err = d.readColorTable(fields); err != nil {
return err
}
d.hasGlobalColorTable = true
}
// d.tmp[12] is the Pixel Aspect Ratio, which is ignored.
return nil
}
func (d *decoder) readColorTable(fields byte) error {
n := 1 << (1 + uint(fields&fColorTableBitsMask))
err := readFull(d.r, d.tmp[:3*n])
if err != nil {
return fmt.Errorf("gif: reading color table: %s", err)
}
return nil
}
func (d *decoder) readExtension() error {
extension, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
size := 0
switch extension {
case eText:
size = 13
case eGraphicControl:
return d.readGraphicControl()
case eComment:
// nothing to do but read the data.
case eApplication:
b, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
// The spec requires size be 11, but Adobe sometimes uses 10.
size = int(b)
default:
return fmt.Errorf("gif: unknown extension 0x%.2x", extension)
}
if size > 0 {
if err := readFull(d.r, d.tmp[:size]); err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
}
// Application Extension with "NETSCAPE2.0" as string and 1 in data means
// this extension defines a loop count.
if extension == eApplication && string(d.tmp[:size]) == "NETSCAPE2.0" {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
if n == 3 && d.tmp[0] == 1 {
d.loopCount = int(d.tmp[1]) | int(d.tmp[2])<<8
}
}
for {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
}
}
func (d *decoder) readGraphicControl() error {
if err := readFull(d.r, d.tmp[:6]); err != nil {
return fmt.Errorf("gif: can't read graphic control: %s", err)
}
if d.tmp[0] != 4 {
return fmt.Errorf("gif: invalid graphic control extension block size: %d", d.tmp[0])
}
flags := d.tmp[1]
d.disposalMethod = (flags & gcDisposalMethodMask) >> 2
d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8
if flags&gcTransparentColorSet != 0 {
d.transparentIndex = d.tmp[4]
d.hasTransparentIndex = true
}
if d.tmp[5] != 0 {
return fmt.Errorf("gif: invalid graphic control extension block terminator: %d", d.tmp[5])
}
return nil
}
func (d *decoder) readImageDescriptor() error {
err := d.checkImageFromDescriptor()
if err != nil {
return err
}
useLocalColorTable := d.imageFields&fColorTable != 0
if useLocalColorTable {
if err = d.readColorTable(d.imageFields); err != nil {
return err
}
} else if !d.hasGlobalColorTable {
return errors.New("gif: no color table")
}
litWidth, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
if litWidth < 2 || litWidth > 8 {
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
}
// A wonderfully Go-like piece of magic.
br := &blockReader{d: d}
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
defer lzwr.Close()
if _, err := io.Copy(io.Discard, lzwr); err != nil {
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errNotEnough
}
// In theory, both lzwr and br should be exhausted. Reading from them
// should yield (0, io.EOF).
//
// The spec (Appendix F - Compression), says that "An End of
// Information code... must be the last code output by the encoder
// for an image". In practice, though, giflib (a widely used C
// library) does not enforce this, so we also accept lzwr returning
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
// before the LZW decoder saw an explicit end code), provided that
// the io.ReadFull call above successfully read len(m.Pix) bytes.
// See https://golang.org/issue/9856 for an example GIF.
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errTooMuch
}
// In practice, some GIFs have an extra byte in the data sub-block
// stream, which we ignore. See https://golang.org/issue/16146.
if err := br.close(); err == errTooMuch {
return errTooMuch
} else if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
d.imageCount += 1
return nil
}
func (d *decoder) checkImageFromDescriptor() error {
if err := readFull(d.r, d.tmp[:9]); err != nil {
return fmt.Errorf("gif: can't read image descriptor: %s", err)
}
left := int(d.tmp[0]) + int(d.tmp[1])<<8
top := int(d.tmp[2]) + int(d.tmp[3])<<8
width := int(d.tmp[4]) + int(d.tmp[5])<<8
height := int(d.tmp[6]) + int(d.tmp[7])<<8
d.imageFields = d.tmp[8]
// The GIF89a spec, Section 20 (Image Descriptor) says: "Each image must
// fit within the boundaries of the Logical Screen, as defined in the
// Logical Screen Descriptor."
//
// This is conceptually similar to testing
// frameBounds := image.Rect(left, top, left+width, top+height)
// imageBounds := image.Rect(0, 0, d.width, d.height)
// if !frameBounds.In(imageBounds) { etc }
// but the semantics of the Go image.Rectangle type is that r.In(s) is true
// whenever r is an empty rectangle, even if r.Min.X > s.Max.X. Here, we
// want something stricter.
//
// Note that, by construction, left >= 0 && top >= 0, so we only have to
// explicitly compare frameBounds.Max (left+width, top+height) against
// imageBounds.Max (d.width, d.height) and not frameBounds.Min (left, top)
// against imageBounds.Min (0, 0).
if left+width > d.width || top+height > d.height {
return errors.New("gif: frame bounds larger than image bounds")
}
return nil
}
func (d *decoder) readBlock() (int, error) {
n, err := readByte(d.r)
if n == 0 || err != nil {
return 0, err
}
if err := readFull(d.r, d.tmp[:n]); err != nil {
return 0, err
}
return int(n), nil
}
func CountGIFFrames(r io.Reader) (int, error) {
var d decoder
if err := d.decode(r, false); err != nil {
return -1, err
}
return d.imageCount, nil
}
func GenGIFData(width, height uint16, nFrames int) []byte {
header := []byte{
'G', 'I', 'F', '8', '9', 'a', // header
0, 0, 0, 0, // width and height
128, 0, 0, // other header information
0, 0, 0, 1, 1, 1, // color table
}
binary.LittleEndian.PutUint16(header[6:], width)
binary.LittleEndian.PutUint16(header[8:], height)
frame := []byte{
0x2c, // block introducer
0, 0, 0, 0, 1, 0, 1, 0, // position and dimensions of the frame
0, // other frame information
0x2, 0x2, 0x4c, 0x1, 0, // encoded pixel data
}
trailer := []byte{0x3b}
gifData := header
for i := 0; i < nFrames; i++ {
gifData = append(gifData, frame...)
}
gifData = append(gifData, trailer...)
return gifData
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jsonutils
import (
"bytes"
"encoding/json"
"github.com/pkg/errors"
)
type HumanizedJSONError struct {
Err error
Line int
Character int
}
func (e *HumanizedJSONError) Error() string {
return e.Err.Error()
}
// HumanizeJSONError extracts error offsets and annotates the error with useful context
func HumanizeJSONError(err error, data []byte) error {
if syntaxError, ok := err.(*json.SyntaxError); ok {
return NewHumanizedJSONError(syntaxError, data, syntaxError.Offset)
} else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok {
return NewHumanizedJSONError(unmarshalError, data, unmarshalError.Offset)
} else {
return err
}
}
func NewHumanizedJSONError(err error, data []byte, offset int64) *HumanizedJSONError {
if err == nil {
return nil
}
if offset < 0 || offset > int64(len(data)) {
return &HumanizedJSONError{
Err: errors.Wrapf(err, "invalid offset %d", offset),
}
}
lineSep := []byte{'\n'}
line := bytes.Count(data[:offset], lineSep) + 1
lastLineOffset := bytes.LastIndex(data[:offset], lineSep)
character := int(offset) - (lastLineOffset + 1) + 1
return &HumanizedJSONError{
Line: line,
Character: character,
Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var LicenseValidator LicenseValidatorIface
func init() {
if LicenseValidator == nil {
LicenseValidator = &LicenseValidatorImpl{}
}
}
type LicenseValidatorIface interface {
LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError)
ValidateLicense(signed []byte) (bool, string)
}
type LicenseValidatorImpl struct {
}
func (l *LicenseValidatorImpl) LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError) {
success, licenseStr := l.ValidateLicense(licenseBytes)
if !success {
return nil, model.NewAppError("LicenseFromBytes", model.InvalidLicenseError, nil, "", http.StatusBadRequest)
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, model.NewAppError("LicenseFromBytes", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &license, nil
}
func (l *LicenseValidatorImpl) ValidateLicense(signed []byte) (bool, string) {
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed)))
_, err := base64.StdEncoding.Decode(decoded, signed)
if err != nil {
mlog.Error("Encountered error decoding license", mlog.Err(err))
return false, ""
}
// remove null terminator
for len(decoded) > 0 && decoded[len(decoded)-1] == byte(0) {
decoded = decoded[:len(decoded)-1]
}
if len(decoded) <= 256 {
mlog.Error("Signed license not long enough")
return false, ""
}
plaintext := decoded[:len(decoded)-256]
signature := decoded[len(decoded)-256:]
block, _ := pem.Decode(publicKey)
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
mlog.Error("Encountered error signing license", mlog.Err(err))
return false, ""
}
rsaPublic := public.(*rsa.PublicKey)
h := sha512.New()
h.Write(plaintext)
d := h.Sum(nil)
err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
if err != nil {
mlog.Error("Invalid signature", mlog.Err(err))
return false, ""
}
return true, string(plaintext)
}
func GetAndValidateLicenseFileFromDisk(location string) (*model.License, []byte) {
fileName := GetLicenseFileLocation(location)
if _, err := os.Stat(fileName); err != nil {
mlog.Debug("We could not find the license key in the database or on disk at", mlog.String("filename", fileName))
return nil, nil
}
mlog.Info("License key has not been uploaded. Loading license key from disk at", mlog.String("filename", fileName))
licenseBytes := GetLicenseFileFromDisk(fileName)
success, licenseStr := LicenseValidator.ValidateLicense(licenseBytes)
if !success {
mlog.Error("Found license key at %v but it appears to be invalid.", mlog.String("filename", fileName))
return nil, nil
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
mlog.Error("Failed to decode license from JSON", mlog.Err(jsonErr))
return nil, nil
}
return &license, licenseBytes
}
func GetLicenseFileFromDisk(fileName string) []byte {
file, err := os.Open(fileName)
if err != nil {
mlog.Error("Failed to open license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
defer file.Close()
licenseBytes, err := io.ReadAll(file)
if err != nil {
mlog.Error("Failed to read license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
return licenseBytes
}
func GetLicenseFileLocation(fileLocation string) string {
if fileLocation == "" {
configDir, _ := fileutils.FindDir("config")
return filepath.Join(configDir, "mattermost.mattermost-license")
}
return fileLocation
}
func GetClientLicense(l *model.License) map[string]string {
props := make(map[string]string)
props["IsLicensed"] = strconv.FormatBool(l != nil)
if l != nil {
props["Id"] = l.Id
props["SkuName"] = l.SkuName
props["SkuShortName"] = l.SkuShortName
props["Users"] = strconv.Itoa(*l.Features.Users)
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
props["LDAPGroups"] = strconv.FormatBool(*l.Features.LDAPGroups)
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
props["SAML"] = strconv.FormatBool(*l.Features.SAML)
props["Cluster"] = strconv.FormatBool(*l.Features.Cluster)
props["Metrics"] = strconv.FormatBool(*l.Features.Metrics)
props["GoogleOAuth"] = strconv.FormatBool(*l.Features.GoogleOAuth)
props["Office365OAuth"] = strconv.FormatBool(*l.Features.Office365OAuth)
props["OpenId"] = strconv.FormatBool(*l.Features.OpenId)
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS)
props["Announcement"] = strconv.FormatBool(*l.Features.Announcement)
props["Elasticsearch"] = strconv.FormatBool(*l.Features.Elasticsearch)
props["DataRetention"] = strconv.FormatBool(*l.Features.DataRetention)
props["IDLoadedPushNotifications"] = strconv.FormatBool(*l.Features.IDLoadedPushNotifications)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
props["Name"] = l.Customer.Name
props["Email"] = l.Customer.Email
props["Company"] = l.Customer.Company
props["EmailNotificationContents"] = strconv.FormatBool(*l.Features.EmailNotificationContents)
props["MessageExport"] = strconv.FormatBool(*l.Features.MessageExport)
props["CustomPermissionsSchemes"] = strconv.FormatBool(*l.Features.CustomPermissionsSchemes)
props["GuestAccounts"] = strconv.FormatBool(*l.Features.GuestAccounts)
props["GuestAccountsPermissions"] = strconv.FormatBool(*l.Features.GuestAccountsPermissions)
props["CustomTermsOfService"] = strconv.FormatBool(*l.Features.CustomTermsOfService)
props["LockTeammateNameDisplay"] = strconv.FormatBool(*l.Features.LockTeammateNameDisplay)
props["Cloud"] = strconv.FormatBool(*l.Features.Cloud)
props["SharedChannels"] = strconv.FormatBool(*l.Features.SharedChannels)
props["RemoteClusterService"] = strconv.FormatBool(*l.Features.RemoteClusterService)
props["IsTrial"] = strconv.FormatBool(l.IsTrial)
props["IsGovSku"] = strconv.FormatBool(l.IsGovSku)
}
return props
}
func GetSanitizedClientLicense(l map[string]string) map[string]string {
sanitizedLicense := make(map[string]string)
for k, v := range l {
sanitizedLicense[k] = v
}
delete(sanitizedLicense, "Id")
delete(sanitizedLicense, "Name")
delete(sanitizedLicense, "Email")
delete(sanitizedLicense, "IssuedAt")
delete(sanitizedLicense, "StartsAt")
delete(sanitizedLicense, "ExpiresAt")
delete(sanitizedLicense, "SkuName")
delete(sanitizedLicense, "SkuShortName")
return sanitizedLicense
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"html"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
astExt "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// StripMarkdown remove some markdown syntax
func StripMarkdown(markdown string) (string, error) {
md := goldmark.New(
goldmark.WithExtensions(extension.Strikethrough),
goldmark.WithRenderer(
renderer.NewRenderer(renderer.WithNodeRenderers(
util.Prioritized(newNotificationRenderer(), 500),
)),
),
)
var buf strings.Builder
if err := md.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
}
var relLinkReg = regexp.MustCompile(`\[(.*)]\((/.*)\)`)
var blockquoteReg = regexp.MustCompile(`^|\n(>)`)
// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version
func MarkdownToHTML(markdown, siteURL string) (string, error) {
// Turn relative links into absolute links
absLinkMarkdown := relLinkReg.ReplaceAllStringFunc(markdown, func(s string) string {
return relLinkReg.ReplaceAllString(s, "[$1]("+siteURL+"$2)")
})
// Unescape any blockquote text to be parsed by the markdown parser.
markdownClean := blockquoteReg.ReplaceAllStringFunc(absLinkMarkdown, func(s string) string {
return html.UnescapeString(s)
})
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
)
var b strings.Builder
err := md.Convert([]byte(markdownClean), &b)
if err != nil {
return "", err
}
return b.String(), nil
}
type notificationRenderer struct {
}
func newNotificationRenderer() *notificationRenderer {
return ¬ificationRenderer{}
}
func (r *notificationRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// block
reg.Register(ast.KindDocument, r.renderDefault)
reg.Register(ast.KindHeading, r.renderItem)
reg.Register(ast.KindBlockquote, r.renderDefault)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
reg.Register(ast.KindHTMLBlock, r.renderDefault)
reg.Register(ast.KindList, r.renderDefault)
reg.Register(ast.KindListItem, r.renderItem)
reg.Register(ast.KindParagraph, r.renderItem)
reg.Register(ast.KindTextBlock, r.renderTextBlock)
reg.Register(ast.KindThematicBreak, r.renderDefault)
// inlines
reg.Register(ast.KindAutoLink, r.renderDefault)
reg.Register(ast.KindCodeSpan, r.renderDefault)
reg.Register(ast.KindEmphasis, r.renderDefault)
reg.Register(ast.KindImage, r.renderDefault)
reg.Register(ast.KindLink, r.renderDefault)
reg.Register(ast.KindRawHTML, r.renderDefault)
reg.Register(ast.KindText, r.renderText)
reg.Register(ast.KindString, r.renderString)
// strikethrough
reg.Register(astExt.KindStrikethrough, r.renderDefault)
}
// renderDefault renderer function to renderDefault without changes
func (r *notificationRenderer) renderDefault(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderItem(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.CodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.FencedCodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Text)
segment := n.Segment
_, _ = w.Write(segment.Value(source))
if !n.IsRaw() {
if n.HardLineBreak() || n.SoftLineBreak() {
_ = w.WriteByte('\n')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderTextBlock(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil && node.FirstChild() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderString(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.String)
_, _ = w.Write(n.Value)
return ast.WalkContinue, nil
}
func (r *notificationRenderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
for i := 0; i < n.Lines().Len(); i++ {
line := n.Lines().At(i)
value := line.Value(source)
_, _ = w.Write(value)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"reflect"
)
// StructFieldFilter defines a callback function used to decide if a patch value should be applied.
type StructFieldFilter func(structField reflect.StructField, base reflect.Value, patch reflect.Value) bool
// MergeConfig allows for optional merge customizations.
type MergeConfig struct {
StructFieldFilter StructFieldFilter
}
// Merge will return a new value of the same type as base and patch, recursively merging non-nil values from patch on top of base.
//
// Restrictions/guarantees:
// - base and patch must be the same type
// - base and patch will never be modified
// - values from patch are always selected when non-nil
// - structs are merged recursively
// - maps and slices are treated as pointers, and merged as a single value
//
// Note that callers need to cast the returned interface back into the original type:
//
// func mergeTestStruct(base, patch *testStruct) (*testStruct, error) {
// ret, err := merge(base, patch)
// if err != nil {
// return nil, err
// }
//
// retTS := ret.(testStruct)
// return &retTS, nil
// }
func Merge(base any, patch any, mergeConfig *MergeConfig) (any, error) {
if reflect.TypeOf(base) != reflect.TypeOf(patch) {
return nil, fmt.Errorf(
"cannot merge different types. base type: %s, patch type: %s",
reflect.TypeOf(base),
reflect.TypeOf(patch),
)
}
commonType := reflect.TypeOf(base)
baseVal := reflect.ValueOf(base)
patchVal := reflect.ValueOf(patch)
if commonType.Kind() == reflect.Ptr {
commonType = commonType.Elem()
baseVal = baseVal.Elem()
patchVal = patchVal.Elem()
}
ret := reflect.New(commonType)
val, ok := merge(baseVal, patchVal, mergeConfig)
if ok {
ret.Elem().Set(val)
}
return ret.Elem().Interface(), nil
}
// merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value
func merge(base, patch reflect.Value, mergeConfig *MergeConfig) (reflect.Value, bool) {
commonType := base.Type()
switch commonType.Kind() {
case reflect.Struct:
merged := reflect.New(commonType).Elem()
for i := 0; i < base.NumField(); i++ {
if !merged.Field(i).CanSet() {
continue
}
if mergeConfig != nil && mergeConfig.StructFieldFilter != nil {
if !mergeConfig.StructFieldFilter(commonType.Field(i), base.Field(i), patch.Field(i)) {
merged.Field(i).Set(base.Field(i))
continue
}
}
val, ok := merge(base.Field(i), patch.Field(i), mergeConfig)
if ok {
merged.Field(i).Set(val)
}
}
return merged, true
case reflect.Ptr:
mergedPtr := reflect.New(commonType.Elem())
if base.IsNil() && patch.IsNil() {
return mergedPtr, false
}
// clone reference values (if any)
if base.IsNil() {
val, _ := merge(patch.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else if patch.IsNil() {
val, _ := merge(base.Elem(), base.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else {
val, _ := merge(base.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
}
return mergedPtr, true
case reflect.Slice:
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
if !patch.IsNil() {
// use patch
merged := reflect.MakeSlice(commonType, 0, patch.Len())
for i := 0; i < patch.Len(); i++ {
// recursively merge patch with itself. This will clone reference values.
val, _ := merge(patch.Index(i), patch.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
}
// use base
merged := reflect.MakeSlice(commonType, 0, base.Len())
for i := 0; i < base.Len(); i++ {
// recursively merge base with itself. This will clone reference values.
val, _ := merge(base.Index(i), base.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
case reflect.Map:
// maps are merged according to these rules:
// - if patch is not nil, replace the base map completely
// - otherwise, keep the base map
// - reference values (eg. slice/ptr/map) will be cloned
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
merged := reflect.MakeMap(commonType)
mapPtr := base
if !patch.IsNil() {
mapPtr = patch
}
for _, key := range mapPtr.MapKeys() {
// clone reference values
val, ok := merge(mapPtr.MapIndex(key), mapPtr.MapIndex(key), mergeConfig)
if !ok {
val = reflect.New(mapPtr.MapIndex(key).Type()).Elem()
}
merged.SetMapIndex(key, val)
}
return merged, true
case reflect.Interface:
var val reflect.Value
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
// clone reference values (if any)
if base.IsNil() {
val, _ = merge(patch.Elem(), patch.Elem(), mergeConfig)
} else if patch.IsNil() {
val, _ = merge(base.Elem(), base.Elem(), mergeConfig)
} else {
val, _ = merge(base.Elem(), patch.Elem(), mergeConfig)
}
return val, true
default:
return patch, true
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
)
type Range struct {
Begin int
End int
}
func RandIntFromRange(r Range) int {
if r.End-r.Begin <= 0 {
return r.Begin
}
return rand.Intn((r.End-r.Begin)+1) + r.Begin
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
func getSubpathScript(subpath string) string {
if subpath == "" {
subpath = "/"
}
newPath := path.Join(subpath, "static") + "/"
return fmt.Sprintf("window.publicPath='%s'", newPath)
}
// GetSubpathScriptHash computes the script-src addition required for the subpath script to bypass CSP protections.
func GetSubpathScriptHash(subpath string) string {
// No hash is required for the default subpath.
if subpath == "" || subpath == "/" {
return ""
}
scriptHash := sha256.Sum256([]byte(getSubpathScript(subpath)))
return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
}
// UpdateAssetsSubpathInDir rewrites assets in the given directory to assume the application is
// hosted at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpathInDir(subpath, directory string) error {
if subpath == "" {
subpath = "/"
}
staticDir, found := fileutils.FindDir(directory)
if !found {
return errors.New("failed to find client dir")
}
staticDir, err := filepath.EvalSymlinks(staticDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
}
rootHTMLPath := filepath.Join(staticDir, "root.html")
oldRootHTML, err := os.ReadFile(rootHTMLPath)
if err != nil {
return errors.Wrap(err, "failed to open root.html")
}
oldSubpath := "/"
// Determine if a previous subpath had already been rewritten into the assets.
reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+/)static/'")
alreadyRewritten := false
if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHTML)); matches != nil {
oldSubpath = matches[1]
alreadyRewritten = true
}
pathToReplace := path.Join(oldSubpath, "static") + "/"
newPath := path.Join(subpath, "static") + "/"
mlog.Debug("Rewriting static assets", mlog.String("from_subpath", oldSubpath), mlog.String("to_subpath", subpath))
newRootHTML := string(oldRootHTML)
reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3([^"]*)">`)
if results := reCSP.FindAllString(newRootHTML, -1); len(results) == 0 {
return fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")
}
newRootHTML = reCSP.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf(
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3%s">`,
GetSubpathScriptHash(subpath),
))
// Rewrite the root.html references to `/static/*` to include the given subpath.
// This potentially includes a previously injected inline script that needs to
// be updated (and isn't covered by the cases above).
newRootHTML = strings.Replace(newRootHTML, pathToReplace, newPath, -1)
if alreadyRewritten && subpath == "/" {
// Remove the injected script since no longer required. Note that the rewrite above
// will have affected the script, so look for the new subpath, not the old one.
oldScript := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, fmt.Sprintf("</style><script>%s</script>", oldScript), "</style>", 1)
} else if !alreadyRewritten && subpath != "/" {
// Otherwise, inject the script to define `window.publicPath`.
script := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
}
// Write out the updated root.html.
if err = os.WriteFile(rootHTMLPath, []byte(newRootHTML), 0); err != nil {
return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
}
// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
old, err := os.ReadFile(walkPath)
if err != nil {
return errors.Wrapf(err, "failed to open %s", walkPath)
}
new := strings.Replace(string(old), pathToReplace, newPath, -1)
if err = os.WriteFile(walkPath, []byte(new), 0); err != nil {
return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
}
}
return nil
})
if err != nil {
return errors.Wrapf(err, "error walking %s", staticDir)
}
return nil
}
// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
// at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpath(subpath string) error {
return UpdateAssetsSubpathInDir(subpath, model.ClientDir)
}
// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
func UpdateAssetsSubpathFromConfig(config *model.Config) error {
// Don't rewrite in development environments, since webpack in developer mode constantly
// updates the assets and must be configured separately.
if model.BuildNumber == "dev" {
mlog.Debug("Skipping update to assets subpath since dev build")
return nil
}
// Similarly, don't rewrite during a CI build, when the assets may not even be present.
if os.Getenv("IS_CI") == "true" {
mlog.Debug("Skipping update to assets subpath since CI build")
return nil
}
subpath, err := GetSubpathFromConfig(config)
if err != nil {
return err
}
return UpdateAssetsSubpath(subpath)
}
func GetSubpathFromConfig(config *model.Config) (string, error) {
if config == nil {
return "", errors.New("no config provided")
} else if config.ServiceSettings.SiteURL == nil {
return "/", nil
}
u, err := url.Parse(*config.ServiceSettings.SiteURL)
if err != nil {
return "", errors.Wrap(err, "failed to parse SiteURL from config")
}
if u.Path == "" {
return "/", nil
}
return path.Clean(u.Path), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
func CompileGo(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "build", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}
func CompileGoTest(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main_test.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "test", "-c", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
"strings"
)
const (
ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
)
// Strings that should pass as acceptable posts
var FuzzyStringsPosts = []string{
`**[1] - [Markdown Tests]**
_italics_
more _italics_
**bold**
more **bold**
**_bold-italic_**
more **_bold-italic_*8
~~strikethrough~~
more ~~strikethrough~~
` + "```" + `
multi-line code block<enter here>
multi-line code block
emoji that should not render in code block: :ice_cream:
` + "```" + `
` + "`monospace`" + `
[Link to Mattermost](www.mattermost.com)
Inline Image with link, alt text, and hover text: ](https://travis-ci.org/mattermost/mattermost-server)
Three types of lines:
***
___
---
`,
` **[2] - **[More Markdown Tests]**
> i am a blockquote!
> i am a 2nd multiline
> quote.
i am text right after a multiline quote, but not in the quote
* list item
* another list item
* indented list item
1. numbered list, item number 1
2. item number two
`,
` **[3]** - **[More Markdown Tests]**
Table
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Ugly table
Markdown | Less | Pretty
--- | --- | ---
*Still* | ~~renders~~ | **nicely**
1 | 2 | 3
# Large heading
## Smaller heading
### Even smaller heading
# Large heading
## Smaller heading
### Even smaller heading
`,
` **[4]** - **[More Markdown Tests]**
# This is a heading
I am a multiline
text.
#### I am a level four heading
` + "```tex" + `
f(x) = \int_{-\infty}^\infty
\hat f(\xi)\,e^{2 \pi i \xi x}
\,d\xi
` + "```" + `
* This was some tex code*
`,
`**[5]** - **[Markdown and automatic preview of content test]**
## This should display a preview for the given vine url
Some text *before* the link
And a smiley :)
https://vine.co/v/eDeVgbFrt9L
Some more text here
and here
and even more here
`,
`**[6]** - **[More markdown and automatic preview of content test]**
## Only the first given url should render an "attachment"
Lets also add a table here, because why not
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Wiki should render:
http://en.wikipedia.org/wiki/Foo
https://vine.co/v/eDeVgbFrt9L
`,
`**[7] [Image Test]**
## this *should* display an image
http://37.media.tumblr.com/tumblr_mavsumGGAd1qboaw8o1_500.jpg
`,
/* `**[2] [Username Linking Test]**
I saw @alice--and I said "Hi @alice!" then "What's up @alice?" and then @alice, was totally @alice; she just "@alice"'d me and walked on by. That's @alice...
@alice‽‽
`,
`**[3] [Mention Highlighting Test]**
`,*/
`**[8] [Emoji Display Test 1]**
:+1: :-1: :100: :1234: :8ball: :a: :ab: :abc: :abcd: :accept:
:aerial_tramway: :airplane: :alarm_clock: :ambulance: :anchor: :angel: :anger: :angry: :anguished: :ant:
:apple: :aquarius: :aries: :arrow_backward: :arrow_double_down: :arrow_double_up: :arrow_down: :arrow_down_small: :arrow_forward: :arrow_heading_down:
:arrow_heading_up: :arrow_left: :arrow_lower_left: :arrow_lower_right: :arrow_right: :arrow_right_hook: :arrow_up: :arrow_up_down:
:arrow_upper_left: :arrow_upper_right: :arrows_clockwise: :arrows_counterclockwise: :art: :articulated_lorry: :astonished: :atm: :arrow_up_small: :b:
:baby: :baby_bottle: :baby_chick: :baby_symbol: :back: :baggage_claim: :balloon: :ballot_box_with_check: :bamboo: :banana:
:bangbang: :bank: :bar_chart: :barber: :baseball: :basketball: :bath: :bathtub: :battery: :bear:
:bee: :beer: :beers: :beetle: :beginner: :bell: :bento: :bicyclist: :bike: :bikini:
:bird: :birthday: :black_circle: :black_joker: :black_medium_small_square: :black_medium_square: :black_nib: :black_small_square: :black_square: :black_square_button:
:blossom: :blowfish: :blue_book: :blue_car: :blue_heart: :blush: :boar: :boat: :bomb: :book:
:bookmark: :bookmark_tabs: :books: :boom: :boot: :bouquet: :bow: :bowling: :bowtie: :boy:
:bread: :bride_with_veil: :bridge_at_night: :briefcase: :broken_heart: :bug: :bulb: :bullettrain_front: :bullettrain_side: :bus:
:busstop: :bust_in_silhouette: :busts_in_silhouette: :cactus: :cake: :calendar: :calling: :camel: :camera: :cancer:
:candy: :capital_abcd: :capricorn: :car: :card_index: :carousel_horse: :cat: :cat2: :cd: :chart:
:chart_with_downwards_trend: :chart_with_upwards_trend: :checkered_flag: :cherries: :cherry_blossom: :chestnut: :chicken: :children_crossing: :chocolate_bar: :christmas_tree:
:church: :cinema: :circus_tent: :city_sunrise: :city_sunset: :cl: :clap: :clapper: :clipboard: :clock1:
:clock10: :clock1030: :clock11: :clock1130: :clock12: :clock1230: :clock130: :clock2: :clock230: :clock3:
:clock330: :clock4: :clock430: :clock5: :clock530: :clock6: :clock630: :clock7: :clock730: :clock8:
:clock830: :clock9: :clock930: :closed_book: :closed_lock_with_key: :closed_umbrella: :cloud: :clubs: :cn: :cocktail:
:coffee: :cold_sweat: :collision: :computer: :confetti_ball: :confounded: :confused: :congratulations: :construction: :construction_worker:
:convenience_store: :cookie: :cool: :cop: :copyright: :corn: :couple: :couple_with_heart: :couplekiss: :cow:
:cow2: :credit_card: :crescent_moon: :crocodile: :crossed_flags: :crown: :cry: :crying_cat_face: :crystal_ball: :cupid:
:curly_loop: :currency_exchange: :curry: :custard: :customs: :cyclone: :dancer: :dancers: :dango: :dart:
:dash: :date: :de: :deciduous_tree: :department_store: :diamond_shape_with_a_dot_inside: :diamonds: :disappointed: :disappointed_relieved: :dizzy:
:dizzy_face: :do_not_litter: :dog: :dog2: :dollar: :dolls: :dolphin: :donut: :door: :doughnut:
:dragon: :dragon_face: :dress: :dromedary_camel: :droplet: :dvd: :e-mail: :ear: :ear_of_rice: :earth_africa:
:earth_americas: :earth_asia: :egg: :eggplant: :eight: :eight_pointed_black_star: :eight_spoked_asterisk: :electric_plug: :elephant: :email:
:end: :envelope: :es: :euro: :european_castle: :european_post_office: :evergreen_tree: :exclamation: :expressionless: :eyeglasses:
:eyes: :facepunch: :factory: :fallen_leaf: :family: :fast_forward: :fax: :fearful: :feelsgood: :feet:
:ferris_wheel: :file_folder: :finnadie: :fire: :fire_engine: :fireworks: :first_quarter_moon: :first_quarter_moon_with_face: :fish: :fish_cake:
:fishing_pole_and_fish: :fist: :five: :flags: :flashlight: :floppy_disk: :flower_playing_cards: :flushed: :foggy: :football:
:fork_and_knife: :fountain: :four: :four_leaf_clover: :fr: :free: :fried_shrimp: :fries: :frog: :frowning:
:fu: :fuelpump: :full_moon: :full_moon_with_face: :game_die: :gb: :gem: :gemini: :ghost: :gift:`,
`**[9] [Emoji Display Test 2]**
:gift_heart: :girl: :globe_with_meridians: :goat: :goberserk: :godmode: :golf: :grapes: :green_apple: :green_book:
:green_heart: :grey_exclamation: :grey_question: :grimacing: :grin: :grinning: :guardsman: :guitar: :gun: :haircut:
:hamburger: :hammer: :hamster: :hand: :handbag: :hankey: :hash: :hatched_chick: :hatching_chick: :headphones:
:hear_no_evil: :heart: :heart_decoration: :heart_eyes: :heart_eyes_cat: :heartbeat: :heartpulse: :hearts: :heavy_check_mark: :heavy_division_sign:
:heavy_dollar_sign: :heavy_exclamation_mark: :heavy_minus_sign: :heavy_multiplication_x: :heavy_plus_sign: :helicopter: :herb: :hibiscus: :high_brightness: :high_heel:
:hocho: :honey_pot: :honeybee: :horse: :horse_racing: :hospital: :hotel: :hotsprings: :hourglass: :hourglass_flowing_sand:
:house: :house_with_garden: :hurtrealbad: :hushed: :ice_cream: :icecream: :id: :ideograph_advantage: :imp: :inbox_tray:
:incoming_envelope: :information_desk_person: :information_source: :innocent: :interrobang: :iphone: :it: :izakaya_lantern: :jack_o_lantern:
:japan: :japanese_castle: :japanese_goblin: :japanese_ogre: :jeans: :joy: :joy_cat: :jp: :key: :keycap_ten:
:kimono: :kiss: :kissing: :kissing_cat: :kissing_closed_eyes: :kissing_face: :kissing_heart: :kissing_smiling_eyes: :koala: :koko:
:kr: :large_blue_circle: :large_blue_diamond: :large_orange_diamond: :last_quarter_moon: :last_quarter_moon_with_face: :laughing: :leaves: :ledger: :left_luggage:
:left_right_arrow: :leftwards_arrow_with_hook: :lemon: :leo: :leopard: :libra: :light_rail: :link: :lips: :lipstick:
:lock: :lock_with_ink_pen: :lollipop: :loop: :loudspeaker: :love_hotel: :love_letter: :low_brightness: :m: :mag:
:mag_right: :mahjong: :mailbox: :mailbox_closed: :mailbox_with_mail: :mailbox_with_no_mail: :man: :man_with_gua_pi_mao: :man_with_turban: :mans_shoe:
:maple_leaf: :mask: :massage: :meat_on_bone: :mega: :melon: :memo: :mens: :metal: :metro:
:microphone: :microscope: :milky_way: :minibus: :minidisc: :mobile_phone_off: :money_with_wings: :moneybag: :monkey: :monkey_face:
:monorail: :mortar_board: :mount_fuji: :mountain_bicyclist: :mountain_cableway: :mountain_railway: :mouse: :mouse2: :movie_camera: :moyai:
:muscle: :mushroom: :musical_keyboard: :musical_note: :musical_score: :mute: :nail_care: :name_badge: :neckbeard: :necktie:
:negative_squared_cross_mark: :neutral_face: :new: :new_moon: :new_moon_with_face: :newspaper: :ng: :nine: :no_bell:
:no_bicycles: :no_entry: :no_entry_sign: :no_good: :no_mobile_phones: :no_mouth: :no_pedestrians: :no_smoking: :non-potable_water: :nose:
:notebook: :notebook_with_decorative_cover: :notes: :nut_and_bolt: :o: :o2: :ocean: :octocat: :octopus: :oden:
:office: :ok: :ok_hand: :ok_woman: :older_man: :older_woman: :on: :oncoming_automobile: :oncoming_bus: :oncoming_police_car:
:oncoming_taxi: :one: :open_file_folder: :open_hands: :open_mouth: :ophiuchus: :orange_book: :outbox_tray: :ox: :package:
:page_facing_up: :page_with_curl: :pager: :palm_tree: :panda_face: :paperclip: :parking: :part_alternation_mark: :partly_sunny: :passport_control:
:paw_prints: :peach: :pear: :pencil: :pencil2: :penguin: :pensive: :performing_arts: :persevere: :person_frowning:
:person_with_blond_hair: :person_with_pouting_face: :phone: :pig: :pig2: :pig_nose: :pill: :pineapple: :pisces: :pizza:
`,
`**[10] [Emoji Display Test 3]**
:plus1: :point_down: :point_left: :point_right: :point_up: :point_up_2: :police_car: :poodle: :poop: :post_office:
:postal_horn: :postbox: :potable_water: :pouch: :poultry_leg: :pound: :pouting_cat: :pray: :princess: :punch:
:purple_heart: :purse: :pushpin: :put_litter_in_its_place: :question: :rabbit: :rabbit2: :racehorse: :radio: :radio_button:
:rage: :rage1: :rage2: :rage3: :rage4: :railway_car: :rainbow: :raised_hand: :raised_hands: :raising_hand:
:ram: :ramen: :rat: :recycle: :red_car: :red_circle: :registered: :relaxed: :relieved: :repeat:
:repeat_one: :restroom: :revolving_hearts: :rewind: :ribbon: :rice: :rice_ball: :rice_cracker: :rice_scene: :ring:
:rocket: :roller_coaster: :rooster: :rose: :rotating_light: :round_pushpin: :rowboat: :ru:
:rugby_football: :runner: :running: :running_shirt_with_sash: :sa: :sagittarius: :sailboat: :sake: :sandal: :santa:
:satellite: :satisfied: :saxophone: :school: :school_satchel: :scissors: :scorpius: :scream: :scream_cat: :scroll:
:seat: :secret: :see_no_evil: :seedling: :seven: :shaved_ice: :sheep: :shell: :ship: :shipit:
:shirt: :shit: :shoe: :shower: :signal_strength: :six: :six_pointed_star: :ski: :skull: :sleeping:
:sleepy: :slot_machine: :small_blue_diamond: :small_orange_diamond: :small_red_triangle: :small_red_triangle_down: :smile: :smile_cat: :smiley: :smiley_cat:
:smiling_imp: :smirk: :smirk_cat: :smoking: :snail: :snake: :snowboarder: :snowflake: :snowman: :sob:
:soccer: :soon: :sos: :sound: :space_invader: :spades: :spaghetti: :sparkle: :sparkler: :sparkles:
:sparkling_heart: :speak_no_evil: :speaker: :speech_balloon: :speedboat: :squirrel: :star: :star2: :stars: :station:
:statue_of_liberty: :steam_locomotive: :stew: :straight_ruler: :strawberry: :stuck_out_tongue: :stuck_out_tongue_closed_eyes: :stuck_out_tongue_winking_eye: :sun_with_face: :sunflower:
:sunglasses: :sunny: :sunrise: :sunrise_over_mountains: :surfer: :sushi: :suspect: :suspension_railway: :sweat: :sweat_drops:
:sweat_smile: :sweet_potato: :swimmer: :symbols: :syringe: :tada: :tanabata_tree: :tangerine: :taurus: :taxi:
:tea: :telephone: :telephone_receiver: :telescope: :tennis: :tent: :thought_balloon: :three: :thumbsdown: :thumbsup:
:ticket: :tiger: :tiger2: :tired_face: :tm: :toilet: :tokyo_tower: :tomato: :tongue: :top:
:tophat: :tractor: :traffic_light: :train: :train2: :tram: :triangular_flag_on_post: :triangular_ruler: :trident: :triumph:
:trolleybus: :trollface: :trophy: :tropical_drink: :tropical_fish: :truck: :trumpet: :tshirt: :tulip: :turtle:
:tv: :twisted_rightwards_arrows: :two: :two_hearts: :two_men_holding_hands: :two_women_holding_hands:
:uk: :umbrella: :unamused: :underage: :unlock: :up: :us: :v: :vertical_traffic_light: :vhs:
:vibration_mode: :video_camera: :video_game: :violin: :virgo: :volcano: :vs: :walking: :waning_crescent_moon: :waning_gibbous_moon:
:warning: :watch: :water_buffalo: :watermelon: :wave: :wavy_dash: :waxing_crescent_moon: :waxing_gibbous_moon: :wc: :weary:
:wedding: :whale: :whale2: :wheelchair: :white_check_mark: :white_circle: :white_flower: :white_large_square: :white_medium_small_square: :white_medium_square:
:white_small_square: :white_square_button: :wind_chime: :wine_glass: :wink: :wolf: :woman: :womans_clothes: :womans_hat: :womens:
:worried: :wrench: :x: :yellow_heart: :yen: :yum: :zap: :zero: :zzz:
Unnamed: :u5272: :u5408: :u55b6: :u6307: :u6708: :u6709: :u6e80: :u7121: :u7533: :u7981: :u7a7a:
`,
`**[11] [Auto Linking]**
#### should be turned into links:
http://example.com
https://example.com
www.example.com
www.example.com/index
www.example.com/index.html
www.example.com/index/sub
www.example.com/index?params=1
www.example.com/index?params=1&other=2
www.example.com/index?params=1;other=2
http://example.com:8065
<http://example.com>
<www.example.com>
http://www.example.com/_/page
www.example.com/_/page
https://en.wikipedia.org/wiki/🐬
https://en.wikipedia.org/wiki/Rendering_(computer_graphics)
http://127.0.0.1
http://192.168.1.1:4040
http://[::1]:80
http://[::1]:8065
https://[::1]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065
https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443
http://username:password@example.com
http://username:password@127.0.0.1
http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
test@example.com
#### should be turned into links which link to the correct place:
[example link](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other_link](example.com/example) links to ` + "`" + `http://example.com/example` + "`" + `
www.example.com links to ` + "`" + `http://www.example.com` + "`" + `
https://example.com links to ` + "`" + `https://example.com` + "`" + `and not ` + "`" + `http://example.com` + "`" + `
https://en.wikipedia.org/wiki/🐬 links to the Wikipedia article on dolphins
https://en.wikipedia.org/wiki/URLs#Syntax links to the Syntax section of the Wikipedia article on URLs
test@example.com links to ` + "`" + `mailto:test@example.com` + "`" + `
[email link](mailto:test@example.com) links to ` + "`" + `mailto:test@example.com` + "`" + `and not ` + "`" + `http://mailto:test@example.com` + "`" + `
[other link](ts3server://example.com) links to ` + "`" + `ts3server://example.com` + "`" + `and not ` + "`" + `http://ts3server://example.com` + "`" + `
#### should not be turned into links:
example.com
readme.md
<example.com>
http://
@example.com
#### should only turn the actual link into a link and not change surrounding text
(http://example.com)
(test@example.com)
This is a sentence with a http://example.com in it.
This is a sentence with a [link](http://example.com) in it.
This is a sentence with a http://example.com/_/underscore in it.
This is a sentence with a link (http://example.com) in it.
This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.
This is a sentence with a http://192.168.1.1:4040 in it.
This is a sentence with a https://::1 in it.
This is a link to http://example.com.
`,
"*", "?", ".", "}{][)(><", "{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"& < &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"local@[127.0.0.1]",
"!@$%^&:*.,/|;'\"+=?`~#",
"'\"/\\\"\"''\\/",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"ą ć ę ł ń ó ś ź ż č ď ě ň ř š ť ž ă î ø å æ á é í ó ú Ç Ğ İ Ö Ş Ü",
"abcdefghijklmnopqrstuvwrxyz0123456789 -_",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋vok̂̓ͤ̓̂ěͬ ͆tͬ̐́̐͆h̒̏͌̓e͂ ̎̊h̽͆ͯ̄ͮi͊̂ͧͫ̇̃vͥͦ́ẻͤ-͒m̈́̀i̓ͮ͗̑͌̆̅n̓̓ͨd̊̑͛̔̚ ͨͮ̊̾rͪeͭͭ͑ͧ́͋p̈́̅̚rͧe̒̈̌s̍̽ͩ̓̇e͗n̏͊ͬͭtͨ͆ͤ̚iͪ͗̍n͐͒g̾ͦ̎ ͥ͌̽̊ͩͥ͗c̀ͬͣha̍̏̉ͪ̈̚o̊̏s̊̋̀̏̽̚.͒ͫ͛͛̎ͥ",
"H҉̵̞̟̠̖̗̘Ȅ̐̑̒̚̕̚ IS C̒̓̔̿̿̿̕̚̚̕̚̕̚̕̚̕̚̕̚OMI҉̵̞̟̠̖̗̘NG > ͡҉҉ ̵̡̢̛̗̘̙̜̝̞̟̠͇̊̋̌̍̎̏̿̿̿̚ ҉ ҉҉̡̢̡̢̛̛̖̗̘̙̜̝̞̟̠̖̗̘̙̜̝̞̟̠̊̋̌̍̎̏̐̑̒̓̔̊̋̌̍̎̏̐̑ ͡҉҉",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipedia/meta/b/be/Wikipedia-logo-v2_2x.png\" />",
"& < " '",
" %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D %0D %0A %0D%0A %20 %22 %25 %2D %2E %3C %3E %5C %5E %5F %60 %7B %7C %7D %7E",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"http?q=foobar%0d%0aContent-\nLength:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-\nType:%20text/html%0d%0aContent-Length:%2019%0d%0a%0d%0a<html>Shazam</html>",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"jill@(comment)example.com",
"jill@example.com(comment)",
"ben@ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"judy@gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.com",
}
// Strings that should pass as acceptable team names
var FuzzyStringsNames = []string{
"*",
"?",
".",
"}{][)(><",
"{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"& < &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"sue @ thatmightbe.com",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+san@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"local@[127.0.0.1]",
"jill@(comment)example.com",
"jill@example.com(comment)",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipelogo-v2_2x.png\" />",
"<b><i><u><strong><em>",
"& < " '",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋v",
"H҉̵̞̟̠̖̗̘Ȅ̐̐̑̒̚OMI҉̵̞̟̠",
}
// Strings that should pass as acceptable emails
var FuzzyStringsEmails = []string{
"sue@thatmightbe",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
}
// Lovely giberish for all to use
const GibberishText = `
Thus one besides much goodness shyly far some hyena overtook since rhinoceros nodded withdrew wombat before deserved apart a alongside the far dalmatian less ouch where yet a salmon.
Then jeez far marginal hey aboard more as leaned much oversold that inside spoke showed much went crud close save so and and after and informally much lion commendably less conductive oh excepting conductive compassionate jeepers hey a much leopard alas woolly untruthful outside snug rashly one cunning past fabulous adjusted far woodchuck and and indecisive crud loving exotic less resolute ladybug sprang drank under following far the as hence passably stolidly jeez the inset spaciously more cozily fishily the hey alas petted one audible yikes dear preparatory darn goldfinch gosh a then as moth more guinea.
Timid mislaid as salamander yikes alas ouch much that goldfinch shark in before instead dear one swore vivid versus one until regardless sang panther tolerable much preparatory hardily shuddered where coquettish far sheep coarsely exaggerated preparatory because cordial awesome gradually nutria that dear mocking behind off staunchly regarding a the komodo crud shrewd well jeez iguanodon strove strived and moodily and sought and and mounted gosh aboard crud spitefully boa.
One as highhanded fortuitous angelfish so one woodchuck dazedly kangaroo nasty instead far parrot away the worm yet testy where caribou a cuckoo onto dear reined because less tranquil kindhearted and shuddered plankton astride monkey methodically above evasive otter this wrung and courageous iguana wayward along cowered prior a.
Freely since ouch octopus the heated apart on hey the some pending placed fearless jeepers hardheadedly more that less jolly bit cuddled.
Caterpillar laboriously far wistful spilled aside far oriole newt and immeasurably yikes revealed raptly obdurately definitely scallop titilatingly one alongside monumentally ouch much wretched the spoke a before alas insolent abortive that turned hey hare much poignantly re-laid goodness yet the dear compassionate a hey scooped sped darn warmly oh and more darn craven that overtook fell and bluebird misheard that needless less ravenously in positively far romantically some babbled that rose honey then immaturely this and jollily irresistible much rarely earthworm parrot wow.
Less less bluntly jeez at goodness panther opposite oh purred a pathetically mildly less cat badly much much on from obscure in gull off manatee hatchet goodness euphemistically hence or understandable after this so that thus shook hence that mindfully yellow behind far bat wayward thanks more wrote so the flapped however alas and mallard that temperately irritably yikes squirrel.
Some reset some therefore demonstrably considering dachshund kindhearted far wow far whispered far clung this by partook much upon fit inscrutably so affirmative diligently far grinned and manifestly hummingbird hello caudal considering when aboard much buoyantly that unfitting far attractively far during much crud baneful jeez one toneless cynically oh spurious athletic meadowlark much generously one subconsciously arguable much forthrightly hawk inoffensively.
Snorted tidy stiffly against one fiendishly began burst hey revealed a beside the soothingly ceremonially affirmatively cowered when fitted this static hello emoted assenting however while far that gross besides because and dear.
Far therefore the blushed momentously the however one a wholeheartedly and considering incessantly that neurotically wore firefly grouped impotently dear one abjectly goodness so far a honey far insolently far so greyhound between above raucously echidna more halfhearted thankful squid one.
Raccoon cockatoo this while but this a far among ouch and hey alas scallop black sane as yikes hello sexy far tacky and balked wrongly more near shrewdly the yet gosh much caribou ruthlessly a on far a threw well less at the one after.
Spoke touched barbarously before much thus therefore darn scratched oh howled the less much hello after and jeez flagrantly weirdly crud komodo fabulous the much some cow jeering much egregiously a bucolically a admirably jeepers essential when ouch and tapir this while and wolverine.
Cm more much in this rewrote ouch on from aside wildebeest crane saddled where much opposite endearingly hummingbird together some beside a the goodness dear ouch ouch struck the input smooched shrugged until slick as waked hawk sincere irksomely.
Camel the pulled this richly grimaced leopard more false thought dear militant added yikes supp infallibly set orca beat hello while accurately reliably while lorikeet one strategic less hello without and smooched across plankton but jeepers pangolin the rich seal sneered pre-set lynx on radical nasty alas onto more hence flabby outbid murkily congenially dived much lubber added far eccentrically turtle before outsold onto ouch thus much and hawk tolerable much knitted yikes shot much limpet one this woolly much however hence up angry up well.
Unicorn yawned hello boundless this when express jaded closed wept tranquil after came airily merry much dismounted for much extensively less interminably far one far armadillo pled dolphin alas nutria and more oh positively koala grizzly after falcon goat strict hooted next browbeat split more far far antagonistic lingering the depending pending sheared since up before jeepers distant mastodon dropped as this more some much set far infinitesimal well shark grasshopper as hey one via some fishy and immaturely remote where weasel leopard annoying correctly wherever that sniffled much mandrill on jeez adventurous much.
Jeepers before spitefully buoyant concentric the reset moth a darn decidedly baboon giraffe outrageously groundhog on one at more overslept gosh worm away far far less much hysteric showed on so rattlesnake the and immature yikes baneful hence wow lynx hence past scornfully groaned pounded dived this one outside dachshund scowled one prior tenable therefore before scratched much much drank hey while added rabbit shark and supp cut this ironic limpet hedgehog bound more rebuking the jeepers thorough while more far due but yikes nastily brave dangerous opened tangibly aside after acrimoniously one cackled scratched.
Canny salmon hatchet more far opposite much coughed excited expedient far lizard one indiscriminate yikes jeez powerlessly forcefully tiger rooster and brought far more during this sank onto after then less amorally rude unerring some alongside irrespective bat hungrily kangaroo extravagantly inside ouch much gosh dreadfully oh much darn prior as fired guinea.
Irksomely upon up for amicably one since contrary one until flamingo tarantula far koala despite easy well gazelle ungracefully rose less that under hey more criminal unique furrowed so disbanded normal where one a a hey circuitous ouch feverish for the kookaburra and pithy far far then more the versus cliquishly across oh and explicitly much therefore as tamely alongside underlay much yikes imminently off however far across instantaneous therefore wallaby evidently foul foretold as far a jeepers invidious bearish.
More and until scandalously after wallaby petted oh much as poked much caterpillar drank beside rode actively walking scooped weird this duteous that far before human during dear house thrust more flinched opposite that ahead in far.
The painful essential jeepers merrily proudly essential and less far dismounted inside mongoose beyond confessedly robin shined heron the during since according suggestively and less some strident combed alas much man-of-war forgave so and to then inanimately.
Beside far this this a crud polite cantankerous exclusively misheard pled far circuitously and frugal less more temperately gauche goldfinch oh against this along excitedly goodhearted more classically quit serenely outside vulture ouch after one a this yet.
Less and handsomely manatee some amidst much reined komodo busted exultingly but fatuously less across mighty goodness objective alas glaringly gregariously hello the since one pridefully much well placed far less goodness jellyfish unnecessary reciprocating a far stylistic gazed one.
Hey rethought excepting lamely much and naughtily amidst more since jeez then bluebird hence less bald by some brought left the across logic loyal brightly jeez capitally that less more forward rebound a yikes chose convulsively confidently repeated broadcast much dipped when awesomely or some some regal the scowled merry zebra since more credible so inescapably fetchingly and lantern that due dear one went gosh wow well furrowed much much specially spoiled as vitally instead the seriously some rooster irrespective well imprecisely rapidly more llama.
Up to and hey without pill that this squid alas brusque on inventoried and spread the more excepting aristocratically due piquant wove beneath that macaw in more until much grimaced far and jeez enticingly unicorn some far crab more barring purely jeepers clear groomed glaring hey dear hence before the this hello.`
func RandString(l int, charset string) string {
ret := make([]byte, l)
for i := 0; i < l; i++ {
ret[i] = charset[rand.Intn(len(charset))]
}
return string(ret)
}
// func RandomEmail(length Range, charset string) string {
// emaillen := RandIntFromRange(length)
// username := RandString(emaillen, charset)
// domain := "simulator.amazonses.com"
// return "success+" + username + "@" + domain
// }
// func FuzzEmail() string {
// return FuzzyStringsEmails[RandIntFromRange(Range{0, len(FuzzyStringsEmails) - 1})]
// }
func RandomName(length Range, charset string) string {
namelen := RandIntFromRange(length)
return RandString(namelen, charset)
}
func FuzzName() string {
return FuzzyStringsNames[RandIntFromRange(Range{0, len(FuzzyStringsNames) - 1})]
}
// Random selection of text for post
func RandomText(length Range, hashtags Range, mentions Range, users []string) string {
textLength := RandIntFromRange(length)
numHashtags := RandIntFromRange(hashtags)
numMentions := RandIntFromRange(mentions)
if textLength > len(GibberishText) || textLength < 0 {
textLength = len(GibberishText)
}
startPosition := RandIntFromRange(Range{0, len(GibberishText) - textLength - 1})
words := strings.Split(GibberishText[startPosition:startPosition+textLength], " ")
for i := 0; i < numHashtags; i++ {
randword := RandIntFromRange(Range{0, len(words) - 1})
words = append(words, " #"+words[randword])
}
if len(users) > 0 {
for i := 0; i < numMentions; i++ {
randuser := RandIntFromRange(Range{0, len(users) - 1})
words = append(words, " @"+users[randuser])
}
}
// Shuffle the words
for i := range words {
j := rand.Intn(i + 1)
words[i], words[j] = words[j], words[i]
}
return strings.Join(words, " ")
}
func FuzzPost() string {
return FuzzyStringsPosts[RandIntFromRange(Range{0, len(FuzzyStringsPosts) - 1})]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
func MillisFromTime(t time.Time) int64 {
return t.UnixNano() / int64(time.Millisecond)
}
func TimeFromMillis(millis int64) time.Time {
return time.Unix(0, millis*int64(time.Millisecond))
}
func StartOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func EndOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 23, 59, 59, 999999999, t.Location())
}
func Yesterday() time.Time {
return time.Now().AddDate(0, 0, -1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
const trueUpReviewDueDay = 15
const day = time.Hour * 24
type DueDateWindow struct {
Start time.Time
End time.Time
}
func GetNextTrueUpReviewDueDate(now time.Time) time.Time {
nowYear := now.Year()
nowMonth := now.Month()
nowDay := now.Day()
finalQuarterYear := nowYear
if nowMonth >= time.October && nowMonth <= time.December {
finalQuarterYear = nowYear + 1
}
trueUpSubmissionWindows := []DueDateWindow{
{
Start: time.Date(now.Year(), time.January, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.April, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.April, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.July, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.July, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.October, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.October, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(finalQuarterYear, time.January, 15, 0, 0, 0, 0, now.Location()),
},
}
for _, window := range trueUpSubmissionWindows {
withinWindow := false
// Our due dates "wrap" around (i.e. can go into the next year), so we'll need to check the months different. Since January = 1 and December = 12, the checks
// for the current month being greater or equal to the start month and less than or equal to the end month will not work.
if window.End.Month() == time.January {
withinWindow = (nowMonth != time.January && nowMonth >= window.Start.Month()) || nowMonth == window.End.Month()
} else {
withinWindow = nowMonth >= window.Start.Month() && nowMonth <= window.End.Month()
}
// Only check the days if the current month is equal to the start or end months.
// The dates of the middle month(s) don't matter so much.
isFirstMonth := nowMonth == window.Start.Month()
if isFirstMonth {
withinWindow = withinWindow && nowDay >= window.Start.Day()
}
isFinalMonth := nowMonth == window.End.Month()
if isFinalMonth {
withinWindow = withinWindow && nowDay <= window.End.Day()
}
if withinWindow {
return window.End
}
}
return trueUpSubmissionWindows[0].End
}
func IsTrueUpReviewDueDateWithinTheNext30Days(now time.Time, dueDate time.Time) bool {
dueDateWindow := dueDate.Add(-day * 30)
if now.Before(dueDateWindow) || now.After(dueDate) {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"net/url"
"strings"
)
func URLEncode(str string) string {
strs := strings.Split(str, " ")
for i, s := range strs {
strs[i] = url.QueryEscape(s)
}
return strings.Join(strs, "%20")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"io"
"math"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
)
func StringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}
// RemoveStringFromSlice removes the first occurrence of a from slice.
func RemoveStringFromSlice(a string, slice []string) []string {
for i, str := range slice {
if str == a {
return append(slice[:i], slice[i+1:]...)
}
}
return slice
}
// RemoveStringsFromSlice removes all occurrences of strings from slice.
func RemoveStringsFromSlice(slice []string, strings ...string) []string {
newSlice := []string{}
for _, item := range slice {
if !StringInSlice(item, strings) {
newSlice = append(newSlice, item)
}
}
return newSlice
}
func StringArrayIntersection(arr1, arr2 []string) []string {
arrMap := map[string]bool{}
result := []string{}
for _, value := range arr1 {
arrMap[value] = true
}
for _, value := range arr2 {
if arrMap[value] {
result = append(result, value)
}
}
return result
}
func RemoveDuplicatesFromStringArray(arr []string) []string {
result := make([]string, 0, len(arr))
seen := make(map[string]bool)
for _, item := range arr {
if !seen[item] {
result = append(result, item)
seen[item] = true
}
}
return result
}
func StringSliceDiff(a, b []string) []string {
m := make(map[string]bool)
result := []string{}
for _, item := range b {
m[item] = true
}
for _, item := range a {
if !m[item] {
result = append(result, item)
}
}
return result
}
func GetIPAddress(r *http.Request, trustedProxyIPHeader []string) string {
address := ""
for _, proxyHeader := range trustedProxyIPHeader {
header := r.Header.Get(proxyHeader)
if header != "" {
addresses := strings.Split(header, ",")
if len(addresses) > 0 {
address = strings.TrimSpace(addresses[0])
}
}
if address != "" {
return address
}
}
if address == "" {
address, _, _ = net.SplitHostPort(r.RemoteAddr)
}
return address
}
func GetHostnameFromSiteURL(siteURL string) string {
u, err := url.Parse(siteURL)
if err != nil {
return ""
}
return u.Hostname()
}
type RequestCache struct {
Data []byte
Date string
Key string
}
// Fetch JSON data from the notices server
// if skip is passed, does a fetch without touching the cache
func GetURLWithCache(url string, cache *RequestCache, skip bool) ([]byte, error) {
// Build a GET Request, including optional If-None-Match header.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
cache.Data = nil
return nil, err
}
if !skip && cache.Data != nil {
req.Header.Add("If-None-Match", cache.Key)
req.Header.Add("If-Modified-Since", cache.Date)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
cache.Data = nil
return nil, err
}
defer resp.Body.Close()
// No change from latest known Etag?
if resp.StatusCode == http.StatusNotModified {
return cache.Data, nil
}
if resp.StatusCode != 200 {
cache.Data = nil
return nil, errors.Errorf("Fetching notices failed with status code %d", resp.StatusCode)
}
cache.Data, err = io.ReadAll(resp.Body)
if err != nil {
cache.Data = nil
return nil, err
}
// If etags headers are missing, ignore.
cache.Key = resp.Header.Get("ETag")
cache.Date = resp.Header.Get("Date")
return cache.Data, err
}
// Append tokens to passed baseURL as query params
func AppendQueryParamsToURL(baseURL string, params map[string]string) string {
u, err := url.Parse(baseURL)
if err != nil {
return ""
}
q, err := url.ParseQuery(u.RawQuery)
if err != nil {
return ""
}
for key, value := range params {
q.Add(key, value)
}
u.RawQuery = q.Encode()
return u.String()
}
// Validates RedirectURL passed during OAuth or SAML
func IsValidWebAuthRedirectURL(config *model.Config, redirectURL string) bool {
u, err := url.Parse(redirectURL)
if err == nil && (u.Scheme == "http" || u.Scheme == "https") {
if config.ServiceSettings.SiteURL != nil {
siteURL := *config.ServiceSettings.SiteURL
return strings.Index(strings.ToLower(redirectURL), strings.ToLower(siteURL)) == 0
}
return false
}
return true
}
// Validates Mobile Custom URL Scheme passed during OAuth or SAML
func IsValidMobileAuthRedirectURL(config *model.Config, redirectURL string) bool {
for _, URLScheme := range config.NativeAppSettings.AppCustomURLSchemes {
if strings.Index(strings.ToLower(redirectURL), strings.ToLower(URLScheme)) == 0 {
return true
}
}
return false
}
// RoundOffToZeroes converts all digits to 0 except the 1st one.
// Special case: If there is only 1 digit, then returns 0.
func RoundOffToZeroes(n float64) int64 {
if n >= -9 && n <= 9 {
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
tens := int64(math.Pow10(zeroes))
firstDigit := int64(n) / tens
return firstDigit * tens
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// RoundOffToZeroesResolution truncates off at most minResolution zero places.
// It implicitly sets the lowest minResolution to 0.
// e.g. 0 reports 1s, 1 reports 10s, 2 reports 100s, 3 reports 1000s
func RoundOffToZeroesResolution(n float64, minResolution int) int64 {
resolution := max(0, minResolution)
if n >= -9 && n <= 9 {
if resolution == 0 {
return int64(n)
}
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
resolution = min(zeroes, resolution)
tens := int64(math.Pow10(resolution))
significantDigits := int64(n) / tens
return significantDigits * tens
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"path"
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Context struct {
App app.AppIface
AppContext *request.Context
Logger *mlog.Logger
Params *Params
Err *model.AppError
// This is used to track the graphQL query that's being executed,
// so that we can monitor the timings in Grafana.
GraphQLOperationName string
siteURLHeader string
}
// LogAuditRec logs an audit record using default LevelAPI.
func (c *Context) LogAuditRec(rec *audit.Record) {
c.LogAuditRecWithLevel(rec, app.LevelAPI)
}
// LogAuditRec logs an audit record using specified Level.
// If the context is flagged with a permissions error then `level`
// is ignored and the audit record is emitted with `LevelPerms`.
func (c *Context) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level) {
if rec == nil {
return
}
if c.Err != nil {
rec.AddErrorCode(c.Err.StatusCode)
rec.AddErrorDesc(c.Err.Error())
if c.Err.Id == "api.context.permissions.app_error" {
level = app.LevelPerms
}
rec.Fail()
}
c.App.Srv().Audit.LogRecord(level, *rec)
}
// MakeAuditRecord creates a audit record pre-populated with data from this context.
func (c *Context) MakeAuditRecord(event string, initialStatus string) *audit.Record {
rec := &audit.Record{
EventName: event,
Status: initialStatus,
Actor: audit.EventActor{
UserId: c.AppContext.Session().UserId,
SessionId: c.AppContext.Session().Id,
Client: c.AppContext.UserAgent(),
IpAddress: c.AppContext.IPAddress(),
},
Meta: map[string]interface{}{
audit.KeyAPIPath: c.AppContext.Path(),
audit.KeyClusterID: c.App.GetClusterId(),
},
EventData: audit.EventData{
Parameters: map[string]interface{}{},
PriorState: map[string]interface{}{},
ResultState: map[string]interface{}{},
ObjectType: "",
},
}
return rec
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.AppContext.Session().UserId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
if err := c.App.Srv().Store().Audit().Save(audit); err != nil {
appErr := model.NewAppError("LogAudit", "app.audit.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.LogErrorByCode(appErr)
}
}
func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
if c.AppContext.Session().UserId != "" {
extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.AppContext.Session().UserId)
}
audit := &model.Audit{UserId: userId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
if err := c.App.Srv().Store().Audit().Save(audit); err != nil {
appErr := model.NewAppError("LogAuditWithUserId", "app.audit.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.LogErrorByCode(appErr)
}
}
func (c *Context) LogErrorByCode(err *model.AppError) {
code := err.StatusCode
msg := err.SystemMessage(i18n.TDefault)
fields := []mlog.Field{
mlog.String("err_where", err.Where),
mlog.Int("http_code", err.StatusCode),
mlog.String("error", err.Error()),
}
switch {
case (code >= http.StatusBadRequest && code < http.StatusInternalServerError) ||
err.Id == "web.check_browser_compatibility.app_error":
c.Logger.Debug(msg, fields...)
case code == http.StatusNotImplemented:
c.Logger.Info(msg, fields...)
default:
c.Logger.Error(msg, fields...)
}
}
func (c *Context) IsSystemAdmin() bool {
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
}
func (c *Context) SessionRequired() {
if !*c.App.Config().ServiceSettings.EnableUserAccessTokens &&
c.AppContext.Session().Props[model.SessionPropType] == model.SessionTypeUserAccessToken &&
c.AppContext.Session().Props[model.SessionPropIsBot] != model.SessionPropIsBotValue {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized)
return
}
if c.AppContext.Session().UserId == "" {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) CloudKeyRequired() {
if license := c.App.Channels().License(); license == nil || !license.IsCloud() || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeCloudKey {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) RemoteClusterTokenRequired() {
if license := c.App.Channels().License(); license == nil || !license.HasRemoteClusterService() || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeRemoteclusterToken {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if license := c.App.Channels().License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
// OAuth integrations are excepted
if c.AppContext.Session().IsOAuth {
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("MfaRequired", "api.context.get_user.app_error", nil, "", http.StatusUnauthorized).Wrap(err)
return
}
if user.IsGuest() && !*c.App.Config().GuestAccountsSettings.EnforceMultifactorAuthentication {
return
}
// Only required for email and ldap accounts
if user.AuthService != "" &&
user.AuthService != model.UserAuthServiceEmail &&
user.AuthService != model.UserAuthServiceLdap {
return
}
// Special case to let user get themself
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
if c.AppContext.Path() == path.Join(subpath, "/api/v4/users/me") {
return
}
// Bots are exempt
if user.IsBot {
return
}
if !user.MfaActive {
c.Err = model.NewAppError("MfaRequired", "api.context.mfa_required.app_error", nil, "", http.StatusForbidden)
return
}
}
// ExtendSessionExpiryIfNeeded will update Session.ExpiresAt based on session lengths in config.
// Session cookies will be resent to the client with updated max age.
func (c *Context) ExtendSessionExpiryIfNeeded(w http.ResponseWriter, r *http.Request) {
if ok := c.App.ExtendSessionExpiryIfNeeded(c.AppContext.Session()); ok {
c.App.AttachSessionCookies(c.AppContext, w, r)
}
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
cookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
func (c *Context) SetInvalidParam(parameter string) {
c.Err = NewInvalidParamError(parameter)
}
func (c *Context) SetInvalidParamWithErr(parameter string, err error) {
c.Err = NewInvalidParamError(parameter).Wrap(err)
}
func (c *Context) SetInvalidURLParam(parameter string) {
c.Err = NewInvalidURLParamError(parameter)
}
func (c *Context) SetServerBusyError() {
c.Err = NewServerBusyError()
}
func (c *Context) SetInvalidRemoteIdError(id string) {
c.Err = NewInvalidRemoteIdError(id)
}
func (c *Context) SetInvalidRemoteClusterTokenError() {
c.Err = NewInvalidRemoteClusterTokenError()
}
func (c *Context) SetJSONEncodingError(err error) {
c.Err = NewJSONEncodingError(err)
}
func (c *Context) SetCommandNotFoundError() {
c.Err = model.NewAppError("GetCommand", "store.sql_command.save.get.app_error", nil, "", http.StatusNotFound)
}
func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
metrics := c.App.Metrics()
if et := r.Header.Get(model.HeaderEtagClient); etag != "" {
if et == etag {
w.Header().Set(model.HeaderEtagServer, etag)
w.WriteHeader(http.StatusNotModified)
if metrics != nil {
metrics.IncrementEtagHitCounter(routeName)
}
return true
}
}
if metrics != nil {
metrics.IncrementEtagMissCounter(routeName)
}
return false
}
func NewInvalidParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidURLParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_url_param.app_error", map[string]any{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewServerBusyError() *model.AppError {
err := model.NewAppError("Context", "api.context.server_busy.app_error", nil, "", http.StatusServiceUnavailable)
return err
}
func NewInvalidRemoteIdError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", map[string]any{"RemoteId": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidRemoteClusterTokenError() *model.AppError {
err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", nil, "", http.StatusUnauthorized)
return err
}
func NewJSONEncodingError(err error) *model.AppError {
appErr := model.NewAppError("Context", "api.context.json_encoding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return appErr
}
func (c *Context) SetPermissionError(permissions ...*model.Permission) {
c.Err = c.App.MakePermissionError(c.AppContext.Session(), permissions)
}
func (c *Context) SetSiteURLHeader(url string) {
c.siteURLHeader = strings.TrimRight(url, "/")
}
func (c *Context) GetSiteURLHeader() string {
return c.siteURLHeader
}
func (c *Context) RequireUserId() *Context {
if c.Err != nil {
return c
}
if c.Params.UserId == model.Me {
c.Params.UserId = c.AppContext.Session().UserId
}
if !model.IsValidId(c.Params.UserId) {
c.SetInvalidURLParam("user_id")
}
return c
}
func (c *Context) RequireTeamId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
}
return c
}
func (c *Context) RequireCategoryId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidCategoryId(c.Params.CategoryId) {
c.SetInvalidURLParam("category_id")
}
return c
}
func (c *Context) RequireInviteId() *Context {
if c.Err != nil {
return c
}
if c.Params.InviteId == "" {
c.SetInvalidURLParam("invite_id")
}
return c
}
func (c *Context) RequireTokenId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.TokenId) {
c.SetInvalidURLParam("token_id")
}
return c
}
func (c *Context) RequireThreadId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ThreadId) {
c.SetInvalidURLParam("thread_id")
}
return c
}
func (c *Context) RequireTimestamp() *Context {
if c.Err != nil {
return c
}
if c.Params.Timestamp == 0 {
c.SetInvalidURLParam("timestamp")
}
return c
}
func (c *Context) RequireChannelId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ChannelId) {
c.SetInvalidURLParam("channel_id")
}
return c
}
func (c *Context) RequireUsername() *Context {
if c.Err != nil {
return c
}
if !model.IsValidUsername(c.Params.Username) {
c.SetInvalidParam("username")
}
return c
}
func (c *Context) RequirePostId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.PostId) {
c.SetInvalidURLParam("post_id")
}
return c
}
func (c *Context) RequirePolicyId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.PolicyId) {
c.SetInvalidURLParam("policy_id")
}
return c
}
func (c *Context) RequireAppId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.AppId) {
c.SetInvalidURLParam("app_id")
}
return c
}
func (c *Context) RequireFileId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.FileId) {
c.SetInvalidURLParam("file_id")
}
return c
}
func (c *Context) RequireUploadId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.UploadId) {
c.SetInvalidURLParam("upload_id")
}
return c
}
func (c *Context) RequireFilename() *Context {
if c.Err != nil {
return c
}
if c.Params.Filename == "" {
c.SetInvalidURLParam("filename")
}
return c
}
func (c *Context) RequirePluginId() *Context {
if c.Err != nil {
return c
}
if c.Params.PluginId == "" {
c.SetInvalidURLParam("plugin_id")
}
return c
}
func (c *Context) RequireReportId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ReportId) {
c.SetInvalidURLParam("report_id")
}
return c
}
func (c *Context) RequireEmojiId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.EmojiId) {
c.SetInvalidURLParam("emoji_id")
}
return c
}
func (c *Context) RequireTeamName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidTeamName(c.Params.TeamName) {
c.SetInvalidURLParam("team_name")
}
return c
}
func (c *Context) RequireChannelName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidChannelIdentifier(c.Params.ChannelName) {
c.SetInvalidURLParam("channel_name")
}
return c
}
func (c *Context) SanitizeEmail() *Context {
if c.Err != nil {
return c
}
c.Params.Email = strings.ToLower(c.Params.Email)
if !model.IsValidEmail(c.Params.Email) {
c.SetInvalidURLParam("email")
}
return c
}
func (c *Context) RequireCategory() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.Category, true) {
c.SetInvalidURLParam("category")
}
return c
}
func (c *Context) RequireService() *Context {
if c.Err != nil {
return c
}
if c.Params.Service == "" {
c.SetInvalidURLParam("service")
}
return c
}
func (c *Context) RequirePreferenceName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.PreferenceName, true) {
c.SetInvalidURLParam("preference_name")
}
return c
}
func (c *Context) RequireEmojiName() *Context {
if c.Err != nil {
return c
}
validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
if c.Params.EmojiName == "" || len(c.Params.EmojiName) > model.EmojiNameMaxLength || !validName.MatchString(c.Params.EmojiName) {
c.SetInvalidURLParam("emoji_name")
}
return c
}
func (c *Context) RequireHookId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.HookId) {
c.SetInvalidURLParam("hook_id")
}
return c
}
func (c *Context) RequireCommandId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.CommandId) {
c.SetInvalidURLParam("command_id")
}
return c
}
func (c *Context) RequireJobId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.JobId) {
c.SetInvalidURLParam("job_id")
}
return c
}
func (c *Context) RequireJobType() *Context {
if c.Err != nil {
return c
}
if c.Params.JobType == "" || len(c.Params.JobType) > 32 {
c.SetInvalidURLParam("job_type")
}
return c
}
func (c *Context) RequireRoleId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.RoleId) {
c.SetInvalidURLParam("role_id")
}
return c
}
func (c *Context) RequireSchemeId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.SchemeId) {
c.SetInvalidURLParam("scheme_id")
}
return c
}
func (c *Context) RequireRoleName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidRoleName(c.Params.RoleName) {
c.SetInvalidURLParam("role_name")
}
return c
}
func (c *Context) RequireGroupId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.GroupId) {
c.SetInvalidURLParam("group_id")
}
return c
}
func (c *Context) RequireRemoteId() *Context {
if c.Err != nil {
return c
}
if c.Params.RemoteId == "" {
c.SetInvalidURLParam("remote_id")
}
return c
}
func (c *Context) RequireSyncableId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.SyncableId) {
c.SetInvalidURLParam("syncable_id")
}
return c
}
func (c *Context) RequireSyncableType() *Context {
if c.Err != nil {
return c
}
if c.Params.SyncableType != model.GroupSyncableTypeTeam && c.Params.SyncableType != model.GroupSyncableTypeChannel {
c.SetInvalidURLParam("syncable_type")
}
return c
}
func (c *Context) RequireBotUserId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.BotUserId) {
c.SetInvalidURLParam("bot_user_id")
}
return c
}
func (c *Context) RequireInvoiceId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.InvoiceId) != 27 && c.Params.InvoiceId != model.UpcomingInvoice {
c.SetInvalidURLParam("invoice_id")
}
return c
}
func (c *Context) GetRemoteID(r *http.Request) string {
return r.Header.Get(model.HeaderRemoteclusterId)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"github.com/mattermost/gziphandler"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
app_opentracing "github.com/mattermost/mattermost-server/v6/server/channels/app/opentracing"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store/opentracinglayer"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
pos := strings.LastIndex(handlerName, ".")
if pos != -1 && len(handlerName) > pos {
handlerName = handlerName[pos+1:]
}
return handlerName
}
func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
}
func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
// Determine the CSP SHA directive needed for subpath support, if any. This value is fixed
// on server start and intentionally requires a restart to take effect.
subpath, _ := utils.GetSubpathFromConfig(w.srv.Config())
return &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: true,
cspShaDirective: utils.GetSubpathScriptHash(subpath),
}
}
type Handler struct {
Srv *app.Server
HandleFunc func(*Context, http.ResponseWriter, *http.Request)
HandlerName string
RequireSession bool
RequireCloudKey bool
RequireRemoteClusterToken bool
TrustRequester bool
RequireMfa bool
IsStatic bool
IsLocal bool
DisableWhenBusy bool
cspShaDirective string
}
func generateDevCSP(c Context) string {
var devCSP []string
// Add unsafe-eval to the content security policy for faster source maps in development mode
if model.BuildNumber == "dev" {
devCSP = append(devCSP, "'unsafe-eval'")
}
// Add unsafe-inline to unlock extensions like React & Redux DevTools in Firefox
// see https://github.com/reduxjs/redux-devtools/issues/380
if model.BuildNumber == "dev" {
devCSP = append(devCSP, "'unsafe-inline'")
}
// Add supported flags for debugging during development, even if not on a dev build.
if *c.App.Config().ServiceSettings.DeveloperFlags != "" {
for _, devFlagKVStr := range strings.Split(*c.App.Config().ServiceSettings.DeveloperFlags, ",") {
devFlagKVSplit := strings.SplitN(devFlagKVStr, "=", 2)
if len(devFlagKVSplit) != 2 {
c.Logger.Warn("Unable to parse developer flag", mlog.String("developer_flag", devFlagKVStr))
continue
}
devFlagKey := devFlagKVSplit[0]
devFlagValue := devFlagKVSplit[1]
// Ignore disabled keys
if devFlagValue != "true" {
continue
}
// Honour only supported keys
switch devFlagKey {
case "unsafe-eval", "unsafe-inline":
if model.BuildNumber == "dev" {
// These flags are added automatically for dev builds
continue
}
devCSP = append(devCSP, "'"+devFlagKey+"'")
default:
c.Logger.Warn("Unrecognized developer flag", mlog.String("developer_flag", devFlagKVStr))
}
}
}
// Add flags for Webpack dev servers used by other products during development
if model.BuildNumber == "dev" {
boardsURL := os.Getenv("MM_BOARDS_DEV_SERVER_URL")
if boardsURL == "" {
// Focalboard runs on http://localhost:9006 by default
boardsURL = "http://localhost:9006"
}
devCSP = append(devCSP, boardsURL)
playbooksURL := os.Getenv("MM_PLAYBOOKS_DEV_SERVER_URL")
if playbooksURL == "" {
// Playbooks runs on http://localhost:9007 by default
playbooksURL = "http://localhost:9007"
}
devCSP = append(devCSP, playbooksURL)
}
if len(devCSP) == 0 {
return ""
}
return " " + strings.Join(devCSP, " ")
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w = newWrappedWriter(w)
now := time.Now()
appInstance := app.New(app.ServerConnector(h.Srv.Channels()))
requestID := model.NewId()
var statusCode string
defer func() {
responseLogFields := []mlog.Field{
mlog.String("method", r.Method),
mlog.String("url", r.URL.Path),
mlog.String("request_id", requestID),
}
// Websockets are returning status code 0 to requests after closing the socket
if statusCode != "0" {
responseLogFields = append(responseLogFields, mlog.String("status_code", statusCode))
}
mlog.Debug("Received HTTP request", responseLogFields...)
}()
c := &Context{
AppContext: &request.Context{},
App: appInstance,
}
t, _ := i18n.GetTranslationsAndLocaleFromRequest(r)
c.AppContext.SetT(t)
c.AppContext.SetRequestId(requestID)
c.AppContext.SetIPAddress(utils.GetIPAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader))
c.AppContext.SetUserAgent(r.UserAgent())
c.AppContext.SetAcceptLanguage(r.Header.Get("Accept-Language"))
c.AppContext.SetPath(r.URL.Path)
c.AppContext.SetContext(context.Background())
c.Params = ParamsFromRequest(r)
c.Logger = c.App.Log()
if *c.App.Config().ServiceSettings.EnableOpenTracing {
span, ctx := tracing.StartRootSpanByContext(context.Background(), "web:ServeHTTP")
carrier := opentracing.HTTPHeadersCarrier(r.Header)
_ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, carrier)
ext.HTTPMethod.Set(span, r.Method)
ext.HTTPUrl.Set(span, c.AppContext.Path())
ext.PeerAddress.Set(span, c.AppContext.IPAddress())
span.SetTag("request_id", c.AppContext.RequestId())
span.SetTag("user_agent", c.AppContext.UserAgent())
defer func() {
if c.Err != nil {
span.LogFields(spanlog.Error(c.Err))
ext.HTTPStatusCode.Set(span, uint16(c.Err.StatusCode))
ext.Error.Set(span, true)
}
span.Finish()
}()
c.AppContext.SetContext(ctx)
tmpSrv := *c.App.Srv()
tmpSrv.SetStore(opentracinglayer.New(c.App.Srv().Store(), ctx))
c.App.SetServer(&tmpSrv)
c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
}
// Set the max request body size to be equal to MaxFileSize.
// Ideally, non-file request bodies should be smaller than file request bodies,
// but we don't have a clean way to identify all file upload handlers.
// So to keep it simple, we clamp it to the max file size.
// We add a buffer of bytes.MinRead so that file sizes close to max file size
// do not get cut off.
r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
if c.App.Channels().License().IsCloud() {
siteURLHeader = *c.App.Config().ServiceSettings.SiteURL + subpath
}
c.SetSiteURLHeader(siteURLHeader)
w.Header().Set(model.HeaderRequestId, c.AppContext.RequestId())
w.Header().Set(model.HeaderVersionId, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.Channels().License() != nil))
if *c.App.Config().ServiceSettings.TLSStrictTransport {
w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge))
}
// Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress
w.Header().Set("Permissions-Policy", "")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
cloudCSP := ""
if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase {
cloudCSP = " js.stripe.com/v3"
}
if h.IsStatic {
// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
devCSP := generateDevCSP(*c)
// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com%s%s%s",
cloudCSP,
h.cspShaDirective,
devCSP,
))
} else {
// All api response bodies will be JSON formatted by default
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
w.Header().Set("Expires", "0")
}
}
token, tokenLocation := app.ParseAuthTokenFromRequest(r)
if token != "" && tokenLocation != app.TokenLocationCloudHeader && tokenLocation != app.TokenLocationRemoteClusterHeader {
session, err := c.App.GetSession(token)
defer c.App.ReturnSessionToPool(session)
if err != nil {
c.Logger.Info("Invalid session", mlog.Err(err))
if err.StatusCode == http.StatusInternalServerError {
c.Err = err
} else if h.RequireSession {
c.RemoveSessionCookie(w, r)
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
}
} else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
} else {
c.AppContext.SetSession(session)
}
// Rate limit by UserID
if c.App.Srv().RateLimiter != nil && c.App.Srv().RateLimiter.UserIdRateLimit(c.AppContext.Session().UserId, w) {
return
}
h.checkCSRFToken(c, r, token, tokenLocation, session)
} else if token != "" && c.App.Channels().License().IsCloud() && tokenLocation == app.TokenLocationCloudHeader {
// Check to see if this provided token matches our CWS Token
session, err := c.App.GetCloudSession(token)
if err != nil {
c.Logger.Warn("Invalid CWS token", mlog.Err(err))
c.Err = err
} else {
c.AppContext.SetSession(session)
}
} else if token != "" && c.App.Channels().License() != nil && c.App.Channels().License().HasRemoteClusterService() && tokenLocation == app.TokenLocationRemoteClusterHeader {
// Get the remote cluster
if remoteId := c.GetRemoteID(r); remoteId == "" {
c.Logger.Warn("Missing remote cluster id") //
c.Err = model.NewAppError("ServeHTTP", "api.context.remote_id_missing.app_error", nil, "", http.StatusUnauthorized)
} else {
// Check the token is correct for the remote cluster id.
session, err := c.App.GetRemoteClusterSession(token, remoteId)
if err != nil {
c.Logger.Warn("Invalid remote cluster token", mlog.Err(err))
c.Err = err
} else {
c.AppContext.SetSession(session)
}
}
}
c.Logger = c.App.Log().With(
mlog.String("path", c.AppContext.Path()),
mlog.String("request_id", c.AppContext.RequestId()),
mlog.String("ip_addr", c.AppContext.IPAddress()),
mlog.String("user_id", c.AppContext.Session().UserId),
mlog.String("method", r.Method),
)
c.AppContext.SetLogger(c.Logger)
if c.Err == nil && h.RequireSession {
c.SessionRequired()
}
if c.Err == nil && h.RequireMfa {
c.MfaRequired()
}
if c.Err == nil && h.DisableWhenBusy && c.App.Srv().Platform().Busy.IsBusy() {
c.SetServerBusyError()
}
if c.Err == nil && h.RequireCloudKey {
c.CloudKeyRequired()
}
if c.Err == nil && h.RequireRemoteClusterToken {
c.RemoteClusterTokenRequired()
}
if c.Err == nil && h.IsLocal {
// if the connection is local, RemoteAddr shouldn't have the
// shape IP:PORT (it will be "@" in Linux, for example)
isLocalOrigin := !strings.Contains(r.RemoteAddr, ":")
if *c.App.Config().ServiceSettings.EnableLocalMode && isLocalOrigin {
c.AppContext.SetSession(&model.Session{Local: true})
} else if !isLocalOrigin {
c.Err = model.NewAppError("", "api.context.local_origin_required.app_error", nil, "LocalOriginRequired", http.StatusUnauthorized)
}
}
if c.Err == nil {
h.HandleFunc(c, w, r)
}
// Handle errors that have occurred
if c.Err != nil {
c.Err.RequestId = c.AppContext.RequestId()
c.LogErrorByCode(c.Err)
// The locale translation needs to happen after we have logged it.
// We don't want the server logs to be translated as per user locale.
c.Err.Translate(c.AppContext.T)
c.Err.Where = r.URL.Path
// Block out detailed error when not in developer mode
if !*c.App.Config().ServiceSettings.EnableDeveloper {
c.Err.DetailedError = ""
}
// Sanitize all 5xx error messages in hardened mode
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode && c.Err.StatusCode >= 500 {
c.Err.Id = ""
c.Err.Message = "Internal Server Error"
c.Err.DetailedError = ""
c.Err.StatusCode = 500
c.Err.Where = ""
c.Err.IsOAuth = false
}
if IsAPICall(c.App, r) || IsWebhookCall(c.App, r) || IsOAuthAPICall(c.App, r) || r.Header.Get("X-Mobile-App") != "" {
w.WriteHeader(c.Err.StatusCode)
w.Write([]byte(c.Err.ToJSON()))
} else {
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
}
if c.App.Metrics() != nil {
c.App.Metrics().IncrementHTTPError()
}
}
statusCode = strconv.Itoa(w.(*responseWriterWrapper).StatusCode())
if c.App.Metrics() != nil {
c.App.Metrics().IncrementHTTPRequest()
if r.URL.Path != model.APIURLSuffix+"/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
var endpoint string
if strings.HasPrefix(r.URL.Path, model.APIURLSuffixV5) {
// It's a graphQL query, so use the operation name.
endpoint = c.GraphQLOperationName
} else {
endpoint = h.HandlerName
}
c.App.Metrics().ObserveAPIEndpointDuration(endpoint, r.Method, statusCode, elapsed)
}
}
}
// checkCSRFToken performs a CSRF check on the provided request with the given CSRF token. Returns whether or not
// a CSRF check occurred and whether or not it succeeded.
func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, tokenLocation app.TokenLocation, session *model.Session) (checked bool, passed bool) {
csrfCheckNeeded := session != nil && c.Err == nil && tokenLocation == app.TokenLocationCookie && !h.TrustRequester && r.Method != "GET"
csrfCheckPassed := false
if csrfCheckNeeded {
csrfHeader := r.Header.Get(model.HeaderCsrfToken)
if csrfHeader == session.GetCSRF() {
csrfCheckPassed = true
} else if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header"
sid := ""
userId := ""
if session != nil {
sid = session.Id
userId = session.UserId
}
fields := []mlog.Field{
mlog.String("path", r.URL.Path),
mlog.String("ip", r.RemoteAddr),
mlog.String("session_id", sid),
mlog.String("user_id", userId),
}
if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
c.Logger.Warn(csrfErrorMessage, fields...)
} else {
c.Logger.Debug(csrfErrorMessage, fields...)
csrfCheckPassed = true
}
}
if !csrfCheckPassed {
c.AppContext.SetSession(&model.Session{})
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
}
}
return csrfCheckNeeded, csrfCheckPassed
}
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
// granted.
func (w *Web) APIHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
// websocket.
func (w *Web) APIHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
// be granted.
func (w *Web) APISessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"encoding/json"
"html"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (w *Web) InitOAuth() {
// API version independent OAuth 2.0 as a service provider endpoints
w.MainRouter.Handle("/oauth/authorize", w.APIHandlerTrustRequester(authorizeOAuthPage)).Methods("GET")
w.MainRouter.Handle("/oauth/authorize", w.APISessionRequired(authorizeOAuthApp)).Methods("POST")
w.MainRouter.Handle("/oauth/deauthorize", w.APISessionRequired(deauthorizeOAuthApp)).Methods("POST")
w.MainRouter.Handle("/oauth/access_token", w.APIHandlerTrustRequester(getAccessToken)).Methods("POST")
// API version independent OAuth as a client endpoints
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/login", w.APIHandler(loginWithOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.APIHandler(mobileLoginWithOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.APIHandler(signupWithOAuth)).Methods("GET")
// Old endpoints for backwards compatibility, needed to not break SSO for any old setups
w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/api/v4/oauth_test", w.APISessionRequired(testHandler)).Methods("GET")
}
func testHandler(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
var authRequest *model.AuthorizeRequest
err := json.NewDecoder(r.Body).Decode(&authRequest)
if err != nil || authRequest == nil {
c.SetInvalidParamWithErr("authorize_request", err)
return
}
if err := authRequest.IsValid(); err != nil {
c.Err = err
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
auditRec := c.MakeAuditRecord("authorizeOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
redirectURL, appErr := c.App.AllowOAuthAppAccessToUser(c.AppContext.Session().UserId, authRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("")
w.Write([]byte(model.MapToJSON(map[string]string{"redirect": redirectURL})))
}
func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
requestData := model.MapFromJSON(r.Body)
clientId := requestData["client_id"]
if !model.IsValidId(clientId) {
c.SetInvalidParam("client_id")
return
}
auditRec := c.MakeAuditRecord("deauthorizeOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
err := c.App.DeauthorizeOAuthAppForUser(c.AppContext.Session().UserId, clientId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented)
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
authRequest := &model.AuthorizeRequest{
ResponseType: r.URL.Query().Get("response_type"),
ClientId: r.URL.Query().Get("client_id"),
RedirectURI: r.URL.Query().Get("redirect_uri"),
Scope: r.URL.Query().Get("scope"),
State: r.URL.Query().Get("state"),
}
loginHint := r.URL.Query().Get("login_hint")
if err := authRequest.IsValid(); err != nil {
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
url.Values{
"type": []string{"oauth_invalid_param"},
"message": []string{err.Message},
}, c.App.AsymmetricSigningKey())
return
}
oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId)
if err != nil {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
// here we should check if the user is logged in
if c.AppContext.Session().UserId == "" {
if loginHint == model.UserAuthServiceSaml {
http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
} else {
http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
}
return
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
url.Values{
"type": []string{"oauth_invalid_redirect_url"},
"message": []string{i18n.T("api.oauth.allow_oauth.redirect_callback.app_error")},
}, c.App.AsymmetricSigningKey())
return
}
isAuthorized := false
if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext.Session().UserId, model.PreferenceCategoryAuthorizedOAuthApp, authRequest.ClientId); err == nil {
// when we support scopes we should check if the scopes match
isAuthorized = true
}
// Automatically allow if the app is trusted
if oauthApp.IsTrusted || isAuthorized {
redirectURL, err := c.App.AllowOAuthAppAccessToUser(c.AppContext.Session().UserId, authRequest)
if err != nil {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
staticDir, _ := fileutils.FindDir(model.ClientDir)
http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
}
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
code := r.FormValue("code")
refreshToken := r.FormValue("refresh_token")
grantType := r.FormValue("grant_type")
switch grantType {
case model.AccessTokenGrantType:
if code == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest)
return
}
case model.RefreshTokenGrantType:
if refreshToken == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest)
return
}
default:
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest)
return
}
clientId := r.FormValue("client_id")
if !model.IsValidId(clientId) {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest)
return
}
secret := r.FormValue("client_secret")
if secret == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "", http.StatusBadRequest)
return
}
redirectURI := r.FormValue("redirect_uri")
auditRec := c.MakeAuditRecord("getAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("grant_type", grantType)
auditRec.AddMeta("client_id", clientId)
c.LogAudit("attempt")
accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(accessRsp); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
service := c.Params.Service
oauthError := r.URL.Query().Get("error")
if oauthError == "access_denied" {
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
"type": []string{"oauth_access_denied"},
"service": []string{strings.Title(service)},
}, c.App.AsymmetricSigningKey())
return
}
code := r.URL.Query().Get("code")
if code == "" {
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
"type": []string{"oauth_missing_code"},
"service": []string{strings.Title(service)},
}, c.App.AsymmetricSigningKey())
return
}
state := r.URL.Query().Get("state")
uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
body, teamId, props, tokenUser, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri)
action := ""
hasRedirectURL := false
isMobile := false
redirectURL := ""
if props != nil {
action = props["action"]
isMobile = action == model.OAuthActionMobile
if val, ok := props["redirect_to"]; ok {
redirectURL = val
hasRedirectURL = redirectURL != ""
}
}
redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL)
renderError := func(err *model.AppError) {
if isMobile && hasRedirectURL {
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
} else {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
}
}
if err != nil {
err.Translate(c.AppContext.T)
c.LogErrorByCode(err)
renderError(err)
return
}
user, err := c.App.CompleteOAuth(c.AppContext, service, body, teamId, props, tokenUser)
if err != nil {
err.Translate(c.AppContext.T)
c.LogErrorByCode(err)
renderError(err)
return
}
if action == model.OAuthActionEmailToSSO {
redirectURL = c.GetSiteURLHeader() + "/login?extra=signin_change"
} else if action == model.OAuthActionSSOToEmail {
redirectURL = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
} else {
err = c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, false)
if err != nil {
err.Translate(c.AppContext.T)
mlog.Error(err.Error())
renderError(err)
return
}
// Old mobile version
if isMobile && !hasRedirectURL {
c.App.AttachSessionCookies(c.AppContext, w, r)
return
} else
// New mobile version
if isMobile && hasRedirectURL {
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
model.SessionCookieToken: c.AppContext.Session().Token,
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
})
utils.RenderMobileAuthComplete(w, redirectURL)
return
} else { // For web
c.App.AttachSessionCookies(c.AppContext, w, r)
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
}
func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
loginHint := r.URL.Query().Get("login_hint")
redirectURL := r.URL.Query().Get("redirect_to")
if redirectURL != "" && !utils.IsValidWebAuthRedirectURL(c.App.Config(), redirectURL) {
c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, "", http.StatusBadRequest)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
if redirectURL != "" && !utils.IsValidMobileAuthRedirectURL(c.App.Config(), redirectURL) {
err := model.NewAppError("mobileLoginWithOAuth", "api.invalid_custom_url_scheme", nil, "", http.StatusBadRequest)
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableUserCreation {
utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{
"message": []string{i18n.T("api.oauth.singup_with_oauth.disabled.app_error")},
}, c.App.AsymmetricSigningKey())
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string {
parsed, _ := url.Parse(targetURL)
if parsed == nil || parsed.Scheme != "" || parsed.Host != "" {
return targetURL
}
if targetURL != "" && targetURL[0] != '/' {
targetURL = "/" + targetURL
}
return siteURLPrefix + targetURL
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
PageDefault = 0
PerPageDefault = 60
PerPageMaximum = 200
LogsPerPageDefault = 10000
LogsPerPageMaximum = 10000
LimitDefault = 60
LimitMaximum = 200
)
type Params struct {
UserId string
TeamId string
InviteId string
TokenId string
ThreadId string
Timestamp int64
TimeRange string
ChannelId string
PostId string
PolicyId string
FileId string
Filename string
UploadId string
PluginId string
CommandId string
HookId string
ReportId string
EmojiId string
AppId string
Email string
Username string
TeamName string
ChannelName string
PreferenceName string
EmojiName string
Category string
Service string
JobId string
JobType string
ActionId string
RoleId string
RoleName string
SchemeId string
Scope string
GroupId string
Page int
PerPage int
LogsPerPage int
Permanent bool
RemoteId string
SyncableId string
SyncableType model.GroupSyncableType
BotUserId string
Q string
IsLinked *bool
IsConfigured *bool
NotAssociatedToTeam string
NotAssociatedToChannel string
Paginate *bool
IncludeMemberCount bool
NotAssociatedToGroup string
ExcludeDefaultChannels bool
LimitAfter int
LimitBefore int
GroupIDs string
IncludeTotalCount bool
IncludeDeleted bool
FilterAllowReference bool
FilterParentTeamPermitted bool
CategoryId string
WarnMetricId string
ExportName string
ExcludePolicyConstrained bool
GroupSource model.GroupSource
FilterHasMember string
IncludeChannelMemberCount string
// Cloud
InvoiceId string
}
func ParamsFromRequest(r *http.Request) *Params {
params := &Params{}
props := mux.Vars(r)
query := r.URL.Query()
params.UserId = props["user_id"]
params.TeamId = props["team_id"]
params.CategoryId = props["category_id"]
params.InviteId = props["invite_id"]
params.TokenId = props["token_id"]
params.ThreadId = props["thread_id"]
if val, ok := props["channel_id"]; ok {
params.ChannelId = val
} else {
params.ChannelId = query.Get("channel_id")
}
params.PostId = props["post_id"]
params.PolicyId = props["policy_id"]
params.FileId = props["file_id"]
params.Filename = query.Get("filename")
params.UploadId = props["upload_id"]
params.PluginId = props["plugin_id"]
params.CommandId = props["command_id"]
params.HookId = props["hook_id"]
params.ReportId = props["report_id"]
params.EmojiId = props["emoji_id"]
params.AppId = props["app_id"]
params.Email = props["email"]
params.Username = props["username"]
params.TeamName = strings.ToLower(props["team_name"])
params.ChannelName = strings.ToLower(props["channel_name"])
params.Category = props["category"]
params.Service = props["service"]
params.PreferenceName = props["preference_name"]
params.EmojiName = props["emoji_name"]
params.JobId = props["job_id"]
params.JobType = props["job_type"]
params.ActionId = props["action_id"]
params.RoleId = props["role_id"]
params.RoleName = props["role_name"]
params.SchemeId = props["scheme_id"]
params.GroupId = props["group_id"]
params.RemoteId = props["remote_id"]
params.InvoiceId = props["invoice_id"]
params.Scope = query.Get("scope")
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
params.Page = PageDefault
} else {
params.Page = val
}
if val, err := strconv.ParseInt(props["timestamp"], 10, 64); err != nil || val < 0 {
params.Timestamp = 0
} else {
params.Timestamp = val
}
params.TimeRange = query.Get("time_range")
params.Permanent, _ = strconv.ParseBool(query.Get("permanent"))
params.PerPage = getPerPageFromQuery(query)
if val, err := strconv.Atoi(query.Get("logs_per_page")); err != nil || val < 0 {
params.LogsPerPage = LogsPerPageDefault
} else if val > LogsPerPageMaximum {
params.LogsPerPage = LogsPerPageMaximum
} else {
params.LogsPerPage = val
}
if val, err := strconv.Atoi(query.Get("limit_after")); err != nil || val < 0 {
params.LimitAfter = LimitDefault
} else if val > LimitMaximum {
params.LimitAfter = LimitMaximum
} else {
params.LimitAfter = val
}
if val, err := strconv.Atoi(query.Get("limit_before")); err != nil || val < 0 {
params.LimitBefore = LimitDefault
} else if val > LimitMaximum {
params.LimitBefore = LimitMaximum
} else {
params.LimitBefore = val
}
params.SyncableId = props["syncable_id"]
switch props["syncable_type"] {
case "teams":
params.SyncableType = model.GroupSyncableTypeTeam
case "channels":
params.SyncableType = model.GroupSyncableTypeChannel
}
params.BotUserId = props["bot_user_id"]
params.Q = query.Get("q")
if val, err := strconv.ParseBool(query.Get("is_linked")); err == nil {
params.IsLinked = &val
}
if val, err := strconv.ParseBool(query.Get("is_configured")); err == nil {
params.IsConfigured = &val
}
params.NotAssociatedToTeam = query.Get("not_associated_to_team")
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
params.IncludeChannelMemberCount = query.Get("include_channel_member_count")
if val, err := strconv.ParseBool(query.Get("paginate")); err == nil {
params.Paginate = &val
}
params.IncludeMemberCount, _ = strconv.ParseBool(query.Get("include_member_count"))
params.NotAssociatedToGroup = query.Get("not_associated_to_group")
params.ExcludeDefaultChannels, _ = strconv.ParseBool(query.Get("exclude_default_channels"))
params.GroupIDs = query.Get("group_ids")
params.IncludeTotalCount, _ = strconv.ParseBool(query.Get("include_total_count"))
params.IncludeDeleted, _ = strconv.ParseBool(query.Get("include_deleted"))
params.WarnMetricId = props["warn_metric_id"]
params.ExportName = props["export_name"]
params.ExcludePolicyConstrained, _ = strconv.ParseBool(query.Get("exclude_policy_constrained"))
if val := query.Get("group_source"); val != "" {
switch val {
case "custom":
params.GroupSource = model.GroupSourceCustom
default:
params.GroupSource = model.GroupSourceLdap
}
}
params.FilterHasMember = query.Get("filter_has_member")
return params
}
// getPerPageFromQuery returns the PerPage value from the given query.
// This function should be removed and the support for `pageSize`
// should be dropped after v1.46 of the mobile app is no longer supported
// https://mattermost.atlassian.net/browse/MM-38131
func getPerPageFromQuery(query url.Values) int {
val, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
val, err = strconv.Atoi(query.Get("pageSize"))
}
if err != nil || val < 0 {
return PerPageDefault
} else if val > PerPageMaximum {
return PerPageMaximum
}
return val
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bufio"
"errors"
"net"
"net/http"
)
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
statusCodeWritten bool
hijacker http.Hijacker
flusher http.Flusher
}
func newWrappedWriter(original http.ResponseWriter) *responseWriterWrapper {
hijacker, _ := original.(http.Hijacker)
flusher, _ := original.(http.Flusher)
return &responseWriterWrapper{
ResponseWriter: original,
statusCodeWritten: false,
hijacker: hijacker,
flusher: flusher,
}
}
func (rw *responseWriterWrapper) StatusCode() int {
return rw.statusCode
}
func (rw *responseWriterWrapper) WriteHeader(statusCode int) {
rw.statusCode = statusCode
rw.statusCodeWritten = true
rw.ResponseWriter.WriteHeader(statusCode)
}
func (rw *responseWriterWrapper) Write(data []byte) (int, error) {
if !rw.statusCodeWritten {
rw.statusCode = http.StatusOK
}
return rw.ResponseWriter.Write(data)
}
// Using as embedded makes the ResponseWrite be stored as interface and that way
// it loses the access to the implementation for Hijack or Flush
func (rw *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if rw.hijacker == nil {
return nil, nil, errors.New("Hijacker interface not supported by the wrapped ResponseWriter")
}
return rw.hijacker.Hijack()
}
func (rw *responseWriterWrapper) Flush() {
if rw.flusher != nil {
rw.flusher.Flush()
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
b64 "encoding/base64"
"html"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const maxSAMLResponseSize = 2 * 1024 * 1024 // 2MB
func (w *Web) InitSaml() {
w.MainRouter.Handle("/login/sso/saml", w.APIHandler(loginWithSaml)).Methods("GET")
w.MainRouter.Handle("/login/sso/saml", w.APIHandlerTrustRequester(completeSaml)).Methods("POST")
}
func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
samlInterface := c.App.Saml()
if samlInterface == nil {
c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
action := r.URL.Query().Get("action")
isMobile := action == model.OAuthActionMobile
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
relayProps := map[string]string{}
relayState := ""
if action != "" {
relayProps["team_id"] = teamId
relayProps["action"] = action
if action == model.OAuthActionEmailToSSO {
relayProps["email"] = r.URL.Query().Get("email")
}
}
if redirectURL != "" {
if isMobile && !utils.IsValidMobileAuthRedirectURL(c.App.Config(), redirectURL) {
invalidSchemeErr := model.NewAppError("loginWithOAuth", "api.invalid_custom_url_scheme", nil, "", http.StatusBadRequest)
utils.RenderMobileError(c.App.Config(), w, invalidSchemeErr, redirectURL)
return
}
relayProps["redirect_to"] = redirectURL
}
relayProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
if len(relayProps) > 0 {
relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJSON(relayProps)))
}
data, err := samlInterface.BuildRequest(relayState)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
http.Redirect(w, r, data.URL, http.StatusFound)
}
func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
samlInterface := c.App.Saml()
if samlInterface == nil {
c.Err = model.NewAppError("completeSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
return
}
//Validate that the user is with SAML and all that
encodedXML := r.FormValue("SAMLResponse")
relayState := r.FormValue("RelayState")
relayProps := make(map[string]string)
if relayState != "" {
stateStr := ""
b, err := b64.StdEncoding.DecodeString(relayState)
if err != nil {
c.Err = model.NewAppError("completeSaml", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusFound).Wrap(err)
return
}
stateStr = string(b)
relayProps = model.MapFromJSON(strings.NewReader(stateStr))
}
auditRec := c.MakeAuditRecord("completeSaml", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
action := relayProps["action"]
auditRec.AddMeta("action", action)
isMobile := action == model.OAuthActionMobile
redirectURL := ""
hasRedirectURL := false
if val, ok := relayProps["redirect_to"]; ok {
redirectURL = val
hasRedirectURL = val != ""
}
redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL)
handleError := func(err *model.AppError) {
if isMobile && hasRedirectURL {
err.Translate(c.AppContext.T)
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
} else {
c.Err = err
c.Err.StatusCode = http.StatusFound
}
}
if len(encodedXML) > maxSAMLResponseSize {
err := model.NewAppError("completeSaml", "api.user.authorize_oauth_user.saml_response_too_long.app_error", nil, "SAML response is too long", http.StatusBadRequest)
mlog.Error(err.Error())
handleError(err)
return
}
user, err := samlInterface.DoLogin(c.AppContext, encodedXML, relayProps)
if err != nil {
c.LogAudit("fail")
mlog.Error(err.Error())
handleError(err)
return
}
if err = c.App.CheckUserAllAuthenticationCriteria(user, ""); err != nil {
mlog.Error(err.Error())
handleError(err)
return
}
switch action {
case model.OAuthActionSignup:
if teamId := relayProps["team_id"]; teamId != "" {
if err = c.App.AddUserToTeamByTeamId(c.AppContext, teamId, user); err != nil {
c.LogErrorByCode(err)
break
}
c.App.AddDirectChannels(c.AppContext, teamId, user)
}
case model.OAuthActionEmailToSSO:
if err = c.App.RevokeAllSessions(user.Id); err != nil {
c.Err = err
return
}
auditRec.AddMeta("revoked_user_id", user.Id)
auditRec.AddMeta("revoked", "Revoked all sessions for user")
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
c.App.Srv().Go(func() {
if err := c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.UserAuthServiceSaml)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err))
}
})
}
auditRec.AddMeta("obtained_user_id", user.Id)
c.LogAuditWithUserId(user.Id, "obtained user")
err = c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
if err != nil {
mlog.Error(err.Error())
handleError(err)
return
}
auditRec.Success()
c.LogAuditWithUserId(user.Id, "success")
c.App.AttachSessionCookies(c.AppContext, w, r)
if hasRedirectURL {
if isMobile {
// Mobile clients with redirect url support
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
model.SessionCookieToken: c.AppContext.Session().Token,
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
})
utils.RenderMobileAuthComplete(w, redirectURL)
} else {
http.Redirect(w, r, redirectURL, http.StatusFound)
}
return
}
switch action {
// Mobile clients with web view implementation
case model.OAuthActionMobile:
ReturnStatusOK(w)
case model.OAuthActionEmailToSSO:
http.Redirect(w, r, c.GetSiteURLHeader()+"/login?extra=signin_change", http.StatusFound)
default:
http.Redirect(w, r, c.GetSiteURLHeader(), http.StatusFound)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bytes"
"fmt"
"html"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/mattermost/gziphandler"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
var robotsTxt = []byte("User-agent: *\nDisallow: /\n")
func (w *Web) InitStatic() {
if *w.srv.Config().ServiceSettings.WebserverMode != "disabled" {
if err := utils.UpdateAssetsSubpathFromConfig(w.srv.Config()); err != nil {
mlog.Error("Failed to update assets subpath from config", mlog.Err(err))
}
staticDir, _ := fileutils.FindDir(model.ClientDir)
mlog.Debug("Using client directory", mlog.String("clientDir", staticDir))
subpath, _ := utils.GetSubpathFromConfig(w.srv.Config())
staticHandler := staticFilesHandler(http.StripPrefix(path.Join(subpath, "static"), http.FileServer(http.Dir(staticDir))))
pluginHandler := staticFilesHandler(http.StripPrefix(path.Join(subpath, "static", "plugins"), http.FileServer(http.Dir(*w.srv.Config().PluginSettings.ClientDirectory))))
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
staticHandler = gziphandler.GzipHandler(staticHandler)
pluginHandler = gziphandler.GzipHandler(pluginHandler)
}
w.MainRouter.PathPrefix("/static/plugins/").Handler(pluginHandler)
w.MainRouter.PathPrefix("/static/").Handler(staticHandler)
w.MainRouter.Handle("/robots.txt", http.HandlerFunc(robotsHandler))
w.MainRouter.Handle("/unsupported_browser.js", http.HandlerFunc(unsupportedBrowserScriptHandler))
w.MainRouter.Handle("/{anything:.*}", w.NewStaticHandler(root)).Methods("GET")
// When a subpath is defined, it's necessary to handle redirects without a
// trailing slash. We don't want to use StrictSlash on the w.MainRouter and affect
// all routes, just /subpath -> /subpath/.
w.MainRouter.HandleFunc("", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path += "/"
http.Redirect(w, r, r.URL.String(), http.StatusFound)
}))
}
}
func root(c *Context, w http.ResponseWriter, r *http.Request) {
if !CheckClientCompatibility(r.UserAgent()) {
w.Header().Set("Cache-Control", "no-store")
data := renderUnsupportedBrowser(c.AppContext, r)
c.App.Srv().TemplatesContainer().Render(w, "unsupported_browser", data)
return
}
if IsAPICall(c.App, r) {
Handle404(c.App, w, r)
return
}
w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
staticDir, _ := fileutils.FindDir(model.ClientDir)
contents, err := os.ReadFile(filepath.Join(staticDir, "root.html"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
titleTemplate := "<title>%s</title>"
originalHTML := fmt.Sprintf(titleTemplate, html.EscapeString(model.TeamSettingsDefaultSiteName))
modifiedHTML := getOpenGraphMetaTags(c)
if originalHTML != modifiedHTML {
contents = bytes.ReplaceAll(contents, []byte(originalHTML), []byte(modifiedHTML))
}
w.Header().Set("Content-Type", "text/html")
w.Write(contents)
}
func staticFilesHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//wrap our ResponseWriter with our no-cache 404-handler
w = ¬FoundNoCacheResponseWriter{ResponseWriter: w}
if path.Base(r.URL.Path) == "remote_entry.js" {
w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
} else {
w.Header().Set("Cache-Control", "max-age=31556926, public")
}
// Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress
w.Header().Set("Permissions-Policy", "")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
handler.ServeHTTP(w, r)
})
}
type notFoundNoCacheResponseWriter struct {
http.ResponseWriter
}
func (w *notFoundNoCacheResponseWriter) WriteHeader(statusCode int) {
if statusCode == http.StatusNotFound {
// we have a 404, update our cache header first then fall through
w.Header().Set("Cache-Control", "no-cache, public")
}
w.ResponseWriter.WriteHeader(statusCode)
}
func robotsHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
w.Write(robotsTxt)
}
func unsupportedBrowserScriptHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
templatesDir, _ := templates.GetTemplateDirectory()
http.ServeFile(w, r, filepath.Join(templatesDir, "unsupported_browser.js"))
}
func getOpenGraphMetaTags(c *Context) string {
siteName := model.TeamSettingsDefaultSiteName
customSiteName := c.App.Srv().Config().TeamSettings.SiteName
if customSiteName != nil && *customSiteName != "" {
siteName = *customSiteName
}
siteDescription := model.TeamSettingsDefaultCustomDescriptionText
customSiteDescription := c.App.Srv().Config().TeamSettings.CustomDescriptionText
if customSiteDescription != nil && *customSiteDescription != "" {
siteDescription = *customSiteDescription
}
titleTemplate := "<title>%s</title>"
titleHTML := fmt.Sprintf(titleTemplate, html.EscapeString(siteName))
descriptionHTML := ""
if siteDescription != "" {
descriptionTemplate := "<meta property=\"og:description\" content=\"%s\" />"
descriptionHTML = fmt.Sprintf(descriptionTemplate, html.EscapeString(siteDescription))
}
return titleHTML + descriptionHTML
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// MattermostApp describes downloads for the Mattermost App
type MattermostApp struct {
LogoSrc string
Title string
SupportedVersionString string
Label string
Link string
InstallGuide string
InstallGuideLink string
}
// Browser describes a browser with a download link
type Browser struct {
LogoSrc string
Title string
SupportedVersionString string
Src string
GetLatestString string
}
// SystemBrowser describes a browser but includes 2 links: one to open the local browser, and one to make it default
type SystemBrowser struct {
LogoSrc string
Title string
SupportedVersionString string
LabelOpen string
LinkOpen string
LinkMakeDefault string
OrString string
MakeDefaultString string
}
func renderUnsupportedBrowser(ctx *request.Context, r *http.Request) templates.Data {
data := templates.Data{
Props: map[string]any{
"DownloadAppOrUpgradeBrowserString": ctx.T("web.error.unsupported_browser.download_app_or_upgrade_browser"),
"LearnMoreString": ctx.T("web.error.unsupported_browser.learn_more"),
},
}
// User Agent info
ua := uasurfer.Parse(r.UserAgent())
isWindows := ua.OS.Platform.String() == "PlatformWindows"
isWindows10 := isWindows && ua.OS.Version.Major == 10
isMacOSX := ua.OS.Name.String() == "OSMacOSX" && ua.OS.Version.Major == 10
isSafari := ua.Browser.Name.String() == "BrowserSafari"
// Basic heading translations
if isSafari {
data.Props["NoLongerSupportString"] = ctx.T("web.error.unsupported_browser.no_longer_support_version")
} else {
data.Props["NoLongerSupportString"] = ctx.T("web.error.unsupported_browser.no_longer_support")
}
// Mattermost app version
if isWindows {
data.Props["App"] = renderMattermostAppWindows(ctx)
} else if isMacOSX {
data.Props["App"] = renderMattermostAppMac(ctx)
}
// Browsers to download
// Show a link to Safari if you're using safari and it's outdated
// Can't show on Mac all the time because there's no way to open it via URI
browsers := []Browser{renderBrowserChrome(ctx), renderBrowserFirefox(ctx)}
if isSafari {
browsers = append(browsers, renderBrowserSafari(ctx))
}
data.Props["Browsers"] = browsers
// If on Windows 10, show link to Edge
if isWindows10 {
data.Props["SystemBrowser"] = renderSystemBrowserEdge(ctx, r)
}
return data
}
func renderMattermostAppMac(ctx *request.Context) MattermostApp {
return MattermostApp{
"/static/images/browser-icons/mac.png",
ctx.T("web.error.unsupported_browser.download_the_app"),
ctx.T("web.error.unsupported_browser.min_os_version.mac"),
ctx.T("web.error.unsupported_browser.download"),
"https://mattermost.com/download/#mattermostApps",
ctx.T("web.error.unsupported_browser.install_guide.mac"),
"https://docs.mattermost.com/install/desktop.html#mac-os-x-10-9",
}
}
func renderMattermostAppWindows(ctx *request.Context) MattermostApp {
return MattermostApp{
"/static/images/browser-icons/windows.svg",
ctx.T("web.error.unsupported_browser.download_the_app"),
ctx.T("web.error.unsupported_browser.min_os_version.windows"),
ctx.T("web.error.unsupported_browser.download"),
"https://mattermost.com/download/#mattermostApps",
ctx.T("web.error.unsupported_browser.install_guide.windows"),
"https://docs.mattermost.com/install/desktop.html#windows-10-windows-8-1-windows-7",
}
}
func renderBrowserChrome(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/chrome.svg",
ctx.T("web.error.unsupported_browser.browser_title.chrome"),
ctx.T("web.error.unsupported_browser.min_browser_version.chrome"),
"http://www.google.com/chrome",
ctx.T("web.error.unsupported_browser.browser_get_latest.chrome"),
}
}
func renderBrowserFirefox(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/firefox.svg",
ctx.T("web.error.unsupported_browser.browser_title.firefox"),
ctx.T("web.error.unsupported_browser.min_browser_version.firefox"),
"https://www.mozilla.org/firefox/new/",
ctx.T("web.error.unsupported_browser.browser_get_latest.firefox"),
}
}
func renderBrowserSafari(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/safari.svg",
ctx.T("web.error.unsupported_browser.browser_title.safari"),
ctx.T("web.error.unsupported_browser.min_browser_version.safari"),
"macappstore://showUpdatesPage",
ctx.T("web.error.unsupported_browser.browser_get_latest.safari"),
}
}
func renderSystemBrowserEdge(ctx *request.Context, r *http.Request) SystemBrowser {
return SystemBrowser{
"/static/images/browser-icons/edge.svg",
ctx.T("web.error.unsupported_browser.browser_title.edge"),
ctx.T("web.error.unsupported_browser.min_browser_version.edge"),
ctx.T("web.error.unsupported_browser.open_system_browser.edge"),
"microsoft-edge:http://" + r.Host + r.RequestURI, //TODO: Can we get HTTP or HTTPS? If someone's server doesn't have a redirect this won't work
"ms-settings:defaultapps",
ctx.T("web.error.unsupported_browser.system_browser_or"),
ctx.T("web.error.unsupported_browser.system_browser_make_default"),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"path"
"strings"
"github.com/avct/uasurfer"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Web struct {
srv *app.Server
MainRouter *mux.Router
}
func New(srv *app.Server) *Web {
mlog.Debug("Initializing web routes")
web := &Web{
srv: srv,
MainRouter: srv.Router,
}
web.InitOAuth()
web.InitWebhooks()
web.InitSaml()
web.InitStatic()
return web
}
// Due to the complexities of UA detection and the ramifications of a misdetection
// only older Safari and IE browsers throw incompatibility errors.
// Map should be of minimum required browser version.
// -1 means that the browser is not supported in any version.
var browserMinimumSupported = map[string]int{
"BrowserIE": 12,
"BrowserSafari": 12,
}
func CheckClientCompatibility(agentString string) bool {
ua := uasurfer.Parse(agentString)
if version, exist := browserMinimumSupported[ua.Browser.Name.String()]; exist && (ua.Browser.Version.Major < version || version < 0) {
return false
}
return true
}
func Handle404(a app.AppIface, w http.ResponseWriter, r *http.Request) {
err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
mlog.Debug("not found handler triggered", mlog.String("path", r.URL.Path), mlog.Int("code", 404), mlog.String("ip", ipAddress))
if IsAPICall(a, r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(err.StatusCode)
err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'. Typo? are you missing a team_id or user_id as part of the url?"
w.Write([]byte(err.ToJSON()))
} else if *a.Config().ServiceSettings.WebserverMode == "disabled" {
http.NotFound(w, r)
} else {
utils.RenderWebAppError(a.Config(), w, r, err, a.AsymmetricSigningKey())
}
}
func IsAPICall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "api")+"/")
}
func IsWebhookCall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "hooks")+"/")
}
func IsOAuthAPICall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
if r.Method == "POST" && r.URL.Path == path.Join(subpath, "oauth", "authorize") {
return true
}
if r.URL.Path == path.Join(subpath, "oauth", "apps", "authorized") ||
r.URL.Path == path.Join(subpath, "oauth", "deauthorize") ||
r.URL.Path == path.Join(subpath, "oauth", "access_token") {
return true
}
return false
}
func ReturnStatusOK(w http.ResponseWriter) {
m := make(map[string]string)
m[model.STATUS] = model.StatusOk
w.Write([]byte(model.MapToJSON(m)))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"encoding/json"
"io"
"mime"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (w *Web) InitWebhooks() {
w.MainRouter.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", w.APIHandlerTrustRequester(commandWebhook)).Methods("POST")
w.MainRouter.Handle("/hooks/{id:[A-Za-z0-9]+}", w.APIHandlerTrustRequester(incomingWebhook)).Methods("POST")
}
func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
r.ParseForm()
var err *model.AppError
var mediaType string
incomingWebhookPayload := &model.IncomingWebhookRequest{}
contentType := r.Header.Get("Content-Type")
// Content-Type header is optional so could be empty
if contentType != "" {
var mimeErr error
mediaType, _, mimeErr = mime.ParseMediaType(contentType)
if mimeErr != nil && mimeErr != mime.ErrInvalidMediaParameter {
c.Err = model.NewAppError("incomingWebhook",
"api.webhook.incoming.error",
nil,
"webhook_id="+id+", error: "+mimeErr.Error(),
http.StatusBadRequest,
)
return
}
}
defer func() {
if *c.App.Config().LogSettings.EnableWebhookDebugging {
if c.Err != nil {
fields := []mlog.Field{mlog.String("webhook_id", id), mlog.String("request_id", c.AppContext.RequestId())}
payload, err := json.Marshal(incomingWebhookPayload)
if err != nil {
fields = append(fields, mlog.NamedErr("encoding_err", err))
} else {
fields = append(fields, mlog.String("payload", string(payload)))
}
mlog.Debug("Incoming webhook received", fields...)
}
}
}()
if mediaType == "application/x-www-form-urlencoded" {
payload := strings.NewReader(r.FormValue("payload"))
incomingWebhookPayload, err = decodePayload(payload)
if err != nil {
c.Err = err
return
}
} else if mediaType == "multipart/form-data" {
r.ParseMultipartForm(0)
decoder := schema.NewDecoder()
err := decoder.Decode(incomingWebhookPayload, r.PostForm)
if err != nil {
c.Err = model.NewAppError("incomingWebhook",
"api.webhook.incoming.error",
nil,
"webhook_id="+id+", error: "+err.Error(),
http.StatusBadRequest,
)
return
}
} else {
incomingWebhookPayload, err = decodePayload(r.Body)
if err != nil {
c.Err = err
return
}
}
err = c.App.HandleIncomingWebhook(c.AppContext, id, incomingWebhookPayload)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
if err != nil {
c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr := c.App.HandleCommandWebhook(c.AppContext, id, response)
if appErr != nil {
c.Err = appErr
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) {
incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJSON(payload)
if decodeError != nil {
return nil, decodeError
}
return incomingWebhookPayload, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
type API struct {
App *app.App
Router *platform.WebSocketRouter
}
func Init(s *app.Server) {
a := app.New(app.ServerConnector(s.Channels()))
router := s.Platform().WebSocketRouter
api := &API{
App: a,
Router: router,
}
api.InitUser()
api.InitSystem()
api.InitStatus()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitStatus() {
api.Router.Handle("get_statuses", api.APIWebSocketHandler(api.getStatuses))
api.Router.Handle("get_statuses_by_ids", api.APIWebSocketHandler(api.getStatusesByIds))
}
func (api *API) getStatuses(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
statusMap := api.App.Srv().Platform().GetAllStatuses()
return model.StatusMapToInterfaceMap(statusMap), nil
}
func (api *API) getStatusesByIds(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
var userIds []string
if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 {
mlog.Debug("Error while parsing user_ids", mlog.String("data", model.StringInterfaceToJSON(req.Data)))
return nil, NewInvalidWebSocketParamError(req.Action, "user_ids")
}
statusMap, err := api.App.Srv().Platform().GetStatusesByIds(userIds)
if err != nil {
return nil, err
}
return statusMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitSystem() {
api.Router.Handle("ping", api.APIWebSocketHandler(ping))
}
func ping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
data := map[string]any{}
data["text"] = "pong"
data["version"] = model.CurrentVersion
data["server_time"] = model.GetMillis()
data["node_id"] = ""
return data, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
)
func (api *API) InitUser() {
api.Router.Handle("user_typing", api.APIWebSocketHandler(api.userTyping))
api.Router.Handle("user_update_active_status", api.APIWebSocketHandler(api.userUpdateActiveStatus))
}
func (api *API) userTyping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
api.App.ExtendSessionExpiryIfNeeded(&req.Session)
if api.App.Srv().Platform().Busy.IsBusy() {
// this is considered a non-critical service and will be disabled when server busy.
return nil, NewServerBusyWebSocketError(req.Action)
}
var ok bool
var channelId string
if channelId, ok = req.Data["channel_id"].(string); !ok || !model.IsValidId(channelId) {
return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
}
if !api.App.SessionHasPermissionToChannel(request.EmptyContext(api.App.Log()), req.Session, channelId, model.PermissionCreatePost) {
return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
}
var parentId string
if parentId, ok = req.Data["parent_id"].(string); !ok {
parentId = ""
}
appErr := api.App.PublishUserTyping(req.Session.UserId, channelId, parentId)
return nil, appErr
}
func (api *API) userUpdateActiveStatus(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
var ok bool
var userIsActive bool
if userIsActive, ok = req.Data["user_is_active"].(bool); !ok {
return nil, NewInvalidWebSocketParamError(req.Action, "user_is_active")
}
var manual bool
if manual, ok = req.Data["manual"].(bool); !ok {
manual = false
}
if userIsActive {
api.App.SetStatusOnline(req.Session.UserId, manual)
} else {
api.App.SetStatusAwayIfNeeded(req.Session.UserId, manual)
}
return nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) APIWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]any, *model.AppError)) webSocketHandler {
return webSocketHandler{api.App, wh}
}
type webSocketHandler struct {
app *app.App
handlerFunc func(*model.WebSocketRequest) (map[string]any, *model.AppError)
}
func (wh webSocketHandler) ServeWebSocket(conn *platform.WebConn, r *model.WebSocketRequest) {
mlog.Debug("Websocket request", mlog.String("action", r.Action))
hub := wh.app.Srv().Platform().GetHubForUserId(conn.UserId)
if hub == nil {
return
}
session, sessionErr := wh.app.GetSession(conn.GetSessionToken())
defer wh.app.ReturnSessionToPool(session)
if sessionErr != nil {
mlog.Error(
"websocket session error",
mlog.String("action", r.Action),
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("error_message", sessionErr.SystemMessage(i18n.T)),
mlog.Err(sessionErr),
)
sessionErr.DetailedError = ""
errResp := model.NewWebSocketError(r.Seq, sessionErr)
hub.SendMessage(conn, errResp)
return
}
r.Session = *session
r.T = conn.T
r.Locale = conn.Locale
var data map[string]any
var err *model.AppError
if data, err = wh.handlerFunc(r); err != nil {
mlog.Error(
"websocket request handling error",
mlog.String("action", r.Action),
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("error_message", err.SystemMessage(i18n.T)),
mlog.Err(err),
)
err.DetailedError = ""
errResp := model.NewWebSocketError(r.Seq, err)
hub.SendMessage(conn, errResp)
return
}
resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, data)
hub.SendMessage(conn, resp)
}
func NewInvalidWebSocketParamError(action string, name string) *model.AppError {
return model.NewAppError("websocket: "+action, "api.websocket_handler.invalid_param.app_error", map[string]any{"Name": name}, "", http.StatusBadRequest)
}
func NewServerBusyWebSocketError(action string) *model.AppError {
return model.NewAppError("websocket: "+action, "api.websocket_handler.server_busy.app_error", nil, "", http.StatusServiceUnavailable)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
var coverprofileCounters map[string]int = make(map[string]int)
var mainHelper *testlib.MainHelper
type testHelper struct {
*api4.TestHelper
config *model.Config
tempDir string
configFilePath string
disableAutoConfig bool
}
// Setup creates an instance of testHelper.
func Setup(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.Setup(t)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// Setup creates an instance of testHelper.
func SetupWithStoreMock(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.SetupWithStoreMock(t)
systemStore := mocks.SystemStore{}
systemStore.On("Get").Return(make(model.StringMap), nil)
licenseStore := mocks.LicenseStore{}
licenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("System").Return(&systemStore)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("License").Return(&licenseStore)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// InitBasic simply proxies to api4.InitBasic, while still returning a testHelper.
func (h *testHelper) InitBasic() *testHelper {
h.TestHelper.InitBasic()
return h
}
// TemporaryDirectory returns the temporary directory created for user by the test helper.
func (h *testHelper) TemporaryDirectory() string {
return h.tempDir
}
// Config returns the configuration passed to a running command.
func (h *testHelper) Config() *model.Config {
return h.config.Clone()
}
// ConfigPath returns the path to the temporary config file passed to a running command.
func (h *testHelper) ConfigPath() string {
return h.configFilePath
}
// SetConfig replaces the configuration passed to a running command.
func (h *testHelper) SetConfig(config *model.Config) {
if !testing.Short() {
config.SqlSettings = *mainHelper.GetSQLSettings()
}
// Disable strict password requirements for test
*config.PasswordSettings.MinimumLength = 5
*config.PasswordSettings.Lowercase = false
*config.PasswordSettings.Uppercase = false
*config.PasswordSettings.Symbol = false
*config.PasswordSettings.Number = false
h.config = config
buf, err := json.Marshal(config)
if err != nil {
panic("failed to marshal config: " + err.Error())
}
if err := os.WriteFile(h.configFilePath, buf, 0600); err != nil {
panic("failed to write file " + h.configFilePath + ": " + err.Error())
}
}
// SetAutoConfig configures whether the --config flag is automatically passed to a running command.
func (h *testHelper) SetAutoConfig(autoConfig bool) {
h.disableAutoConfig = !autoConfig
}
// TearDown cleans up temporary files and assets created during the life of the test helper.
func (h *testHelper) TearDown() {
h.TestHelper.TearDown()
os.RemoveAll(h.tempDir)
}
func (h *testHelper) execArgs(t *testing.T, args []string) []string {
ret := []string{"-test.v", "-test.run", "ExecCommand"}
if coverprofile := flag.Lookup("test.coverprofile").Value.String(); coverprofile != "" {
dir := filepath.Dir(coverprofile)
base := filepath.Base(coverprofile)
baseParts := strings.SplitN(base, ".", 2)
name := strings.Replace(t.Name(), "/", "_", -1)
coverprofileCounters[name] = coverprofileCounters[name] + 1
baseParts[0] = fmt.Sprintf("%v-%v-%v", baseParts[0], name, coverprofileCounters[name])
ret = append(ret, "-test.coverprofile", filepath.Join(dir, strings.Join(baseParts, ".")))
}
ret = append(ret, "--")
// Unless the test passes a `--config` of its own, create a temporary one from the default
// configuration with the current test database applied.
hasConfig := h.disableAutoConfig
for _, arg := range args {
if arg == "--config" {
hasConfig = true
break
}
}
if !hasConfig {
ret = append(ret, "--config", h.configFilePath)
}
ret = append(ret, args...)
return ret
}
func (h *testHelper) cmd(t *testing.T, args []string) *exec.Cmd {
path, err := os.Executable()
require.NoError(t, err)
cmd := exec.Command(path, h.execArgs(t, args)...)
cmd.Env = []string{}
for _, env := range os.Environ() {
// Ignore MM_SQLSETTINGS_DATASOURCE from the environment, since we override.
if strings.HasPrefix(env, "MM_SQLSETTINGS_DATASOURCE=") {
continue
}
cmd.Env = append(cmd.Env, env)
}
return cmd
}
// CheckCommand invokes the test binary, returning the output modified for assertion testing.
func (h *testHelper) CheckCommand(t *testing.T, args ...string) string {
output, err := h.cmd(t, args).CombinedOutput()
require.NoError(t, err, string(output))
return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS"))
}
// RunCommand invokes the test binary, returning only any error.
func (h *testHelper) RunCommand(t *testing.T, args ...string) error {
return h.cmd(t, args).Run()
}
// RunCommandWithOutput is a variant of RunCommand that returns the unmodified output and any error.
func (h *testHelper) RunCommandWithOutput(t *testing.T, args ...string) (string, error) {
cmd := h.cmd(t, args)
var buf bytes.Buffer
reader, writer := io.Pipe()
cmd.Stdout = writer
cmd.Stderr = writer
done := make(chan bool)
go func() {
io.Copy(&buf, reader)
close(done)
}()
err := cmd.Run()
writer.Close()
<-done
return buf.String(), err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/config"
)
var DbCmd = &cobra.Command{
Use: "db",
Short: "Commands related to the database",
}
var InitDbCmd = &cobra.Command{
Use: "init",
Short: "Initialize the database",
Long: `Initialize the database for a given DSN, executing the migrations and loading the custom defaults if any.
This command should be run using a database configuration DSN.`,
Example: ` # you can use the config flag to pass the DSN
$ mattermost db init --config postgres://localhost/mattermost
# or you can use the MM_CONFIG environment variable
$ MM_CONFIG=postgres://localhost/mattermost mattermost db init
# and you can set a custom defaults file to be loaded into the database
$ MM_CUSTOM_DEFAULTS_PATH=custom.json MM_CONFIG=postgres://localhost/mattermost mattermost db init`,
Args: cobra.NoArgs,
RunE: initDbCmdF,
}
var ResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the database to initial state",
Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.",
RunE: resetCmdF,
}
var MigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate the database if there are any unapplied migrations",
Long: "Run the missing migrations from the migrations table.",
RunE: migrateCmdF,
}
var DBVersionCmd = &cobra.Command{
Use: "version",
Short: "Returns the recent applied version number",
RunE: dbVersionCmdF,
}
func init() {
ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.")
DBVersionCmd.Flags().Bool("all", false, "Returns all applied migrations")
DbCmd.AddCommand(
InitDbCmd,
ResetCmd,
MigrateCmd,
DBVersionCmd,
)
RootCmd.AddCommand(
DbCmd,
)
}
func initDbCmdF(command *cobra.Command, _ []string) error {
dsn := getConfigDSN(command, config.GetEnvironment())
if !config.IsDatabaseDSN(dsn) {
return errors.New("this command should be run using a database configuration DSN")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
return errors.Wrap(err, "error loading custom configuration defaults")
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
sqlStore := sqlstore.New(configStore.Get().SqlSettings, nil)
defer sqlStore.Close()
fmt.Println("Database store correctly initialised")
return nil
}
func resetCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
}
a.Srv().Store().DropAllTables()
CommandPrettyPrintln("Database successfully reset")
auditRec := a.MakeAuditRecord("reset", audit.Success)
a.LogAuditRec(auditRec, nil)
return nil
}
func migrateCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
CommandPrettyPrintln("Database successfully migrated")
return nil
}
func dbVersionCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
allFlag, _ := command.Flags().GetBool("all")
if allFlag {
applied, err2 := store.GetAppliedMigrations()
if err2 != nil {
return errors.Wrap(err2, "failed to get applied migrations")
}
for _, migration := range applied {
CommandPrettyPrintln(fmt.Sprintf("Varsion: %d, Name: %s", migration.Version, migration.Name))
}
return nil
}
v, err := store.GetDBSchemaVersion()
if err != nil {
return errors.Wrap(err, "failed to get schema version")
}
CommandPrettyPrintln("Current database schema version is: " + strconv.Itoa(v))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var ExportCmd = &cobra.Command{
Use: "export",
Short: "Export data from Mattermost",
Long: "Export data from Mattermost in a format suitable for import into a third-party application or another Mattermost instance",
}
var ScheduleExportCmd = &cobra.Command{
Use: "schedule",
Short: "Schedule an export data job in Mattermost",
Long: "Schedule an export data job in Mattermost (this will run asynchronously via a background worker)",
Example: "export schedule --format=actiance --exportFrom=12345 --timeoutSeconds=12345",
RunE: scheduleExportCmdF,
}
var CsvExportCmd = &cobra.Command{
Use: "csv",
Short: "Export data from Mattermost in CSV format",
Long: "Export data from Mattermost in CSV format",
Example: "export csv --exportFrom=12345",
RunE: buildExportCmdF("csv"),
}
var ActianceExportCmd = &cobra.Command{
Use: "actiance",
Short: "Export data from Mattermost in Actiance format",
Long: "Export data from Mattermost in Actiance format",
Example: "export actiance --exportFrom=12345",
RunE: buildExportCmdF("actiance"),
}
var GlobalRelayZipExportCmd = &cobra.Command{
Use: "global-relay-zip",
Short: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only.",
Long: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only. This does not archive any information in Global Relay.",
Example: "export global-relay-zip --exportFrom=12345",
RunE: buildExportCmdF("globalrelay-zip"),
}
var BulkExportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Export bulk data.",
Long: "Export data to a file compatible with the Mattermost Bulk Import format.",
Example: "export bulk bulk_data.json",
RunE: bulkExportCmdF,
Args: cobra.ExactArgs(1),
}
func init() {
ScheduleExportCmd.Flags().String("format", "actiance", "The format to export data")
ScheduleExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ScheduleExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.")
CsvExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
CsvExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
ActianceExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ActianceExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
GlobalRelayZipExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
GlobalRelayZipExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
BulkExportCmd.Flags().Bool("all-teams", true, "Export all teams from the server.")
BulkExportCmd.Flags().Bool("attachments", false, "Also export file attachments.")
BulkExportCmd.Flags().Bool("archive", false, "Outputs a single archive file.")
ExportCmd.AddCommand(ScheduleExportCmd)
ExportCmd.AddCommand(CsvExportCmd)
ExportCmd.AddCommand(ActianceExportCmd)
ExportCmd.AddCommand(GlobalRelayZipExportCmd)
ExportCmd.AddCommand(BulkExportCmd)
RootCmd.AddCommand(ExportCmd)
}
func scheduleExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
if !*a.Config().MessageExportSettings.EnableExport {
return errors.New("ERROR: The message export feature is not enabled")
}
// for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data
format, err := command.Flags().GetString("format")
if err != nil {
return errors.New("format flag error")
}
if format != "actiance" {
return errors.New("unsupported export format")
}
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds")
if err != nil {
return errors.New("timeoutSeconds error")
}
if timeoutSeconds < 0 {
return errors.New("timeoutSeconds must be a positive integer")
}
if messageExportI := a.MessageExport(); messageExportI != nil {
ctx := context.Background()
if timeoutSeconds > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeoutSeconds))
defer cancel()
}
job, err := messageExportI.StartSynchronizeJob(ctx, startTime)
if err != nil || job.Status == model.JobStatusError || job.Status == model.JobStatusCanceled {
CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs")
} else {
CommandPrettyPrintln("SUCCESS: Message export job complete")
auditRec := a.MakeAuditRecord("scheduleExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
}
}
return nil
}
func buildExportCmdF(format string) func(command *cobra.Command, args []string) error {
return func(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
license := a.Srv().License()
if err != nil {
return err
}
defer a.Srv().Shutdown()
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
limit, err := command.Flags().GetInt("limit")
if err != nil {
return errors.New("limit flag error")
}
if a.MessageExport() == nil || license == nil || !*license.Features.MessageExport {
return errors.New("message export feature not available")
}
warningsCount, appErr := a.MessageExport().RunExport(format, startTime, limit)
if appErr != nil {
return appErr
}
if warningsCount == 0 {
CommandPrettyPrintln("SUCCESS: Your data was exported.")
} else {
if format == model.ComplianceExportTypeGlobalrelay || format == model.ComplianceExportTypeGlobalrelayZip {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see logs for details.", warningsCount))
} else {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see warning.txt for details.", warningsCount))
}
}
auditRec := a.MakeAuditRecord("buildExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
return nil
}
}
func bulkExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
allTeams, err := command.Flags().GetBool("all-teams")
if err != nil {
return errors.Wrap(err, "all-teams flag error")
}
if !allTeams {
return errors.New("Nothing to export. Please specify the --all-teams flag to export all teams.")
}
attachments, err := command.Flags().GetBool("attachments")
if err != nil {
return errors.Wrap(err, "attachments flag error")
}
archive, err := command.Flags().GetBool("archive")
if err != nil {
return errors.Wrap(err, "archive flag error")
}
fileWriter, err := os.Create(args[0])
if err != nil {
return err
}
defer fileWriter.Close()
outPath, err := filepath.Abs(args[0])
if err != nil {
return err
}
var opts model.BulkExportOpts
opts.IncludeAttachments = attachments
opts.CreateArchive = archive
if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), nil /* nil job since it's spawned from CLI */, opts); err != nil {
CommandPrintErrorln(err.Error())
return err
}
auditRec := a.MakeAuditRecord("bulkExport", audit.Success)
auditRec.AddMeta("all_teams", allTeams)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
var ImportCmd = &cobra.Command{
Use: "import",
Short: "Import data.",
}
var SlackImportCmd = &cobra.Command{
Use: "slack [team] [file]",
Short: "Import a team from Slack.",
Long: "Import a team from a Slack export zip file.",
Example: " import slack myteam slack_export.zip",
RunE: slackImportCmdF,
}
var BulkImportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Import bulk data.",
Long: "Import data from a Mattermost Bulk Import File.",
Example: " import bulk bulk_data.json",
RunE: bulkImportCmdF,
}
func init() {
BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
BulkImportCmd.Flags().String("import-path", "", "A path to the data directory to import files from.")
ImportCmd.AddCommand(
BulkImportCmd,
SlackImportCmd,
)
RootCmd.AddCommand(ImportCmd)
}
func slackImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
if len(args) != 2 {
return errors.New("Incorrect number of arguments.")
}
team := getTeamFromTeamArg(a, args[0])
if team == nil {
return errors.New("Unable to find team '" + args[0] + "'")
}
fileReader, err := os.Open(args[1])
if err != nil {
return err
}
defer fileReader.Close()
fileInfo, err := fileReader.Stat()
if err != nil {
return err
}
CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
importErr, log := a.SlackImport(request.EmptyContext(a.Log()), fileReader, fileInfo.Size(), team.Id)
if importErr != nil {
return err
}
CommandPrettyPrintln("")
CommandPrintln(log.String())
CommandPrettyPrintln("")
CommandPrettyPrintln("Finished Slack Import.")
CommandPrettyPrintln("")
auditRec := a.MakeAuditRecord("slackImport", audit.Success)
auditRec.AddMeta("team", team)
auditRec.AddMeta("file", args[1])
a.LogAuditRec(auditRec, nil)
return nil
}
func bulkImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
apply, err := command.Flags().GetBool("apply")
if err != nil {
return errors.New("Apply flag error")
}
validate, err := command.Flags().GetBool("validate")
if err != nil {
return errors.New("Validate flag error")
}
workers, err := command.Flags().GetInt("workers")
if err != nil {
return errors.New("Workers flag error")
}
importPath, err := command.Flags().GetString("import-path")
if err != nil {
return errors.New("import-path flag error")
}
if len(args) != 1 {
return errors.New("Incorrect number of arguments.")
}
fileReader, err := os.Open(args[0])
if err != nil {
return err
}
defer fileReader.Close()
if apply && validate {
CommandPrettyPrintln("Use only one of --apply or --validate.")
return nil
}
if apply && !validate {
CommandPrettyPrintln("Running Bulk Import. This may take a long time.")
} else {
CommandPrettyPrintln("Running Bulk Import Data Validation.")
CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **")
CommandPrettyPrintln("Use the --apply flag to perform the actual data import.")
}
CommandPrettyPrintln("")
if err, lineNumber := a.BulkImportWithPath(request.EmptyContext(a.Log()), fileReader, nil, !apply, workers, importPath); err != nil {
CommandPrintErrorln(err.Error())
if lineNumber != 0 {
CommandPrintErrorln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
}
return err
}
if apply {
CommandPrettyPrintln("Finished Bulk Import.")
auditRec := a.MakeAuditRecord("bulkImport", audit.Success)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
} else {
CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.")
}
return nil
}
func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team {
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamArg)
if err != nil {
var t *model.Team
if t, err = a.Srv().Store().Team().Get(teamArg); err == nil {
team = t
}
}
return team
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), readOnlyConfigStore, options...)
if err != nil {
// Returning an error just prints the usage message, so actually panic
panic(err)
}
a.InitPlugins(request.EmptyContext(a.Log()), *a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
a.DoAppMigrations()
return a, nil
}
func InitDBCommandContextCobra(command *cobra.Command, options ...app.Option) (*app.App, error) {
return initDBCommandContextCobra(command, true, options...)
}
func initDBCommandContext(configDSN string, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
if err := utils.TranslationsPreInit(); err != nil {
return nil, err
}
model.AppErrorInit(i18n.T)
// The option order is important as app.Config option reads app.StartMetrics option.
options = append(options, app.Config(configDSN, readOnlyConfigStore, nil))
s, err := app.NewServer(options...)
if err != nil {
return nil, err
}
a := app.New(app.ServerConnector(s.Channels()))
if model.BuildEnterpriseReady == "true" {
a.Srv().LoadLicense()
}
return a, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var JobserverCmd = &cobra.Command{
Use: "jobserver",
Short: "Start the Mattermost job server",
RunE: jobserverCmdF,
}
func init() {
JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.")
JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.")
RootCmd.AddCommand(JobserverCmd)
}
func jobserverCmdF(command *cobra.Command, args []string) error {
// Options
noJobs, _ := command.Flags().GetBool("nojobs")
noSchedule, _ := command.Flags().GetBool("noschedule")
// Initialize
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), false, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
a.Srv().LoadLicense()
// Run jobs
mlog.Info("Starting Mattermost job server")
defer mlog.Info("Stopped Mattermost job server")
if !noJobs {
a.Srv().Jobs.StartWorkers()
defer a.Srv().Jobs.StopWorkers()
}
if !noSchedule {
a.Srv().Jobs.StartSchedulers()
defer a.Srv().Jobs.StopSchedulers()
}
if !noJobs || !noSchedule {
auditRec := a.MakeAuditRecord("jobServer", audit.Success)
a.LogAuditRec(auditRec, nil)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-signalChan
// Cleanup anything that isn't handled by a defer statement
mlog.Info("Stopping Mattermost job server")
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"os"
)
func CommandPrintln(a ...any) (int, error) {
return fmt.Println(a...)
}
func CommandPrintErrorln(a ...any) (int, error) {
return fmt.Fprintln(os.Stderr, a...)
}
func CommandPrettyPrintln(a ...any) (int, error) {
return fmt.Fprintln(os.Stdout, a...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
)
type Command = cobra.Command
func Run(args []string) error {
RootCmd.SetArgs(args)
return RootCmd.Execute()
}
var RootCmd = &cobra.Command{
Use: "mattermost",
Short: "Open source, self-hosted Slack-alternative",
Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
}
func init() {
RootCmd.PersistentFlags().StringP("config", "c", "", "Configuration file to use.")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"net"
"os"
"os/signal"
"runtime/debug"
"runtime/pprof"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/manualtesting"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run the Mattermost server",
RunE: serverCmdF,
SilenceUsage: true,
}
func init() {
RootCmd.AddCommand(serverCmd)
RootCmd.RunE = serverCmdF
}
func serverCmdF(command *cobra.Command, args []string) error {
interruptChan := make(chan os.Signal, 1)
if err := utils.TranslationsPreInit(); err != nil {
return errors.Wrap(err, "unable to load Mattermost translation files")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
mlog.Warn("Error loading custom configuration defaults: " + err.Error())
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
return runServer(configStore, interruptChan)
}
func runServer(configStore *config.Store, interruptChan chan os.Signal) error {
// Setting the highest traceback level from the code.
// This is done to print goroutines from all threads (see golang.org/issue/13161)
// and also preserve a crash dump for later investigation.
debug.SetTraceback("crash")
options := []app.Option{
// The option order is important as app.Config option reads app.StartMetrics option.
app.StartMetrics,
app.ConfigStore(configStore),
app.RunEssentialJobs,
app.JoinCluster,
}
server, err := app.NewServer(options...)
if err != nil {
mlog.Error(err.Error())
return err
}
defer server.Shutdown()
// We add this after shutdown so that it can be called
// before server shutdown happens as it can close
// the advanced logger and prevent the mlog call from working properly.
defer func() {
// A panic pass-through layer which just logs it
// and sends it upwards.
if x := recover(); x != nil {
var buf bytes.Buffer
pprof.Lookup("goroutine").WriteTo(&buf, 2)
mlog.Error("A panic occurred",
mlog.Any("error", x),
mlog.String("stack", buf.String()))
panic(x)
}
}()
api, err := api4.Init(server)
if err != nil {
mlog.Error(err.Error())
return err
}
wsapi.Init(server)
web.New(server)
err = server.Start()
if err != nil {
mlog.Error(err.Error())
return err
}
// If we allow testing then listen for manual testing URL hits
if *server.Config().ServiceSettings.EnableTesting {
manualtesting.Init(api)
}
notifyReady()
// wait for kill signal before attempting to gracefully shutdown
// the running service
signal.Notify(interruptChan, syscall.SIGINT, syscall.SIGTERM)
<-interruptChan
return nil
}
func notifyReady() {
// If the environment vars provide a systemd notification socket,
// notify systemd that the server is ready.
systemdSocket := os.Getenv("NOTIFY_SOCKET")
if systemdSocket != "" {
mlog.Info("Sending systemd READY notification.")
err := sendSystemdReadyNotification(systemdSocket)
if err != nil {
mlog.Error(err.Error())
}
}
}
func sendSystemdReadyNotification(socketPath string) error {
msg := "READY=1"
addr := &net.UnixAddr{
Name: socketPath,
Net: "unixgram",
}
conn, err := net.DialUnix(addr.Net, nil, addr)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write([]byte(msg))
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
var TestCmd = &cobra.Command{
Use: "test",
Short: "Testing Commands",
Hidden: true,
}
var RunWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests",
Short: "Run the web client tests",
RunE: webClientTestsCmdF,
}
var RunServerForWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests_server",
Short: "Run the server configured for running the web client tests against it",
RunE: serverForWebClientTestsCmdF,
}
func init() {
TestCmd.AddCommand(
RunWebClientTestsCmd,
RunServerForWebClientTestsCmd,
)
RootCmd.AddCommand(TestCmd)
}
func webClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
runWebClientTests()
return nil
}
func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-c
return nil
}
func setupClientTests(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.ServiceSettings.EnableCommands = false
*cfg.ServiceSettings.EnableCustomEmoji = true
*cfg.ServiceSettings.EnableIncomingWebhooks = false
*cfg.ServiceSettings.EnableOutgoingWebhooks = false
}
func executeTestCommand(command *exec.Cmd) {
cmdOutPipe, err := command.StdoutPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdErrOutPipe, err := command.StderrPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdOutReader := bufio.NewScanner(cmdOutPipe)
cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe)
go func() {
for cmdOutReader.Scan() {
fmt.Println(cmdOutReader.Text())
}
}()
go func() {
for cmdErrOutReader.Scan() {
fmt.Println(cmdErrOutReader.Text())
}
}()
if err := command.Run(); err != nil {
CommandPrintErrorln("Client Tests failed")
os.Exit(1)
return
}
}
func runWebClientTests() {
if webappDir := os.Getenv("WEBAPP_DIR"); webappDir != "" {
os.Chdir(webappDir)
} else {
os.Chdir("../mattermost-webapp")
}
cmd := exec.Command("npm", "test")
executeTestCommand(cmd)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
const CustomDefaultsEnvVar = "MM_CUSTOM_DEFAULTS_PATH"
// printStringMap takes a reflect.Value and prints it out alphabetically based on key values, which must be strings.
// This is done recursively if it's a map, and uses the given tab settings.
func printStringMap(value reflect.Value, tabVal int) string {
out := &bytes.Buffer{}
var sortedKeys []string
stringToKeyMap := make(map[string]reflect.Value)
for _, k := range value.MapKeys() {
sortedKeys = append(sortedKeys, k.String())
stringToKeyMap[k.String()] = k
}
sort.Strings(sortedKeys)
for _, keyString := range sortedKeys {
key := stringToKeyMap[keyString]
val := value.MapIndex(key)
if newVal, ok := val.Interface().(map[string]any); !ok {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface())
} else {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v:\n", key.Interface())
// going one level in, increase the tab
tabVal++
fmt.Fprintf(out, "%s", printStringMap(reflect.ValueOf(newVal), tabVal))
// coming back one level, decrease the tab
tabVal--
}
}
return out.String()
}
func getConfigDSN(command *cobra.Command, env map[string]string) string {
configDSN, _ := command.Flags().GetString("config")
// Config not supplied in flag, check env
if configDSN == "" {
configDSN = env["MM_CONFIG"]
}
// Config not supplied in env or flag use default
if configDSN == "" {
configDSN = "config.json"
}
return configDSN
}
func loadCustomDefaults() (*model.Config, error) {
customDefaultsPath := os.Getenv(CustomDefaultsEnvVar)
if customDefaultsPath == "" {
return nil, nil
}
file, err := os.Open(customDefaultsPath)
if err != nil {
return nil, fmt.Errorf("unable to open custom defaults file at %q: %w", customDefaultsPath, err)
}
defer file.Close()
var customDefaults *model.Config
err = json.NewDecoder(file).Decode(&customDefaults)
if err != nil {
return nil, fmt.Errorf("unable to decode custom defaults configuration: %w", err)
}
return customDefaults, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
var VersionCmd = &cobra.Command{
Use: "version",
Short: "Display version information",
RunE: versionCmdF,
}
func init() {
VersionCmd.Flags().Bool("skip-server-start", false, "Skip the server initialization and return the Mattermost version without the DB version.")
VersionCmd.Flags().MarkDeprecated("skip-server-start", "This flag is not necessary anymore and the flag will be removed in the future releases. Consider removing it from your scripts.")
RootCmd.AddCommand(VersionCmd)
}
func versionCmdF(command *cobra.Command, args []string) error {
CommandPrintln("Version: " + model.CurrentVersion)
CommandPrintln("Build Number: " + model.BuildNumber)
CommandPrintln("Build Date: " + model.BuildDate)
CommandPrintln("Build Hash: " + model.BuildHash)
CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"os"
"github.com/mattermost/mattermost-server/v6/server/cmd/mattermost/commands"
// Import and register app layer slash commands
_ "github.com/mattermost/mattermost-server/v6/server/channels/app/slashcommands"
// Plugins
_ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab"
// Enterprise Imports
_ "github.com/mattermost/mattermost-server/v6/server/channels/imports"
)
func main() {
if err := commands.Run(os.Args[1:]); err != nil {
os.Exit(1)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// GenerateClientConfig renders the given configuration for a client.
func GenerateClientConfig(c *model.Config, telemetryID string, license *model.License) map[string]string {
props := GenerateLimitedClientConfig(c, telemetryID, license)
props["EnableCustomUserStatuses"] = strconv.FormatBool(*c.TeamSettings.EnableCustomUserStatuses)
props["EnableLastActiveTime"] = strconv.FormatBool(*c.TeamSettings.EnableLastActiveTime)
props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation)
props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
props["LockTeammateNameDisplay"] = strconv.FormatBool(*c.TeamSettings.LockTeammateNameDisplay)
props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(*c.ServiceSettings.EnableOAuthServiceProvider)
props["GoogleDeveloperKey"] = *c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableOutgoingWebhooks)
props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
props["EnablePostUsernameOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostIconOverride)
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
props["EnablePermalinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnablePermalinkPreviews)
props["EnableTesting"] = strconv.FormatBool(*c.ServiceSettings.EnableTesting)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
props["EnableClientPerformanceDebugging"] = strconv.FormatBool(*c.ServiceSettings.EnableClientPerformanceDebugging)
props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
props["MinimumHashtagLength"] = fmt.Sprintf("%v", *c.ServiceSettings.MinimumHashtagLength)
props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
props["EnableOnboardingFlow"] = strconv.FormatBool(*c.ServiceSettings.EnableOnboardingFlow)
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
props["EnableSVGs"] = strconv.FormatBool(*c.ServiceSettings.EnableSVGs)
props["EnableMarketplace"] = strconv.FormatBool(*c.PluginSettings.EnableMarketplace)
props["EnableLatex"] = strconv.FormatBool(*c.ServiceSettings.EnableLatex)
props["EnableInlineLatex"] = strconv.FormatBool(*c.ServiceSettings.EnableInlineLatex)
props["ExtendSessionLengthWithActivity"] = strconv.FormatBool(*c.ServiceSettings.ExtendSessionLengthWithActivity)
props["ManagedResourcePaths"] = *c.ServiceSettings.ManagedResourcePaths
// This setting is only temporary, so keep using the old setting name for the mobile and web apps
props["ExperimentalEnablePostMetadata"] = "true"
props["EnableAppBar"] = strconv.FormatBool(*c.ExperimentalSettings.EnableAppBar)
props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
props["SendEmailNotifications"] = strconv.FormatBool(*c.EmailSettings.SendEmailNotifications)
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
props["RequireEmailVerification"] = strconv.FormatBool(*c.EmailSettings.RequireEmailVerification)
props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching)
props["EnablePreviewModeBanner"] = strconv.FormatBool(*c.EmailSettings.EnablePreviewModeBanner)
props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType
props["ShowEmailAddress"] = strconv.FormatBool(*c.PrivacySettings.ShowEmailAddress)
props["ShowFullName"] = strconv.FormatBool(*c.PrivacySettings.ShowFullName)
props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
props["EnablePublicLink"] = strconv.FormatBool(*c.FileSettings.EnablePublicLink)
props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
props["SQLDriverName"] = *c.SqlSettings.DriverName
props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
props["GfycatApiKey"] = *c.ServiceSettings.GfycatAPIKey
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatAPISecret
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
props["CWSURL"] = *c.CloudSettings.CWSURL
// Set default values for all options that require a license.
props["ExperimentalEnableAuthenticationTransfer"] = "true"
props["LdapNicknameAttributeSet"] = "false"
props["LdapFirstNameAttributeSet"] = "false"
props["LdapLastNameAttributeSet"] = "false"
props["LdapPictureAttributeSet"] = "false"
props["LdapPositionAttributeSet"] = "false"
props["EnableCompliance"] = "false"
props["EnableMobileFileDownload"] = "true"
props["EnableMobileFileUpload"] = "true"
props["SamlFirstNameAttributeSet"] = "false"
props["SamlLastNameAttributeSet"] = "false"
props["SamlNicknameAttributeSet"] = "false"
props["SamlPositionAttributeSet"] = "false"
props["EnableCluster"] = "false"
props["EnableMetrics"] = "false"
props["EnableBanner"] = "false"
props["BannerText"] = ""
props["BannerColor"] = ""
props["BannerTextColor"] = ""
props["AllowBannerDismissal"] = "false"
props["EnableThemeSelection"] = "true"
props["DefaultTheme"] = ""
props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["DataRetentionEnableBoardsDeletion"] = "false"
props["DataRetentionBoardsRetentionDays"] = "0"
props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",")
props["IsDefaultMarketplace"] = strconv.FormatBool(*c.PluginSettings.MarketplaceURL == model.PluginSettingsDefaultMarketplaceURL)
props["ExperimentalSharedChannels"] = "false"
props["CollapsedThreads"] = *c.ServiceSettings.CollapsedThreads
props["EnableCustomGroups"] = "false"
props["InsightsEnabled"] = strconv.FormatBool(c.FeatureFlags.InsightsEnabled)
props["PostPriority"] = strconv.FormatBool(*c.ServiceSettings.PostPriority)
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
if license != nil {
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
if *license.Features.LDAP {
props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
props["LdapPictureAttributeSet"] = strconv.FormatBool(*c.LdapSettings.PictureAttribute != "")
props["LdapPositionAttributeSet"] = strconv.FormatBool(*c.LdapSettings.PositionAttribute != "")
}
if *license.Features.Compliance {
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
}
if *license.Features.SAML {
props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
props["SamlPositionAttributeSet"] = strconv.FormatBool(*c.SamlSettings.PositionAttribute != "")
}
if *license.Features.FutureFeatures {
props["ExperimentalClientSideCertEnable"] = strconv.FormatBool(*c.ExperimentalSettings.ClientSideCertEnable)
props["ExperimentalClientSideCertCheck"] = *c.ExperimentalSettings.ClientSideCertCheck
}
if *license.Features.Cluster {
props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
}
if *license.Features.Cluster {
props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
}
if *license.Features.Announcement {
props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
props["BannerText"] = *c.AnnouncementSettings.BannerText
props["BannerColor"] = *c.AnnouncementSettings.BannerColor
props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
}
if *license.Features.ThemeManagement {
props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
}
if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
props["DataRetentionEnableBoardsDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableBoardsDeletion)
props["DataRetentionBoardsRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.BoardsRetentionDays), 10)
}
if license.HasSharedChannels() {
props["ExperimentalSharedChannels"] = strconv.FormatBool(*c.ExperimentalSettings.EnableSharedChannels)
props["ExperimentalRemoteClusterService"] = strconv.FormatBool(c.FeatureFlags.EnableRemoteClusterService && *c.ExperimentalSettings.EnableRemoteClusterService)
}
if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise {
props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups)
}
if (license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise) && c.FeatureFlags.PostPriority {
props["PostAcknowledgements"] = "true"
}
}
return props
}
// GenerateLimitedClientConfig renders the given configuration for an untrusted client.
func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *model.License) map[string]string {
props := make(map[string]string)
props["Version"] = model.CurrentVersion
props["BuildNumber"] = model.BuildNumber
props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash
props["BuildHashEnterprise"] = model.BuildHashEnterprise
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
props["BuildHashBoards"] = model.BuildHashBoards
props["BuildBoards"] = model.BuildBoards
props["BuildHashPlaybooks"] = model.BuildHashPlaybooks
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
props["EnableFile"] = strconv.FormatBool(*c.LogSettings.EnableFile)
props["FileLevel"] = *c.LogSettings.FileLevel
props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
props["SiteName"] = *c.TeamSettings.SiteName
props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation)
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion
props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion
props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion
props["IosMinVersion"] = c.ClientRequirements.IosMinVersion
props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["EnableComplianceExport"] = strconv.FormatBool(*c.MessageExportSettings.EnableExport)
props["EnableSignUpWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignUpWithEmail)
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor
props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor
props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor
props["EnableSignUpWithGitLab"] = strconv.FormatBool(*c.GitLabSettings.Enable)
props["GitLabButtonColor"] = *c.GitLabSettings.ButtonColor
props["GitLabButtonText"] = *c.GitLabSettings.ButtonText
props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
props["AboutLink"] = *c.SupportSettings.AboutLink
props["HelpLink"] = *c.SupportSettings.HelpLink
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
props["SupportEmail"] = *c.SupportSettings.SupportEmail
props["EnableAskCommunityLink"] = strconv.FormatBool(*c.SupportSettings.EnableAskCommunityLink)
props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
props["DiagnosticId"] = telemetryID
props["TelemetryId"] = telemetryID
props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["HasImageProxy"] = strconv.FormatBool(*c.ImageProxySettings.Enable)
props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
// Set default values for all options that require a license.
props["EnableCustomBrand"] = "false"
props["CustomBrandText"] = ""
props["CustomDescriptionText"] = ""
props["EnableLdap"] = "false"
props["LdapLoginFieldName"] = ""
props["LdapLoginButtonColor"] = ""
props["LdapLoginButtonBorderColor"] = ""
props["LdapLoginButtonTextColor"] = ""
props["EnableSaml"] = "false"
props["SamlLoginButtonText"] = ""
props["SamlLoginButtonColor"] = ""
props["SamlLoginButtonBorderColor"] = ""
props["SamlLoginButtonTextColor"] = ""
props["EnableSignUpWithGoogle"] = "false"
props["EnableSignUpWithOffice365"] = "false"
props["EnableSignUpWithOpenId"] = "false"
props["OpenIdButtonText"] = ""
props["OpenIdButtonColor"] = ""
props["CWSURL"] = ""
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
props["EnforceMultifactorAuthentication"] = "false"
props["EnableGuestAccounts"] = strconv.FormatBool(*c.GuestAccountsSettings.Enable)
props["GuestAccountsEnforceMultifactorAuthentication"] = strconv.FormatBool(*c.GuestAccountsSettings.EnforceMultifactorAuthentication)
if license != nil {
if *license.Features.LDAP {
props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
}
if *license.Features.SAML {
props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
}
if *license.Features.CustomTermsOfService {
props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
}
if *license.Features.MFA {
props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
}
if license.IsCloud() {
// MM-48727: enable SSO options for free cloud - not in self hosted
*license.Features.GoogleOAuth = true
*license.Features.Office365OAuth = true
}
if *license.Features.GoogleOAuth {
props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable)
}
if *license.Features.Office365OAuth {
props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable)
}
if *license.Features.OpenId {
props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable)
props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor
props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText
}
}
for key, value := range c.FeatureFlags.ToMap() {
props["FeatureFlag"+key] = value
}
return props
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
// Load the MySQL driver
_ "github.com/go-sql-driver/mysql"
// Load the Postgres driver
_ "github.com/lib/pq"
"github.com/mattermost/morph"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/morph/drivers"
ms "github.com/mattermost/morph/drivers/mysql"
ps "github.com/mattermost/morph/drivers/postgres"
mbindata "github.com/mattermost/morph/sources/embedded"
)
//go:embed migrations
var assets embed.FS
// MaxWriteLength defines the maximum length accepted for write to the Configurations or
// ConfigurationFiles table.
//
// It is imposed by MySQL's default max_allowed_packet value of 4Mb.
const MaxWriteLength = 4 * 1024 * 1024
// We use the something different from the default migration table name of morph
const migrationsTableName = "db_config_migrations"
// The timeout value for each migration file to run.
const migrationsTimeoutInSeconds = 100000
// DatabaseStore is a config store backed by a database.
// Not to be used directly. Only to be used as a backing store for config.Store
type DatabaseStore struct {
originalDsn string
driverName string
dataSourceName string
db *sqlx.DB
}
// NewDatabaseStore creates a new instance of a config store backed by the given database.
func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) {
driverName, dataSourceName, err := parseDSN(dsn)
if err != nil {
return nil, errors.Wrap(err, "invalid DSN")
}
db, err := sqlx.Open(driverName, dataSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to connect to %s database", driverName)
}
// Set conservative connection configuration for configuration database.
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(2)
defer func() {
if err != nil {
db.Close()
}
}()
ds = &DatabaseStore{
driverName: driverName,
originalDsn: dsn,
dataSourceName: dataSourceName,
db: db,
}
if err = ds.initializeConfigurationsTable(); err != nil {
err = errors.Wrap(err, "failed to initialize")
return nil, err
}
return ds, nil
}
// initializeConfigurationsTable ensures the requisite tables in place to form the backing store.
//
// Uses MEDIUMTEXT on MySQL, and TEXT on sane databases.
func (ds *DatabaseStore) initializeConfigurationsTable() error {
assetsList, err := assets.ReadDir(filepath.Join("migrations", ds.driverName))
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNamesForDriver[i] = entry.Name()
}
src, err := mbindata.WithInstance(&mbindata.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(filepath.Join("migrations", ds.driverName, name))
},
})
if err != nil {
return err
}
var driver drivers.Driver
switch ds.driverName {
case model.DatabaseDriverMysql:
dataSource, rErr := sqlstore.ResetReadTimeout(ds.dataSourceName)
if rErr != nil {
return fmt.Errorf("failed to reset read timeout from datasource: %w", rErr)
}
dataSource, err = sqlstore.AppendMultipleStatementsFlag(dataSource)
if err != nil {
return err
}
var db *sqlx.DB
db, err = sqlx.Open(ds.driverName, dataSource)
if err != nil {
return errors.Wrapf(err, "failed to connect to %s database", ds.driverName)
}
driver, err = ms.WithInstance(db.DB)
defer db.Close()
case model.DatabaseDriverPostgres:
driver, err = ps.WithInstance(ds.db.DB)
default:
err = fmt.Errorf("unsupported database type %s for migration", ds.driverName)
}
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLock("mm-config-lock-key"),
morph.SetMigrationTableName(migrationsTableName),
morph.SetStatementTimeoutInSeconds(migrationsTimeoutInSeconds),
}
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer engine.Close()
return engine.ApplyAll()
}
// parseDSN splits up a connection string into a driver name and data source name.
//
// For example:
//
// mysql://mmuser:mostest@localhost:5432/mattermost_test
//
// returns
//
// driverName = mysql
// dataSourceName = mmuser:mostest@localhost:5432/mattermost_test
//
// By contrast, a Postgres DSN is returned unmodified.
func parseDSN(dsn string) (string, string, error) {
// Treat the DSN as the URL that it is.
s := strings.SplitN(dsn, "://", 2)
if len(s) != 2 {
return "", "", errors.New("failed to parse DSN as URL")
}
scheme := s[0]
switch scheme {
case "mysql":
// Strip off the mysql:// for the dsn with which to connect.
dsn = s[1]
case "postgres", "postgresql":
// No changes required
default:
return "", "", errors.Errorf("unsupported scheme %s", scheme)
}
return scheme, dsn, nil
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (ds *DatabaseStore) Set(newCfg *model.Config) error {
return ds.persist(newCfg)
}
// maxLength identifies the maximum length of a configuration or configuration file
func (ds *DatabaseStore) checkLength(length int) error {
if ds.db.DriverName() == "mysql" && length > MaxWriteLength {
return errors.Errorf("value is too long: %d > %d bytes", length, MaxWriteLength)
}
return nil
}
// persist writes the configuration to the configured database.
func (ds *DatabaseStore) persist(cfg *model.Config) error {
b, err := marshalConfig(cfg)
if err != nil {
return errors.Wrap(err, "failed to serialize")
}
value := string(b)
err = ds.checkLength(len(value))
if err != nil {
return errors.Wrap(err, "marshalled configuration failed length check")
}
sum := sha256.Sum256(b)
// Skip the persist altogether if we're effectively writing the same configuration.
var oldValue string
var row *sql.Row
if ds.driverName == model.DatabaseDriverMysql {
// We use a sub-query to get the Id first because selecting the Id column using
// active uses the index, but selecting SHA column using active does not use the index.
// The sub-query uses the active index, and then the top-level query uses the primary key.
// This takes 2 queries, but it is actually faster than one slow query for MySQL
row = ds.db.QueryRow("SELECT SHA FROM Configurations WHERE Id = (select Id from Configurations Where Active)")
} else {
row = ds.db.QueryRow("SELECT SHA FROM Configurations WHERE Active")
}
if err = row.Scan(&oldValue); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "failed to query active configuration")
}
// postgres retruns blank-padded therefore we trim the space
oldSum, err := hex.DecodeString(strings.TrimSpace(oldValue))
if err != nil {
return errors.Wrap(err, "could not encode value")
}
// compare checksums, it's more efficient rather than comparing entire config itself
if bytes.Equal(oldSum, sum[0:]) {
return nil
}
tx, err := ds.db.Beginx()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer func() {
// Rollback after Commit just returns sql.ErrTxDone.
if err = tx.Rollback(); err != nil && err != sql.ErrTxDone {
mlog.Error("Failed to rollback configuration transaction", mlog.Err(err))
}
}()
var oldId string
if ds.driverName == model.DatabaseDriverMysql {
// the query doesn't use active index if we query for value (mysql, no surprise)
// we select Id column which triggers using index hence we do quicker reads
// that's the reason we select id first then query against id to get the value.
row = tx.QueryRow("SELECT Id FROM Configurations WHERE Active")
if err = row.Scan(&oldId); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "failed to query active configuration")
}
if oldId != "" {
if _, err := tx.NamedExec("UPDATE Configurations SET Active = NULL WHERE Id = :id", map[string]any{"id": oldId}); err != nil {
return errors.Wrap(err, "failed to deactivate current configuration")
}
}
} else {
if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil {
return errors.Wrap(err, "failed to deactivate current configuration")
}
}
params := map[string]any{
"id": model.NewId(),
"value": value,
"create_at": model.GetMillis(),
"key": "ConfigurationId",
"sha": hex.EncodeToString(sum[0:]),
}
if _, err := tx.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active, SHA) VALUES (:id, :value, :create_at, TRUE, :sha)", params); err != nil {
return errors.Wrap(err, "failed to record new configuration")
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
// Load updates the current configuration from the backing store.
func (ds *DatabaseStore) Load() ([]byte, error) {
var configurationData []byte
row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active")
if err := row.Scan(&configurationData); err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to query active configuration")
}
// Initialize from the default config if no active configuration could be found.
if len(configurationData) == 0 {
configWithDB := model.Config{}
configWithDB.SqlSettings.DriverName = model.NewString(ds.driverName)
configWithDB.SqlSettings.DataSource = model.NewString(ds.dataSourceName)
return json.Marshal(configWithDB)
}
return configurationData, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (ds *DatabaseStore) GetFile(name string) ([]byte, error) {
query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return nil, err
}
var data []byte
row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
if err = row.Scan(&data); err != nil {
return nil, errors.Wrapf(err, "failed to scan data from row for %s", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ds *DatabaseStore) SetFile(name string, data []byte) error {
err := ds.checkLength(len(data))
if err != nil {
return errors.Wrap(err, "file data failed length check")
}
params := map[string]any{
"name": name,
"data": data,
"create_at": model.GetMillis(),
"update_at": model.GetMillis(),
}
result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params)
if err != nil {
return errors.Wrapf(err, "failed to update row for %s", name)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrapf(err, "failed to count rows affected for %s", name)
} else if count > 0 {
return nil
}
_, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params)
if err != nil {
return errors.Wrapf(err, "failed to insert row for %s", name)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ds *DatabaseStore) HasFile(name string) (bool, error) {
query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return false, err
}
var count int64
row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
if err = row.Scan(&count); err != nil {
return false, errors.Wrapf(err, "failed to scan count of rows for %s", name)
}
return count != 0, nil
}
// RemoveFile remoevs a previously persisted configuration file.
func (ds *DatabaseStore) RemoveFile(name string) error {
_, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return errors.Wrapf(err, "failed to remove row for %s", name)
}
return nil
}
// String returns the path to the database backing the config, masking the password.
func (ds *DatabaseStore) String() string {
return stripPassword(ds.originalDsn, ds.driverName)
}
// Close cleans up resources associated with the store.
func (ds *DatabaseStore) Close() error {
return ds.db.Close()
}
// removes configurations from database if they are older than threshold.
func (ds *DatabaseStore) cleanUp(thresholdCreatAt int) error {
if _, err := ds.db.NamedExec("DELETE FROM Configurations Where CreateAt < :timestamp", map[string]any{"timestamp": thresholdCreatAt}); err != nil {
return errors.Wrap(err, "unable to clean Configurations table")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
)
type ConfigDiffs []ConfigDiff
type ConfigDiff struct {
Path string `json:"path"`
BaseVal any `json:"base_val"`
ActualVal any `json:"actual_val"`
}
func (c *ConfigDiff) Auditable() map[string]interface{} {
return map[string]interface{}{
"path": c.Path,
"base_val": c.BaseVal,
"actual_val": c.ActualVal,
}
}
func (cd *ConfigDiffs) Auditable() map[string]interface{} {
var s []interface{}
for _, d := range cd.Sanitize() {
s = append(s, d.Auditable())
}
return map[string]interface{}{
"config_diffs": s,
}
}
var configSensitivePaths = map[string]bool{
"LdapSettings.BindPassword": true,
"FileSettings.PublicLinkSalt": true,
"FileSettings.AmazonS3SecretAccessKey": true,
"SqlSettings.DataSource": true,
"SqlSettings.AtRestEncryptKey": true,
"SqlSettings.DataSourceReplicas": true,
"SqlSettings.DataSourceSearchReplicas": true,
"EmailSettings.SMTPPassword": true,
"GitLabSettings.Secret": true,
"GoogleSettings.Secret": true,
"Office365Settings.Secret": true,
"OpenIdSettings.Secret": true,
"ElasticsearchSettings.Password": true,
"MessageExportSettings.GlobalRelaySettings.SMTPUsername": true,
"MessageExportSettings.GlobalRelaySettings.SMTPPassword": true,
"MessageExportSettings.GlobalRelaySettings.EmailAddress": true,
"ServiceSettings.GfycatAPISecret": true,
"ServiceSettings.SplitKey": true,
"PluginSettings.Plugins": true,
}
// Sanitize replaces sensitive config values in the diff with asterisks filled strings.
func (cd ConfigDiffs) Sanitize() ConfigDiffs {
if len(cd) == 1 {
cfgPtr, ok := cd[0].BaseVal.(*model.Config)
if ok {
cfgPtr.Sanitize()
}
cfgPtr, ok = cd[0].ActualVal.(*model.Config)
if ok {
cfgPtr.Sanitize()
}
cfgVal, ok := cd[0].BaseVal.(model.Config)
if ok {
cfgVal.Sanitize()
}
cfgVal, ok = cd[0].ActualVal.(model.Config)
if ok {
cfgVal.Sanitize()
}
}
for i := range cd {
if configSensitivePaths[cd[i].Path] {
cd[i].BaseVal = model.FakeSetting
cd[i].ActualVal = model.FakeSetting
}
}
return cd
}
func diff(base, actual reflect.Value, label string) ([]ConfigDiff, error) {
var diffs []ConfigDiff
if base.IsZero() && actual.IsZero() {
return diffs, nil
}
if base.IsZero() || actual.IsZero() {
return append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
}), nil
}
baseType := base.Type()
actualType := actual.Type()
if baseType.Kind() == reflect.Ptr {
base = reflect.Indirect(base)
actual = reflect.Indirect(actual)
baseType = base.Type()
actualType = actual.Type()
}
if baseType != actualType {
return nil, fmt.Errorf("not same type %s %s", baseType, actualType)
}
switch baseType.Kind() {
case reflect.Struct:
if base.NumField() != actual.NumField() {
return nil, fmt.Errorf("not same number of fields in struct")
}
for i := 0; i < base.NumField(); i++ {
fieldLabel := baseType.Field(i).Name
if label != "" {
fieldLabel = label + "." + fieldLabel
}
d, err := diff(base.Field(i), actual.Field(i), fieldLabel)
if err != nil {
return nil, err
}
diffs = append(diffs, d...)
}
default:
if !reflect.DeepEqual(base.Interface(), actual.Interface()) {
diffs = append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
})
}
}
return diffs, nil
}
func Diff(base, actual *model.Config) (ConfigDiffs, error) {
if base == nil || actual == nil {
return nil, fmt.Errorf("input configs should not be nil")
}
baseVal := reflect.Indirect(reflect.ValueOf(base))
actualVal := reflect.Indirect(reflect.ValueOf(actual))
return diff(baseVal, actualVal, "")
}
func (cd ConfigDiffs) String() string {
return fmt.Sprintf("%+v", []ConfigDiff(cd))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Listener is a callback function invoked when the configuration changes.
type Listener func(oldCfg, newCfg *model.Config)
// emitter enables threadsafe registration and broadcasting to configuration listeners
type emitter struct {
listeners sync.Map
}
// AddListener adds a callback function to invoke when the configuration is modified.
func (e *emitter) AddListener(listener Listener) string {
id := model.NewId()
e.listeners.Store(id, listener)
return id
}
// RemoveListener removes a callback function using an id returned from AddListener.
func (e *emitter) RemoveListener(id string) {
e.listeners.Delete(id)
}
// invokeConfigListeners synchronously notifies all listeners about the configuration change.
func (e *emitter) invokeConfigListeners(oldCfg, newCfg *model.Config) {
e.listeners.Range(func(key, value any) bool {
listener := value.(Listener)
listener(oldCfg, newCfg)
return true
})
}
// srcEmitter enables threadsafe registration and broadcasting to configuration listeners
type logSrcEmitter struct {
listeners sync.Map
}
// AddListener adds a callback function to invoke when the configuration is modified.
func (e *logSrcEmitter) AddListener(listener LogSrcListener) string {
id := model.NewId()
e.listeners.Store(id, listener)
return id
}
// RemoveListener removes a callback function using an id returned from AddListener.
func (e *logSrcEmitter) RemoveListener(id string) {
e.listeners.Delete(id)
}
// invokeConfigListeners synchronously notifies all listeners about the configuration change.
func (e *logSrcEmitter) invokeConfigListeners(oldCfg, newCfg mlog.LoggerConfiguration) {
e.listeners.Range(func(key, value any) bool {
listener := value.(LogSrcListener)
listener(oldCfg, newCfg)
return true
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"os"
"reflect"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func GetEnvironment() map[string]string {
mmenv := make(map[string]string)
for _, env := range os.Environ() {
kv := strings.SplitN(env, "=", 2)
key := strings.ToUpper(kv[0])
if strings.HasPrefix(key, "MM") {
mmenv[key] = kv[1]
}
}
return mmenv
}
func applyEnvKey(key, value string, rValueSubject reflect.Value) {
keyParts := strings.SplitN(key, "_", 2)
if len(keyParts) < 1 {
return
}
rFieldValue := rValueSubject.FieldByNameFunc(func(candidate string) bool {
candidateUpper := strings.ToUpper(candidate)
return candidateUpper == keyParts[0]
})
if !rFieldValue.IsValid() {
return
}
if rFieldValue.Kind() == reflect.Ptr {
rFieldValue = rFieldValue.Elem()
if !rFieldValue.IsValid() {
return
}
}
switch rFieldValue.Kind() {
case reflect.Struct:
// If we have only one part left, we can't deal with a struct
// the env var is incomplete so give up.
if len(keyParts) < 2 {
return
}
applyEnvKey(keyParts[1], value, rFieldValue)
case reflect.String:
rFieldValue.Set(reflect.ValueOf(value))
case reflect.Bool:
boolVal, err := strconv.ParseBool(value)
if err == nil {
rFieldValue.Set(reflect.ValueOf(boolVal))
}
case reflect.Int:
intVal, err := strconv.ParseInt(value, 10, 0)
if err == nil {
rFieldValue.Set(reflect.ValueOf(int(intVal)))
}
case reflect.Int64:
intVal, err := strconv.ParseInt(value, 10, 0)
if err == nil {
rFieldValue.Set(reflect.ValueOf(intVal))
}
case reflect.SliceOf(reflect.TypeOf("")).Kind():
rFieldValue.Set(reflect.ValueOf(strings.Split(value, " ")))
case reflect.Map:
target := reflect.New(rFieldValue.Type()).Interface()
if err := json.Unmarshal([]byte(value), target); err == nil {
rFieldValue.Set(reflect.ValueOf(target).Elem())
}
}
}
func applyEnvironmentMap(inputConfig *model.Config, env map[string]string) *model.Config {
appliedConfig := inputConfig.Clone()
rvalConfig := reflect.ValueOf(appliedConfig).Elem()
for envKey, envValue := range env {
applyEnvKey(strings.TrimPrefix(envKey, "MM_"), envValue, rvalConfig)
}
return appliedConfig
}
// generateEnvironmentMap creates a map[string]any containing true at the leaves mirroring the
// configuration structure so the client can know which env variables are overridden
func generateEnvironmentMap(env map[string]string, filter func(reflect.StructField) bool) map[string]any {
rType := reflect.TypeOf(model.Config{})
return generateEnvironmentMapWithBaseKey(env, rType, "MM", filter)
}
func generateEnvironmentMapWithBaseKey(env map[string]string, rType reflect.Type, base string, filter func(reflect.StructField) bool) map[string]any {
if rType.Kind() != reflect.Struct {
return nil
}
mapRepresentation := make(map[string]any)
for i := 0; i < rType.NumField(); i++ {
rField := rType.Field(i)
if filter != nil && !filter(rField) {
continue
}
if rField.Type.Kind() == reflect.Struct {
if val := generateEnvironmentMapWithBaseKey(env, rField.Type, base+"_"+rField.Name, filter); val != nil {
mapRepresentation[rField.Name] = val
}
} else {
if _, ok := env[strings.ToUpper(base+"_"+rField.Name)]; ok {
mapRepresentation[rField.Name] = true
}
}
}
if len(mapRepresentation) == 0 {
return nil
}
return mapRepresentation
}
// removeEnvOverrides returns a new config without the given environment overrides.
// If a config variable has an environment override, that variable is set to the value that was
// read from the store.
func removeEnvOverrides(cfg, cfgWithoutEnv *model.Config, envOverrides map[string]any) *model.Config {
paths := getPaths(envOverrides)
newCfg := cfg.Clone()
for _, path := range paths {
originalVal := getVal(cfgWithoutEnv, path)
newVal := getVal(newCfg, path)
if newVal.CanSet() {
newVal.Set(originalVal)
}
}
return newCfg
}
// getPaths turns a nested map into a slice of paths describing the keys of the map. Eg:
// map[string]map[string]map[string]bool{"this":{"is first":{"path":true}, "is second":{"path":true}))) is turned into:
// [][]string{{"this", "is first", "path"}, {"this", "is second", "path"}}
func getPaths(m map[string]any) [][]string {
return getPathsRec(m, nil)
}
// getPathsRec assembles the paths (see `getPaths` above)
func getPathsRec(src any, curPath []string) [][]string {
if srcMap, ok := src.(map[string]any); ok {
paths := [][]string{}
for k, v := range srcMap {
paths = append(paths, getPathsRec(v, append(curPath, k))...)
}
return paths
}
return [][]string{curPath}
}
// getVal walks `src` (here it starts with a model.Config, then recurses into its leaves)
// and returns the reflect.Value of the leaf at the end `path`
func getVal(src any, path []string) reflect.Value {
var val reflect.Value
// If we recursed on a Value, we already have it. If we're calling on an any, get the Value.
switch v := src.(type) {
case reflect.Value:
val = v
default:
val = reflect.ValueOf(src)
}
// Move into the struct
if val.Kind() == reflect.Ptr {
val = val.Elem().FieldByName(path[0])
} else {
val = val.FieldByName(path[0])
}
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() == reflect.Struct {
return getVal(val, path[1:])
}
return val
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
// ErrReadOnlyConfiguration is returned when an attempt to modify a read-only configuration is made.
ErrReadOnlyConfiguration = errors.New("configuration is read-only")
)
// FileStore is a config store backed by a file such as config/config.json.
//
// It also uses the folder containing the configuration file for storing other configuration files.
// Not to be used directly. Only to be used as a backing store for config.Store
type FileStore struct {
path string
}
// NewFileStore creates a new instance of a config store backed by the given file path.
func NewFileStore(path string, createFileIfNotExists bool) (fs *FileStore, err error) {
resolvedPath, err := resolveConfigFilePath(path)
if err != nil {
return nil, err
}
f, err := os.Open(resolvedPath)
if err != nil && errors.Is(err, os.ErrNotExist) && createFileIfNotExists {
file, err2 := os.Create(resolvedPath)
if err2 != nil {
return nil, fmt.Errorf("could not create config file: %w", err2)
}
defer file.Close()
} else if err != nil {
return nil, err
} else {
defer f.Close()
}
return &FileStore{
path: resolvedPath,
}, nil
}
// resolveConfigFilePath attempts to resolve the given configuration file path to an absolute path.
//
// Consideration is given to maintaining backwards compatibility when resolving the path to the
// configuration file.
func resolveConfigFilePath(path string) (string, error) {
// Absolute paths are explicit and require no resolution.
if filepath.IsAbs(path) {
return path, nil
}
// Search for the relative path to the file in the channels/config folder, taking into account
// various common starting points.
if configFile := fileutils.FindFile(filepath.Join("channels/config", path)); configFile != "" {
return configFile, nil
}
// Search for the relative path to the file in the config folder, taking into account
// various common starting points.
if configFile := fileutils.FindFile(filepath.Join("config", path)); configFile != "" {
return configFile, nil
}
// Search for the relative path in the current working directory, also taking into account
// various common starting points.
if configFile := fileutils.FindPath(path, []string{"."}, nil); configFile != "" {
return configFile, nil
}
if configFolder, found := fileutils.FindDir("config"); found {
return filepath.Join(configFolder, path), nil
}
// Fail altogether if we can't even find the config/ folder. This should only happen if
// the executable is relocated away from the supporting files.
return "", fmt.Errorf("failed to find config file %s", path)
}
// resolveFilePath uses the name if name is absolute path.
// otherwise returns the combined path/name
func (fs *FileStore) resolveFilePath(name string) string {
// Absolute paths are explicit and require no resolution.
if filepath.IsAbs(name) {
return name
}
return filepath.Join(filepath.Dir(fs.path), name)
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (fs *FileStore) Set(newCfg *model.Config) error {
if *newCfg.ClusterSettings.Enable && *newCfg.ClusterSettings.ReadOnlyConfig {
return ErrReadOnlyConfiguration
}
return fs.persist(newCfg)
}
// persist writes the configuration to the configured file.
func (fs *FileStore) persist(cfg *model.Config) error {
b, err := marshalConfig(cfg)
if err != nil {
return errors.Wrap(err, "failed to serialize")
}
err = os.WriteFile(fs.path, b, 0600)
if err != nil {
return errors.Wrap(err, "failed to write file")
}
return nil
}
// Load updates the current configuration from the backing store.
func (fs *FileStore) Load() ([]byte, error) {
f, err := os.Open(fs.path)
if os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, errors.Wrapf(err, "failed to open %s for reading", fs.path)
}
defer f.Close()
fileBytes, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return fileBytes, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (fs *FileStore) GetFile(name string) ([]byte, error) {
resolvedPath := fs.resolveFilePath(name)
data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read file from %s", resolvedPath)
}
return data, nil
}
// GetFilePath returns the resolved path of a configuration file.
// The file may not necessarily exist.
func (fs *FileStore) GetFilePath(name string) string {
return fs.resolveFilePath(name)
}
// SetFile sets or replaces the contents of a configuration file.
func (fs *FileStore) SetFile(name string, data []byte) error {
resolvedPath := fs.resolveFilePath(name)
err := os.WriteFile(resolvedPath, data, 0600)
if err != nil {
return errors.Wrapf(err, "failed to write file to %s", resolvedPath)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (fs *FileStore) HasFile(name string) (bool, error) {
if name == "" {
return false, nil
}
resolvedPath := fs.resolveFilePath(name)
_, err := os.Stat(resolvedPath)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, errors.Wrap(err, "failed to check if file exists")
}
return true, nil
}
// RemoveFile removes a previously persisted configuration file.
func (fs *FileStore) RemoveFile(name string) error {
if filepath.IsAbs(name) {
// Don't delete absolute filenames, as may be mounted drive, etc.
mlog.Debug("Skipping removal of configuration file with absolute path", mlog.String("filename", name))
return nil
}
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
err := os.Remove(resolvedPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return errors.Wrap(err, "failed to remove file")
}
return nil
}
// String returns the path to the file backing the config.
func (fs *FileStore) String() string {
return "file://" + fs.path
}
// Close cleans up resources associated with the store.
func (fs *FileStore) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"errors"
"path/filepath"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type LogSrcListener func(old, new mlog.LoggerConfiguration)
// LogConfigSrc abstracts the Advanced Logging configuration so that implementations can
// fetch from file, database, etc.
type LogConfigSrc interface {
// Get fetches the current, cached configuration.
Get() mlog.LoggerConfiguration
// Set updates the dsn specifying the source and reloads
Set(dsn string, configStore *Store) (err error)
// Close cleans up resources.
Close() error
}
// NewLogConfigSrc creates an advanced logging configuration source, backed by a
// file, JSON string, or database.
func NewLogConfigSrc(dsn string, configStore *Store) (LogConfigSrc, error) {
if dsn == "" {
return nil, errors.New("dsn should not be empty")
}
if configStore == nil {
return nil, errors.New("configStore should not be nil")
}
dsn = strings.TrimSpace(dsn)
if isJSONMap(dsn) {
return newJSONSrc(dsn)
}
path := dsn
// If this is a file based config we need the full path so it can be watched.
if strings.HasPrefix(configStore.String(), "file://") && !filepath.IsAbs(dsn) {
configPath := strings.TrimPrefix(configStore.String(), "file://")
path = filepath.Join(filepath.Dir(configPath), dsn)
}
return newFileSrc(path, configStore)
}
// jsonSrc
type jsonSrc struct {
logSrcEmitter
mutex sync.RWMutex
cfg mlog.LoggerConfiguration
}
func newJSONSrc(data string) (*jsonSrc, error) {
src := &jsonSrc{}
return src, src.Set(data, nil)
}
// Get fetches the current, cached configuration
func (src *jsonSrc) Get() mlog.LoggerConfiguration {
src.mutex.RLock()
defer src.mutex.RUnlock()
return src.cfg
}
// Set updates the JSON specifying the source and reloads
func (src *jsonSrc) Set(data string, _ *Store) error {
cfg, err := logTargetCfgFromJSON([]byte(data))
if err != nil {
return err
}
src.set(cfg)
return nil
}
func (src *jsonSrc) set(cfg mlog.LoggerConfiguration) {
src.mutex.Lock()
defer src.mutex.Unlock()
old := src.cfg
src.cfg = cfg
src.invokeConfigListeners(old, cfg)
}
// Close cleans up resources.
func (src *jsonSrc) Close() error {
return nil
}
// fileSrc
type fileSrc struct {
mutex sync.RWMutex
cfg mlog.LoggerConfiguration
path string
}
func newFileSrc(path string, configStore *Store) (*fileSrc, error) {
src := &fileSrc{
path: path,
}
if err := src.Set(path, configStore); err != nil {
return nil, err
}
return src, nil
}
// Get fetches the current, cached configuration
func (src *fileSrc) Get() mlog.LoggerConfiguration {
src.mutex.RLock()
defer src.mutex.RUnlock()
return src.cfg
}
// Set updates the dsn specifying the file source and reloads.
// The file will be watched for changes and reloaded as needed,
// and all listeners notified.
func (src *fileSrc) Set(path string, configStore *Store) error {
data, err := configStore.GetFile(path)
if err != nil {
return err
}
cfg, err := logTargetCfgFromJSON(data)
if err != nil {
return err
}
src.set(cfg)
return nil
}
func (src *fileSrc) set(cfg mlog.LoggerConfiguration) {
src.mutex.Lock()
defer src.mutex.Unlock()
src.cfg = cfg
}
// Close cleans up resources.
func (src *fileSrc) Close() error {
return nil
}
func logTargetCfgFromJSON(data []byte) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
LogRotateSizeMB = 100
LogCompress = true
LogRotateMaxAge = 0
LogRotateMaxBackups = 0
LogFilename = "mattermost.log"
LogNotificationFilename = "notifications.log"
LogMinLevelLen = 5
LogMinMsgLen = 45
LogDelim = " "
LogEnableCaller = true
)
type fileLocationFunc func(string) string
func MloggerConfigFromLoggerConfig(s *model.LogSettings, configSrc LogConfigSrc, getFileFunc fileLocationFunc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple logging config
if *s.EnableConsole {
targetCfg, err = makeSimpleConsoleTarget(*s.ConsoleLevel, *s.ConsoleJson, *s.EnableColor)
if err != nil {
return cfg, err
}
cfg["_defConsole"] = targetCfg
}
if *s.EnableFile {
targetCfg, err = makeSimpleFileTarget(getFileFunc(*s.FileLocation), *s.FileLevel, *s.FileJson)
if err != nil {
return cfg, err
}
cfg["_defFile"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced logging config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func MloggerConfigFromAuditConfig(auditSettings model.ExperimentalAuditSettings, configSrc LogConfigSrc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple audit config
if *auditSettings.FileEnabled {
targetCfg, err = makeSimpleFileTarget(*auditSettings.FileName, "error", true)
if err != nil {
return nil, err
}
// apply audit specific levels
targetCfg.Levels = []mlog.Level{mlog.LvlAuditAPI, mlog.LvlAuditContent, mlog.LvlAuditPerms, mlog.LvlAuditCLI}
// apply audit specific formatting
targetCfg.FormatOptions = json.RawMessage(`{"disable_timestamp": false, "disable_msg": true, "disable_stacktrace": true, "disable_level": true}`)
cfg["_defAudit"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced audit config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
}
return filepath.Join(fileLocation, LogFilename)
}
func GetNotificationsLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
}
return filepath.Join(fileLocation, LogNotificationFilename)
}
func GetLogSettingsFromNotificationsLogSettings(notificationLogSettings *model.NotificationLogSettings) *model.LogSettings {
settings := &model.LogSettings{}
settings.SetDefaults()
settings.ConsoleJson = notificationLogSettings.ConsoleJson
settings.ConsoleLevel = notificationLogSettings.ConsoleLevel
settings.EnableConsole = notificationLogSettings.EnableConsole
settings.EnableFile = notificationLogSettings.EnableFile
settings.FileJson = notificationLogSettings.FileJson
settings.FileLevel = notificationLogSettings.FileLevel
settings.FileLocation = notificationLogSettings.FileLocation
settings.AdvancedLoggingConfig = notificationLogSettings.AdvancedLoggingConfig
settings.EnableColor = notificationLogSettings.EnableColor
return settings
}
func makeSimpleConsoleTarget(level string, outputJSON bool, color bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {
return mlog.TargetCfg{}, err
}
target := mlog.TargetCfg{
Type: "console",
Levels: levels,
Options: json.RawMessage(`{"out": "stdout"}`),
MaxQueueSize: 1000,
}
if outputJSON {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(color)
}
return target, nil
}
func makeSimpleFileTarget(filename string, level string, json bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {
return mlog.TargetCfg{}, err
}
fileOpts, err := makeFileOptions(filename)
if err != nil {
return mlog.TargetCfg{}, fmt.Errorf("cannot encode file options: %w", err)
}
target := mlog.TargetCfg{
Type: "file",
Levels: levels,
Options: fileOpts,
MaxQueueSize: 1000,
}
if json {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(false)
}
return target, nil
}
func stdLevels(level string) ([]mlog.Level, error) {
stdLevel, err := stringToStdLevel(level)
if err != nil {
return nil, err
}
var levels []mlog.Level
for _, l := range mlog.StdAll {
if l.ID <= stdLevel.ID {
levels = append(levels, l)
}
}
return levels, nil
}
func stringToStdLevel(level string) (mlog.Level, error) {
level = strings.ToLower(level)
for _, l := range mlog.StdAll {
if l.Name == level {
return l, nil
}
}
return mlog.Level{}, fmt.Errorf("%s is not a standard level", level)
}
func makeJSONFormatOptions() json.RawMessage {
str := fmt.Sprintf(`{"enable_caller": %t}`, LogEnableCaller)
return json.RawMessage(str)
}
func makePlainFormatOptions(enableColor bool) json.RawMessage {
str := fmt.Sprintf(`{"delim": "%s", "min_level_len": %d, "min_msg_len": %d, "enable_color": %t, "enable_caller": %t}`,
LogDelim, LogMinLevelLen, LogMinMsgLen, enableColor, LogEnableCaller)
return json.RawMessage(str)
}
func makeFileOptions(filename string) (json.RawMessage, error) {
opts := struct {
Filename string `json:"filename"`
Max_size int `json:"max_size"`
Max_age int `json:"max_age"`
Max_backups int `json:"max_backups"`
Compress bool `json:"compress"`
}{
Filename: filename,
Max_size: LogRotateSizeMB,
Max_age: LogRotateMaxAge,
Max_backups: LogRotateMaxBackups,
Compress: LogCompress,
}
b, err := json.Marshal(opts)
if err != nil {
return nil, err
}
return json.RawMessage(b), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
)
// MemoryStore implements the Store interface. It is meant primarily for testing.
// Not to be used directly. Only to be used as a backing store for config.Store
type MemoryStore struct {
allowEnvironmentOverrides bool
validate bool
files map[string][]byte
savedConfig *model.Config
}
// MemoryStoreOptions makes configuration of the memory store explicit.
type MemoryStoreOptions struct {
IgnoreEnvironmentOverrides bool
SkipValidation bool
InitialConfig *model.Config
InitialFiles map[string][]byte
}
// NewMemoryStore creates a new MemoryStore instance with default options.
func NewMemoryStore() (*MemoryStore, error) {
return NewMemoryStoreWithOptions(&MemoryStoreOptions{})
}
// NewMemoryStoreWithOptions creates a new MemoryStore instance.
func NewMemoryStoreWithOptions(options *MemoryStoreOptions) (*MemoryStore, error) {
savedConfig := options.InitialConfig
if savedConfig == nil {
savedConfig = &model.Config{}
savedConfig.SetDefaults()
}
initialFiles := options.InitialFiles
if initialFiles == nil {
initialFiles = make(map[string][]byte)
}
ms := &MemoryStore{
allowEnvironmentOverrides: !options.IgnoreEnvironmentOverrides,
validate: !options.SkipValidation,
files: initialFiles,
savedConfig: savedConfig,
}
return ms, nil
}
// Set replaces the current configuration in its entirety.
func (ms *MemoryStore) Set(newCfg *model.Config) error {
return ms.persist(newCfg)
}
// persist copies the active config to the saved config.
func (ms *MemoryStore) persist(cfg *model.Config) error {
ms.savedConfig = cfg.Clone()
return nil
}
// Load applies environment overrides to the default config as if a re-load had occurred.
func (ms *MemoryStore) Load() ([]byte, error) {
cfgBytes, err := marshalConfig(ms.savedConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to serialize config")
}
return cfgBytes, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (ms *MemoryStore) GetFile(name string) ([]byte, error) {
data, ok := ms.files[name]
if !ok {
return nil, fmt.Errorf("file %s not stored", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ms *MemoryStore) SetFile(name string, data []byte) error {
ms.files[name] = data
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ms *MemoryStore) HasFile(name string) (bool, error) {
_, ok := ms.files[name]
return ok, nil
}
// RemoveFile removes a previously persisted configuration file.
func (ms *MemoryStore) RemoveFile(name string) error {
delete(ms.files, name)
return nil
}
// String returns a hard-coded description, as there is no backing store.
func (ms *MemoryStore) String() string {
return "memory://"
}
// Close does nothing for a memory store.
func (ms *MemoryStore) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"github.com/pkg/errors"
)
// Migrate migrates SAML keys, certificates, and other config files from one store to another given their data source names.
func Migrate(from, to string) error {
source, err := NewStoreFromDSN(from, false, nil, false)
if err != nil {
return errors.Wrapf(err, "failed to access source config %s", from)
}
defer source.Close()
destination, err := NewStoreFromDSN(to, false, nil, true)
if err != nil {
return errors.Wrapf(err, "failed to access destination config %s", to)
}
defer destination.Close()
sourceConfig := source.Get()
if _, _, err = destination.Set(sourceConfig); err != nil {
return errors.Wrapf(err, "failed to set config")
}
files := []string{
*sourceConfig.SamlSettings.IdpCertificateFile,
*sourceConfig.SamlSettings.PublicCertificateFile,
*sourceConfig.SamlSettings.PrivateKeyFile,
}
// Only migrate advanced logging config if it is not embedded JSON.
if !isJSONMap(*sourceConfig.LogSettings.AdvancedLoggingConfig) {
files = append(files, *sourceConfig.LogSettings.AdvancedLoggingConfig)
}
files = append(files, sourceConfig.PluginSettings.SignaturePublicKeyFiles...)
for _, file := range files {
if err := migrateFile(file, source, destination); err != nil {
return err
}
}
return nil
}
func migrateFile(name string, source *Store, destination *Store) error {
fileExists, err := source.HasFile(name)
if err != nil {
return errors.Wrapf(err, "failed to check existence of %s", name)
}
if fileExists {
file, err := source.GetFile(name)
if err != nil {
return errors.Wrapf(err, "failed to migrate %s", name)
}
err = destination.SetFile(name, file)
if err != nil {
return errors.Wrapf(err, "failed to migrate %s", name)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"reflect"
"sync"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/jsonutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
var (
// ErrReadOnlyStore is returned when an attempt to modify a read-only
// configuration store is made.
ErrReadOnlyStore = errors.New("configuration store is read-only")
)
// Store is the higher level object that handles storing and retrieval of config data.
// To do so it relies on a variety of backing stores (e.g. file, database, memory).
type Store struct {
emitter
backingStore BackingStore
configLock sync.RWMutex
config *model.Config
configNoEnv *model.Config
configCustomDefaults *model.Config
readOnly bool
readOnlyFF bool
}
// BackingStore defines the behaviour exposed by the underlying store
// implementation (e.g. file, database).
type BackingStore interface {
// Set replaces the current configuration in its entirety and updates the backing store.
Set(*model.Config) error
// Load retrieves the configuration stored. If there is no configuration stored
// the io.ReadCloser will be nil
Load() ([]byte, error)
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
GetFile(name string) ([]byte, error)
// SetFile sets or replaces the contents of a configuration file.
SetFile(name string, data []byte) error
// HasFile returns true if the given file was previously persisted.
HasFile(name string) (bool, error)
// RemoveFile removes a previously persisted configuration file.
RemoveFile(name string) error
// String describes the backing store for the config.
String() string
// Close cleans up resources associated with the store.
Close() error
}
// NewStoreFromBacking creates and returns a new config store given a backing store.
func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) {
store := &Store{
backingStore: backingStore,
configCustomDefaults: customDefaults,
readOnly: readOnly,
readOnlyFF: true,
}
if err := store.Load(); err != nil {
return nil, errors.Wrap(err, "unable to load on store creation")
}
return store, nil
}
// NewStoreFromDSN creates and returns a new config store backed by either a database or file store
// depending on the value of the given data source name string.
func NewStoreFromDSN(dsn string, readOnly bool, customDefaults *model.Config, createFileIfNotExist bool) (*Store, error) {
var err error
var backingStore BackingStore
if IsDatabaseDSN(dsn) {
backingStore, err = NewDatabaseStore(dsn)
} else {
backingStore, err = NewFileStore(dsn, createFileIfNotExist)
}
if err != nil {
return nil, err
}
store, err := NewStoreFromBacking(backingStore, customDefaults, readOnly)
if err != nil {
backingStore.Close()
return nil, errors.Wrap(err, "failed to create store")
}
return store, nil
}
// NewTestMemoryStore returns a new config store backed by a memory store
// to be used for testing purposes.
func NewTestMemoryStore() *Store {
memoryStore, err := NewMemoryStore()
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
configStore, err := NewStoreFromBacking(memoryStore, nil, false)
if err != nil {
panic("failed to initialize config store: " + err.Error())
}
return configStore
}
// Get fetches the current, cached configuration.
func (s *Store) Get() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.config
}
// GetNoEnv fetches the current cached configuration without environment variable overrides.
func (s *Store) GetNoEnv() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.configNoEnv
}
// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables.
func (s *Store) GetEnvironmentOverrides() map[string]any {
return generateEnvironmentMap(GetEnvironment(), nil)
}
// GetEnvironmentOverridesWithFilter fetches the configuration fields overridden by environment variables.
// If filter is not nil and returns false for a struct field, that field will be omitted.
func (s *Store) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]any {
return generateEnvironmentMap(GetEnvironment(), filter)
}
// RemoveEnvironmentOverrides returns a new config without the environment
// overrides.
func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides())
}
// SetReadOnlyFF sets whether feature flags should be written out to
// config or treated as read-only.
func (s *Store) SetReadOnlyFF(readOnly bool) {
s.configLock.Lock()
defer s.configLock.Unlock()
s.readOnlyFF = readOnly
}
// Set replaces the current configuration in its entirety and updates the backing store.
// It returns both old and new versions of the config.
func (s *Store) Set(newCfg *model.Config) (*model.Config, *model.Config, error) {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return nil, nil, ErrReadOnlyStore
}
newCfg = newCfg.Clone()
oldCfg := s.config.Clone()
oldCfgNoEnv := s.configNoEnv
// Setting defaults allows us to accept partial config objects.
newCfg.SetDefaults()
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
// data from the existing config as necessary.
desanitize(oldCfg, newCfg)
// We apply back environment overrides since the input config may or
// may not have them applied.
newCfg = applyEnvironmentMap(newCfg, GetEnvironment())
fixConfig(newCfg)
if err := newCfg.IsValid(); err != nil {
return nil, nil, errors.Wrap(err, "new configuration is invalid")
}
// We attempt to remove any environment override that may be present in the input config.
newCfgNoEnv := removeEnvOverrides(newCfg, oldCfgNoEnv, s.GetEnvironmentOverrides())
// Don't store feature flags unless we are on MM cloud
// MM cloud uses config in the DB as a cache of the feature flag
// settings in case the management system is down when a pod starts.
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
oldCfgNoEnvFF := oldCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
newCfg.FeatureFlags = nil
newCfgNoEnv.FeatureFlags = nil
}
if err := s.backingStore.Set(newCfgNoEnv); err != nil {
return nil, nil, errors.Wrap(err, "failed to persist")
}
hasChanged, err := equal(oldCfg, newCfg)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to compare configs")
}
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
newCfg.FeatureFlags = oldCfgFF
newCfgNoEnv.FeatureFlags = oldCfgNoEnvFF
}
s.configNoEnv = newCfgNoEnv
s.config = newCfg
newCfgCopy := newCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, newCfgCopy.Clone())
s.configLock.Lock()
}
return oldCfg, newCfgCopy, nil
}
// Load updates the current configuration from the backing store, possibly initializing.
func (s *Store) Load() error {
s.configLock.Lock()
defer s.configLock.Unlock()
oldCfg := &model.Config{}
if s.config != nil {
oldCfg = s.config.Clone()
}
configBytes, err := s.backingStore.Load()
if err != nil {
return err
}
loadedCfg := &model.Config{}
if len(configBytes) != 0 {
if err = json.Unmarshal(configBytes, &loadedCfg); err != nil {
return jsonutils.HumanizeJSONError(err, configBytes)
}
}
// If we have custom defaults set, the initial config is merged on
// top of them and we delete them not to be used again in the
// configuration reloads
if s.configCustomDefaults != nil {
var mErr error
loadedCfg, mErr = Merge(s.configCustomDefaults, loadedCfg, nil)
if mErr != nil {
return errors.Wrap(mErr, "failed to merge custom config defaults")
}
s.configCustomDefaults = nil
}
// We set the SiteURL to empty (if nil) so that the following call to
// SetDefaults() will generate missing data. This avoids an additional write
// to the backing store.
if loadedCfg.ServiceSettings.SiteURL == nil {
loadedCfg.ServiceSettings.SiteURL = model.NewString("")
}
// Setting defaults allows us to accept partial config objects.
loadedCfg.SetDefaults()
// No need to clone here since the below call to applyEnvironmentMap
// already does that internally.
loadedCfgNoEnv := loadedCfg
fixConfig(loadedCfgNoEnv)
loadedCfg = applyEnvironmentMap(loadedCfg, GetEnvironment())
fixConfig(loadedCfg)
if appErr := loadedCfg.IsValid(); appErr != nil {
// Translating the error before displaying it in the console.
// Defaulting to english for server side language.
appErr.Translate(i18n.GetUserTranslations("en"))
return errors.Wrap(appErr, "invalid config")
}
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
loadedCfgFF := loadedCfg.FeatureFlags
loadedCfgNoEnvFF := loadedCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
loadedCfg.FeatureFlags = nil
loadedCfgNoEnv.FeatureFlags = nil
}
// Check for changes that may have happened on load to the backing store.
hasChanged, err := equal(oldCfg, loadedCfg)
if err != nil {
return errors.Wrap(err, "failed to compare configs")
}
// We write back to the backing store only if the store is not read-only
// and the config has either changed or is missing.
if !s.readOnly && (hasChanged || len(configBytes) == 0) {
err := s.backingStore.Set(loadedCfgNoEnv)
if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) {
return errors.Wrap(err, "failed to persist")
}
}
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
loadedCfg.FeatureFlags = loadedCfgFF
loadedCfgNoEnv.FeatureFlags = loadedCfgNoEnvFF
}
s.config = loadedCfg
s.configNoEnv = loadedCfgNoEnv
loadedCfgCopy := loadedCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, loadedCfgCopy)
s.configLock.Lock()
}
return nil
}
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
func (s *Store) GetFile(name string) ([]byte, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.GetFile(name)
}
// SetFile sets or replaces the contents of a configuration file.
func (s *Store) SetFile(name string, data []byte) error {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return ErrReadOnlyStore
}
return s.backingStore.SetFile(name, data)
}
// HasFile returns true if the given file was previously persisted.
func (s *Store) HasFile(name string) (bool, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.HasFile(name)
}
// RemoveFile removes a previously persisted configuration file.
func (s *Store) RemoveFile(name string) error {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return ErrReadOnlyStore
}
return s.backingStore.RemoveFile(name)
}
// String describes the backing store for the config.
func (s *Store) String() string {
return s.backingStore.String()
}
// Close cleans up resources associated with the store.
func (s *Store) Close() error {
s.configLock.Lock()
defer s.configLock.Unlock()
return s.backingStore.Close()
}
// IsReadOnly returns whether or not the store is read-only.
func (s *Store) IsReadOnly() bool {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.readOnly
}
// Cleanup removes outdated configurations from the database.
// this is a no-op function for FileStore type backing store.
func (s *Store) CleanUp() error {
switch bs := s.backingStore.(type) {
case *DatabaseStore:
dur := time.Duration(*s.config.JobSettings.CleanupConfigThresholdDays) * time.Hour * 24
expiry := model.GetMillisForTime(time.Now().Add(-dur))
return bs.cleanUp(int(expiry))
default:
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// marshalConfig converts the given configuration into JSON bytes for persistence.
func marshalConfig(cfg *model.Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", " ")
}
// desanitize replaces fake settings with their actual values.
func desanitize(actual, target *model.Config) {
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting {
*target.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword
}
if *target.FileSettings.PublicLinkSalt == model.FakeSetting {
*target.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt
}
if *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
target.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey
}
if *target.EmailSettings.SMTPPassword == model.FakeSetting {
target.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword
}
if *target.GitLabSettings.Secret == model.FakeSetting {
target.GitLabSettings.Secret = actual.GitLabSettings.Secret
}
if target.GoogleSettings.Secret != nil && *target.GoogleSettings.Secret == model.FakeSetting {
target.GoogleSettings.Secret = actual.GoogleSettings.Secret
}
if target.Office365Settings.Secret != nil && *target.Office365Settings.Secret == model.FakeSetting {
target.Office365Settings.Secret = actual.Office365Settings.Secret
}
if target.OpenIdSettings.Secret != nil && *target.OpenIdSettings.Secret == model.FakeSetting {
target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret
}
if *target.SqlSettings.DataSource == model.FakeSetting {
*target.SqlSettings.DataSource = *actual.SqlSettings.DataSource
}
if *target.SqlSettings.AtRestEncryptKey == model.FakeSetting {
target.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey
}
if *target.ElasticsearchSettings.Password == model.FakeSetting {
*target.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password
}
if len(target.SqlSettings.DataSourceReplicas) == len(actual.SqlSettings.DataSourceReplicas) {
for i, value := range target.SqlSettings.DataSourceReplicas {
if value == model.FakeSetting {
target.SqlSettings.DataSourceReplicas[i] = actual.SqlSettings.DataSourceReplicas[i]
}
}
}
if len(target.SqlSettings.DataSourceSearchReplicas) == len(actual.SqlSettings.DataSourceSearchReplicas) {
for i, value := range target.SqlSettings.DataSourceSearchReplicas {
if value == model.FakeSetting {
target.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i]
}
}
}
if *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting {
*target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword
}
if target.ServiceSettings.GfycatAPISecret != nil && *target.ServiceSettings.GfycatAPISecret == model.FakeSetting {
*target.ServiceSettings.GfycatAPISecret = *actual.ServiceSettings.GfycatAPISecret
}
if *target.ServiceSettings.SplitKey == model.FakeSetting {
*target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey
}
}
// fixConfig patches invalid or missing data in the configuration.
func fixConfig(cfg *model.Config) {
// Ensure SiteURL has no trailing slash.
if strings.HasSuffix(*cfg.ServiceSettings.SiteURL, "/") {
*cfg.ServiceSettings.SiteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
}
// Ensure the directory for a local file store has a trailing slash.
if *cfg.FileSettings.DriverName == model.ImageDriverLocal {
if *cfg.FileSettings.Directory != "" && !strings.HasSuffix(*cfg.FileSettings.Directory, "/") {
*cfg.FileSettings.Directory += "/"
}
}
FixInvalidLocales(cfg)
}
// FixInvalidLocales checks and corrects the given config for invalid locale-related settings.
//
// Ideally, this function would be completely internal, but it's currently exposed to allow the cli
// to test the config change before allowing the save.
func FixInvalidLocales(cfg *model.Config) bool {
var changed bool
locales := i18n.GetSupportedLocales()
if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
*cfg.LocalizationSettings.DefaultServerLocale = model.DefaultLocale
mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.")
changed = true
}
if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
*cfg.LocalizationSettings.DefaultClientLocale = model.DefaultLocale
mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.")
changed = true
}
if *cfg.LocalizationSettings.AvailableLocales != "" {
isDefaultClientLocaleInAvailableLocales := false
for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
if _, ok := locales[word]; !ok {
*cfg.LocalizationSettings.AvailableLocales = ""
isDefaultClientLocaleInAvailableLocales = true
mlog.Warn("AvailableLocales must include DefaultClientLocale. Setting AvailableLocales to all locales as default value.")
changed = true
break
}
if word == *cfg.LocalizationSettings.DefaultClientLocale {
isDefaultClientLocaleInAvailableLocales = true
}
}
availableLocales := *cfg.LocalizationSettings.AvailableLocales
if !isDefaultClientLocaleInAvailableLocales {
availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
mlog.Warn("Adding DefaultClientLocale to AvailableLocales.")
changed = true
}
*cfg.LocalizationSettings.AvailableLocales = strings.Join(utils.RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
}
return changed
}
// Merge merges two configs together. The receiver's values are overwritten with the patch's
// values except when the patch's values are nil.
func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) {
ret, err := utils.Merge(cfg, patch, mergeConfig)
if err != nil {
return nil, err
}
retCfg := ret.(model.Config)
return &retCfg, nil
}
func IsDatabaseDSN(dsn string) bool {
return strings.HasPrefix(dsn, "mysql://") ||
strings.HasPrefix(dsn, "postgres://") ||
strings.HasPrefix(dsn, "postgresql://")
}
// stripPassword remove the password from a given DSN
func stripPassword(dsn, schema string) string {
prefix := schema + "://"
dsn = strings.TrimPrefix(dsn, prefix)
i := strings.Index(dsn, ":")
j := strings.LastIndex(dsn, "@")
// Return error if no @ sign is found
if j < 0 {
return "(omitted due to error parsing the DSN)"
}
// Return back the input if no password is found
if i < 0 || i > j {
return prefix + dsn
}
return prefix + dsn[:i+1] + dsn[j:]
}
func isJSONMap(data string) bool {
var m map[string]any
return json.Unmarshal([]byte(data), &m) == nil
}
func GetValueByPath(path []string, obj any) (any, bool) {
r := reflect.ValueOf(obj)
var val reflect.Value
if r.Kind() == reflect.Map {
val = r.MapIndex(reflect.ValueOf(path[0]))
if val.IsValid() {
val = val.Elem()
}
} else {
val = r.FieldByName(path[0])
}
if !val.IsValid() {
return nil, false
}
switch {
case len(path) == 1:
return val.Interface(), true
case val.Kind() == reflect.Struct:
return GetValueByPath(path[1:], val.Interface())
case val.Kind() == reflect.Map:
remainingPath := strings.Join(path[1:], ".")
mapIter := val.MapRange()
for mapIter.Next() {
key := mapIter.Key().String()
if strings.HasPrefix(remainingPath, key) {
i := strings.Count(key, ".") + 2 // number of dots + a dot on each side
mapVal := mapIter.Value()
// if no sub field path specified, return the object
if len(path[i:]) == 0 {
return mapVal.Interface(), true
}
data := mapVal.Interface()
if mapVal.Kind() == reflect.Ptr {
data = mapVal.Elem().Interface() // if value is a pointer, dereference it
}
// pass subpath
return GetValueByPath(path[i:], data)
}
}
}
return nil, false
}
func equal(oldCfg, newCfg *model.Config) (bool, error) {
oldCfgBytes, err := json.Marshal(oldCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal old config: %w", err)
}
newCfgBytes, err := json.Marshal(newCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal new config: %w", err)
}
return !bytes.Equal(oldCfgBytes, newCfgBytes), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package awsmeter
import (
"encoding/json"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/marketplacemetering"
"github.com/aws/aws-sdk-go/service/marketplacemetering/marketplacemeteringiface"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AwsMeter struct {
store store.Store
service *AWSMeterService
config *model.Config
}
type AWSMeterService struct {
AwsDryRun bool
AwsProductCode string
AwsMeteringSvc marketplacemeteringiface.MarketplaceMeteringAPI
}
type AWSMeterReport struct {
Dimension string `json:"dimension"`
Value int64 `json:"value"`
Timestamp time.Time `json:"timestamp"`
}
func (o *AWSMeterReport) ToJSON() string {
b, _ := json.Marshal(o)
return string(b)
}
func New(store store.Store, config *model.Config) *AwsMeter {
svc := &AWSMeterService{
AwsDryRun: false,
AwsProductCode: "12345", //TODO
}
service, err := newAWSMarketplaceMeteringService()
if err != nil {
mlog.Debug("Could not create AWS metering service", mlog.String("error", err.Error()))
return nil
}
svc.AwsMeteringSvc = service
return &AwsMeter{
store: store,
service: svc,
config: config,
}
}
func newAWSMarketplaceMeteringService() (*marketplacemetering.MarketplaceMetering, error) {
region := os.Getenv("AWS_REGION")
s, err := session.NewSession(&aws.Config{Region: ®ion})
if err != nil {
return nil, err
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&ec2rolecreds.EC2RoleProvider{
Client: ec2metadata.New(s),
},
})
_, err = creds.Get()
if err != nil {
return nil, errors.Wrap(err, "cannot obtain credentials")
}
return marketplacemetering.New(session.Must(session.NewSession(&aws.Config{
Credentials: creds,
}))), nil
}
// a report entry is for all metrics
func (awsm *AwsMeter) GetUserCategoryUsage(dimensions []string, startTime time.Time, endTime time.Time) []*AWSMeterReport {
reports := make([]*AWSMeterReport, 0)
for _, dimension := range dimensions {
var userCount int64
var err error
switch dimension {
case model.AwsMeteringDimensionUsageHrs:
userCount, err = awsm.store.User().AnalyticsActiveCountForPeriod(model.GetMillisForTime(startTime), model.GetMillisForTime(endTime), model.UserCountOptions{})
if err != nil {
mlog.Warn("Failed to obtain usage data", mlog.String("dimension", dimension), mlog.String("start", startTime.String()), mlog.Int64("count", userCount), mlog.Err(err))
continue
}
default:
mlog.Debug("Dimension does not exist!", mlog.String("dimension", dimension))
continue
}
report := &AWSMeterReport{
Dimension: dimension,
Value: userCount,
Timestamp: startTime,
}
reports = append(reports, report)
}
return reports
}
func (awsm *AwsMeter) ReportUserCategoryUsage(reports []*AWSMeterReport) error {
for _, report := range reports {
err := sendReportToMeteringService(awsm.service, report)
if err != nil {
return err
}
}
return nil
}
func sendReportToMeteringService(ams *AWSMeterService, report *AWSMeterReport) error {
params := &marketplacemetering.MeterUsageInput{
DryRun: aws.Bool(ams.AwsDryRun),
ProductCode: aws.String(ams.AwsProductCode),
UsageDimension: aws.String(report.Dimension),
UsageQuantity: aws.Int64(report.Value),
Timestamp: aws.Time(report.Timestamp),
}
resp, err := ams.AwsMeteringSvc.MeterUsage(params)
if err != nil {
return errors.Wrap(err, "Invalid metering service id.")
}
if resp.MeteringRecordId == nil {
return errors.Wrap(err, "Invalid metering service id.")
}
mlog.Debug("Sent record to AWS metering service", mlog.String("dimension", report.Dimension), mlog.Int64("value", report.Value), mlog.String("timestamp", report.Timestamp.String()))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"container/list"
"sync"
"time"
"github.com/tinylib/msgp/msgp"
"github.com/vmihailenco/msgpack/v5"
"github.com/mattermost/mattermost-server/v6/model"
)
// LRU is a thread-safe fixed size LRU cache.
type LRU struct {
lock sync.RWMutex
size int
len int
currentGeneration int64
evictList *list.List
items map[string]*list.Element
defaultExpiry time.Duration
name string
invalidateClusterEvent model.ClusterEvent
}
// LRUOptions contains options for initializing LRU cache
type LRUOptions struct {
Name string
Size int
DefaultExpiry time.Duration
InvalidateClusterEvent model.ClusterEvent
// StripedBuckets is used only by LRUStriped and shouldn't be greater than the number
// of CPUs available on the machine running this cache.
StripedBuckets int
}
// entry is used to hold a value in the evictList.
type entry struct {
key string
value []byte
expires time.Time
generation int64
}
// NewLRU creates an LRU of the given size.
func NewLRU(opts LRUOptions) Cache {
return &LRU{
name: opts.Name,
size: opts.Size,
evictList: list.New(),
items: make(map[string]*list.Element, opts.Size),
defaultExpiry: opts.DefaultExpiry,
invalidateClusterEvent: opts.InvalidateClusterEvent,
}
}
// Purge is used to completely clear the cache.
func (l *LRU) Purge() error {
l.lock.Lock()
defer l.lock.Unlock()
l.len = 0
l.currentGeneration++
return nil
}
// Set adds the given key and value to the store without an expiry. If the key already exists,
// it will overwrite the previous value.
func (l *LRU) Set(key string, value any) error {
return l.SetWithExpiry(key, value, 0)
}
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
// the key already exists, it will overwrite the previous value
func (l *LRU) SetWithDefaultExpiry(key string, value any) error {
return l.SetWithExpiry(key, value, l.defaultExpiry)
}
// SetWithExpiry adds the given key and value to the cache with the given expiry. If the key
// already exists, it will overwrite the previous value
func (l *LRU) SetWithExpiry(key string, value any, ttl time.Duration) error {
return l.set(key, value, ttl)
}
// Get the content stored in the cache for the given key, and decode it into the value interface.
// return ErrKeyNotFound if the key is missing from the cache
func (l *LRU) Get(key string, value any) error {
return l.get(key, value)
}
// Remove deletes the value for a key.
func (l *LRU) Remove(key string) error {
l.lock.Lock()
defer l.lock.Unlock()
if ent, ok := l.items[key]; ok {
l.removeElement(ent)
}
return nil
}
// Keys returns a slice of the keys in the cache.
func (l *LRU) Keys() ([]string, error) {
l.lock.RLock()
defer l.lock.RUnlock()
keys := make([]string, l.len)
i := 0
for ent := l.evictList.Back(); ent != nil; ent = ent.Prev() {
e := ent.Value.(*entry)
if e.generation == l.currentGeneration {
keys[i] = e.key
i++
}
}
return keys, nil
}
// Len returns the number of items in the cache.
func (l *LRU) Len() (int, error) {
l.lock.RLock()
defer l.lock.RUnlock()
return l.len, nil
}
// GetInvalidateClusterEvent returns the cluster event configured when this cache was created.
func (l *LRU) GetInvalidateClusterEvent() model.ClusterEvent {
return l.invalidateClusterEvent
}
// Name returns the name of the cache
func (l *LRU) Name() string {
return l.name
}
func (l *LRU) set(key string, value any, ttl time.Duration) error {
var expires time.Time
if ttl > 0 {
expires = time.Now().Add(ttl)
}
var buf []byte
var err error
// We use a fast path for hot structs.
if msgpVal, ok := value.(msgp.Marshaler); ok {
buf, err = msgpVal.MarshalMsg(nil)
} else {
// Slow path for other structs.
buf, err = msgpack.Marshal(value)
}
if err != nil {
return err
}
l.lock.Lock()
defer l.lock.Unlock()
// Check for existing item, ignoring expiry since we'd update anyway.
if ent, ok := l.items[key]; ok {
l.evictList.MoveToFront(ent)
e := ent.Value.(*entry)
e.value = buf
e.expires = expires
if e.generation != l.currentGeneration {
e.generation = l.currentGeneration
l.len++
}
return nil
}
// Add new item
ent := &entry{key, buf, expires, l.currentGeneration}
entry := l.evictList.PushFront(ent)
l.items[key] = entry
l.len++
if l.evictList.Len() > l.size {
l.removeElement(l.evictList.Back())
}
return nil
}
func (l *LRU) get(key string, value any) error {
val, err := l.getItem(key)
if err != nil {
return err
}
// We use a fast path for hot structs.
if msgpVal, ok := value.(msgp.Unmarshaler); ok {
_, err := msgpVal.UnmarshalMsg(val)
return err
}
// This is ugly and makes the cache package aware of the model package.
// But this is due to 2 things.
// 1. The msgp package works on methods on structs rather than functions.
// 2. Our cache interface passes pointers to empty pointers, and not pointers
// to values. This is mainly how all our model structs are passed around.
// It might be technically possible to use values _just_ for hot structs
// like these and then return a pointer while returning from the cache function,
// but it will make the codebase inconsistent, and has some edge-cases to take care of.
switch v := value.(type) {
case **model.User:
var u model.User
_, err := u.UnmarshalMsg(val)
*v = &u
return err
case *map[string]*model.User:
var u model.UserMap
_, err := u.UnmarshalMsg(val)
*v = u
return err
}
// Slow path for other structs.
return msgpack.Unmarshal(val, value)
}
func (l *LRU) getItem(key string) ([]byte, error) {
l.lock.Lock()
defer l.lock.Unlock()
ent, ok := l.items[key]
if !ok {
return nil, ErrKeyNotFound
}
e := ent.Value.(*entry)
if e.generation != l.currentGeneration || (!e.expires.IsZero() && time.Now().After(e.expires)) {
l.removeElement(ent)
return nil, ErrKeyNotFound
}
l.evictList.MoveToFront(ent)
return e.value, nil
}
func (l *LRU) removeElement(e *list.Element) {
l.evictList.Remove(e)
kv := e.Value.(*entry)
if kv.generation == l.currentGeneration {
l.len--
}
delete(l.items, kv.key)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"fmt"
"math"
"time"
"github.com/cespare/xxhash/v2"
"github.com/mattermost/mattermost-server/v6/model"
)
// LRUStriped keeps LRU caches in buckets in order to lower mutex contention.
// This is achieved by hashing the input key to map it to a dedicated bucket.
// Each bucket (an LRU cache) has its own lock that helps distributing the lock
// contention on multiple threads/cores, leading to less wait times.
//
// LRUStriped implements the Cache interface with the same behavior as LRU.
//
// Note that, because of it's distributed nature, the fixed size cannot be strictly respected
// and you may have a tiny bit more space for keys than you defined through LRUOptions.
// Bucket size is computed as follows: (size / nbuckets) + (size % nbuckets)
//
// Because of this size limit per bucket, and because of the nature of the data, you
// may have buckets filled unevenly, and because of this, keys will be evicted from the entire
// cache where a simple LRU wouldn't have. Example:
//
// Two buckets B1 and B2, of max size 2 each, meaning, theoretically, a max size of 4:
// - Say you have a set of 3 keys, they could fill an entire LRU cache.
// - But if all those keys are assigned to a single bucket B1, the first key will be evicted from B1
// - B2 will remain empty, even though there was enough memory allocated
//
// With 4 buckets and random UUIDs as keys, the amount of false evictions is around 5%.
//
// By default, the number of buckets equals the number of cpus returned from runtime.NumCPU.
//
// This struct is lock-free and intended to be used without lock.
type LRUStriped struct {
buckets []*LRU
name string
invalidateClusterEvent model.ClusterEvent
}
func (L LRUStriped) hashkeyMapHash(key string) uint64 {
return xxhash.Sum64String(key)
}
func (L LRUStriped) keyBucket(key string) *LRU {
return L.buckets[L.hashkeyMapHash(key)%uint64(len(L.buckets))]
}
// Purge loops through each LRU cache for purging. Since LRUStriped doesn't use any lock,
// each LRU bucket is purged after another one, which means that keys could still
// be present after a call to Purge.
func (L LRUStriped) Purge() error {
for _, lru := range L.buckets {
lru.Purge() // errors from purging LRU can be ignored as they always return nil
}
return nil
}
// Set does the same as LRU.Set
func (L LRUStriped) Set(key string, value any) error {
return L.keyBucket(key).Set(key, value)
}
// SetWithDefaultExpiry does the same as LRU.SetWithDefaultExpiry
func (L LRUStriped) SetWithDefaultExpiry(key string, value any) error {
return L.keyBucket(key).SetWithDefaultExpiry(key, value)
}
// SetWithExpiry does the same as LRU.SetWithExpiry
func (L LRUStriped) SetWithExpiry(key string, value any, ttl time.Duration) error {
return L.keyBucket(key).SetWithExpiry(key, value, ttl)
}
// Get does the same as LRU.Get
func (L LRUStriped) Get(key string, value any) error {
return L.keyBucket(key).Get(key, value)
}
// Remove does the same as LRU.Remove
func (L LRUStriped) Remove(key string) error {
return L.keyBucket(key).Remove(key)
}
// Keys does the same as LRU.Keys. However, because this is lock-free, keys might be
// inserted or removed from a previously scanned LRU cache.
// This is not as precise as using a single LRU instance.
func (L LRUStriped) Keys() ([]string, error) {
var keys []string
for _, lru := range L.buckets {
k, _ := lru.Keys() // Keys never returns any error
keys = append(keys, k...)
}
return keys, nil
}
// Len does the same as LRU.Len. As for LRUStriped.Keys, this call cannot be precise.
func (L LRUStriped) Len() (int, error) {
var size int
for _, lru := range L.buckets {
s, _ := lru.Len() // Len never returns any error
size += s
}
return size, nil
}
// GetInvalidateClusterEvent does the same as LRU.GetInvalidateClusterEvent
func (L LRUStriped) GetInvalidateClusterEvent() model.ClusterEvent {
return L.invalidateClusterEvent
}
// Name does the same as LRU.Name
func (L LRUStriped) Name() string {
return L.name
}
// NewLRUStriped creates a striped LRU cache using the special LRUOptions.StripedBuckets value.
// See LRUStriped and LRUOptions for more details.
//
// Not that in order to prevent false eviction, this LRU cache adds 10% (computation is rounded up) of the
// requested size to the total cache size.
func NewLRUStriped(opts LRUOptions) (Cache, error) {
if opts.StripedBuckets == 0 {
return nil, fmt.Errorf("number of buckets is mandatory")
}
if opts.Size < opts.StripedBuckets {
return nil, fmt.Errorf("cache size must at least be equal to the number of buckets")
}
// add 10% to the total size, before splitting
opts.Size += int(math.Ceil(float64(opts.Size) * 10.0 / 100.0))
// now this is the size for each bucket
opts.Size = (opts.Size / opts.StripedBuckets) + (opts.Size % opts.StripedBuckets)
buckets := make([]*LRU, opts.StripedBuckets)
for i := 0; i < opts.StripedBuckets; i++ {
buckets[i] = NewLRU(opts).(*LRU)
}
return LRUStriped{
buckets: buckets,
invalidateClusterEvent: opts.InvalidateClusterEvent,
name: opts.Name,
}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
// CacheOptions contains options for initializing a cache
type CacheOptions struct {
Size int
DefaultExpiry time.Duration
Name string
InvalidateClusterEvent model.ClusterEvent
Striped bool
StripedBuckets int
}
// Provider is a provider for Cache
type Provider interface {
// NewCache creates a new cache with given options.
NewCache(opts *CacheOptions) (Cache, error)
// Connect opens a new connection to the cache using specific provider parameters.
Connect() error
// Close releases any resources used by the cache provider.
Close() error
}
type cacheProvider struct {
}
// NewProvider creates a new CacheProvider
func NewProvider() Provider {
return &cacheProvider{}
}
// NewCache creates a new cache with given opts
func (c *cacheProvider) NewCache(opts *CacheOptions) (Cache, error) {
if opts.Striped {
return NewLRUStriped(LRUOptions{
Name: opts.Name,
Size: opts.Size,
DefaultExpiry: opts.DefaultExpiry,
InvalidateClusterEvent: opts.InvalidateClusterEvent,
StripedBuckets: opts.StripedBuckets,
})
}
return NewLRU(LRUOptions{
Name: opts.Name,
Size: opts.Size,
DefaultExpiry: opts.DefaultExpiry,
InvalidateClusterEvent: opts.InvalidateClusterEvent,
}), nil
}
// Connect opens a new connection to the cache using specific provider parameters.
func (c *cacheProvider) Connect() error {
return nil
}
// Close releases any resources used by the cache provider.
func (c *cacheProvider) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/mholt/archiver/v3"
)
type archiveExtractor struct {
SubExtractor Extractor
}
func (ae *archiveExtractor) Match(filename string) bool {
_, err := archiver.ByExtension(filename)
return err == nil
}
func (ae *archiveExtractor) Extract(name string, r io.ReadSeeker) (string, error) {
dir, err := os.MkdirTemp(os.TempDir(), "archiver")
if err != nil {
return "", fmt.Errorf("error creating temporary file: %v", err)
}
defer os.RemoveAll(dir)
f, err := os.Create(filepath.Join(dir, name))
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
_, err = io.Copy(f, r)
f.Close()
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
var text strings.Builder
err = archiver.Walk(f.Name(), func(file archiver.File) error {
text.WriteString(file.Name() + " ")
if ae.SubExtractor != nil {
filename := filepath.Base(file.Name())
filename = strings.ReplaceAll(filename, "-", " ")
filename = strings.ReplaceAll(filename, ".", " ")
filename = strings.ReplaceAll(filename, ",", " ")
data, err2 := io.ReadAll(file)
if err2 != nil {
return err2
}
subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data))
if extractErr == nil {
text.WriteString(subtext + " ")
}
}
return nil
})
if err != nil {
return "", err
}
return text.String(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type combineExtractor struct {
SubExtractors []Extractor
}
func (ce *combineExtractor) Add(extractor Extractor) {
ce.SubExtractors = append(ce.SubExtractors, extractor)
}
func (ce *combineExtractor) Match(filename string) bool {
for _, extractor := range ce.SubExtractors {
if extractor.Match(filename) {
return true
}
}
return false
}
func (ce *combineExtractor) Extract(filename string, r io.ReadSeeker) (string, error) {
for _, extractor := range ce.SubExtractors {
if extractor.Match(filename) {
r.Seek(0, io.SeekStart)
text, err := extractor.Extract(filename, r)
if err != nil {
mlog.Warn("unable to extract file content", mlog.Err(err))
continue
}
return text, nil
}
}
return "", nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
)
// ExtractSettings defines the features enabled/disable during the document text extraction.
type ExtractSettings struct {
ArchiveRecursion bool
MMPreviewURL string
MMPreviewSecret string
}
// Extract extract the text from a document using the system default extractors
func Extract(filename string, r io.ReadSeeker, settings ExtractSettings) (string, error) {
return ExtractWithExtraExtractors(filename, r, settings, []Extractor{})
}
// ExtractWithExtraExtractors extract the text from a document using the provided extractors beside the system default extractors.
func ExtractWithExtraExtractors(filename string, r io.ReadSeeker, settings ExtractSettings, extraExtractors []Extractor) (string, error) {
enabledExtractors := &combineExtractor{}
for _, extraExtractor := range extraExtractors {
enabledExtractors.Add(extraExtractor)
}
enabledExtractors.Add(&documentExtractor{})
enabledExtractors.Add(&pdfExtractor{})
if settings.ArchiveRecursion {
enabledExtractors.Add(&archiveExtractor{SubExtractor: enabledExtractors})
} else {
enabledExtractors.Add(&archiveExtractor{})
}
if settings.MMPreviewURL != "" {
enabledExtractors.Add(newMMPreviewExtractor(settings.MMPreviewURL, settings.MMPreviewSecret, pdfExtractor{}))
}
enabledExtractors.Add(&plainExtractor{})
if enabledExtractors.Match(filename) {
return enabledExtractors.Extract(filename, r)
}
return "", nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"errors"
"io"
"path"
"strings"
"code.sajari.com/docconv"
)
type documentExtractor struct{}
var doconvConverterByExtensions = map[string]func(io.Reader) (string, map[string]string, error){
"doc": docconv.ConvertDoc,
"docx": docconv.ConvertDocx,
"pptx": docconv.ConvertPptx,
"odt": docconv.ConvertODT,
"html": func(r io.Reader) (string, map[string]string, error) { return docconv.ConvertHTML(r, true) },
// Temporarily disabled to avoid crashes on malicious .pages files
// "pages": docconv.ConvertPages,
"rtf": docconv.ConvertRTF,
"pdf": docconv.ConvertPDF,
}
func (de *documentExtractor) Match(filename string) bool {
extension := strings.TrimPrefix(path.Ext(filename), ".")
_, ok := doconvConverterByExtensions[extension]
return ok
}
func (de *documentExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) {
defer func() {
if r := recover(); r != nil {
out = ""
outErr = errors.New("error extracting document text")
}
}()
extension := strings.TrimPrefix(path.Ext(filename), ".")
converter, ok := doconvConverterByExtensions[extension]
if !ok {
return "", errors.New("unknown converter")
}
text, _, err := converter(r)
if err != nil {
return "", err
}
return text, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
// MMPreview is a micro-service to convert from any libreoffice supported
// format into a PDF file, and then we use the regular pdf extractor to convert
// it into plain text.
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"path"
"strings"
"github.com/pkg/errors"
)
type mmPreviewExtractor struct {
url string
secret string
pdfExtractor pdfExtractor
}
var mmpreviewSupportedExtensions = map[string]bool{
"ppt": true,
"odp": true,
"xls": true,
"xlsx": true,
"ods": true,
}
func newMMPreviewExtractor(url string, secret string, pdfExtractor pdfExtractor) *mmPreviewExtractor {
return &mmPreviewExtractor{url: url, secret: secret, pdfExtractor: pdfExtractor}
}
func (mpe *mmPreviewExtractor) Match(filename string) bool {
extension := strings.TrimPrefix(path.Ext(filename), ".")
return mmpreviewSupportedExtensions[extension]
}
func (mpe *mmPreviewExtractor) Extract(filename string, file io.ReadSeeker) (string, error) {
b, w, err := createMultipartFormData("file", filename, file)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
req, err := http.NewRequest("POST", mpe.url+"/toPDF", &b)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
req.Header.Set("Content-Type", w.FormDataContentType())
if mpe.secret != "" {
req.Header.Add("Authentication", mpe.secret)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", errors.New("Unable to generate file preview using mmpreview (The server has replied with an error)")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "unable to read the response from mmpreview")
}
return mpe.pdfExtractor.Extract(filename, bytes.NewReader(data))
}
func createMultipartFormData(fieldName, fileName string, fileData io.ReadSeeker) (bytes.Buffer, *multipart.Writer, error) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
var fw io.Writer
if fw, err = w.CreateFormFile(fieldName, fileName); err != nil {
return b, nil, err
}
if _, err = io.Copy(fw, fileData); err != nil {
return b, nil, err
}
w.Close()
return b, w, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/ledongthuc/pdf"
)
type pdfExtractor struct{}
func (pe *pdfExtractor) Match(filename string) bool {
supportedExtensions := map[string]bool{
"pdf": true,
}
extension := strings.TrimPrefix(path.Ext(filename), ".")
return supportedExtensions[extension]
}
func (pe *pdfExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) {
defer func() {
if r := recover(); r != nil {
out = ""
outErr = errors.New("error extracting pdf text")
}
}()
f, err := os.CreateTemp(os.TempDir(), "pdflib")
if err != nil {
return "", fmt.Errorf("error creating temporary file: %v", err)
}
defer f.Close()
defer os.Remove(f.Name())
size, err := io.Copy(f, r)
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
reader, err := pdf.NewReader(f, size)
if err != nil {
return "", err
}
var buf bytes.Buffer
b, err := reader.GetPlainText()
if err != nil {
return "", err
}
buf.ReadFrom(b)
return buf.String(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
"unicode"
"unicode/utf8"
)
type plainExtractor struct{}
func (pe *plainExtractor) Match(filename string) bool {
return true
}
func (pe *plainExtractor) Extract(filename string, r io.ReadSeeker) (string, error) {
// This detects any visible character plus any whitespace
validRanges := append(unicode.GraphicRanges, unicode.White_Space)
runes := make([]byte, 1024)
total, err := r.Read(runes)
if err != nil && err != io.EOF {
return "", err
}
if total == 0 {
return "", nil
}
count := 0
for {
c, size := utf8.DecodeRune(runes[count:])
if !unicode.In(c, validRanges...) {
return "", nil
}
if size == 0 {
break
}
count += size
// subtract the max rune size to prevent accidentally splitted runes at the end of first 1024 bytes
if count > total-utf8.UTFMax {
break
}
}
text, _ := io.ReadAll(r)
return string(runes[0:total]) + string(text), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"time"
)
const (
ConnectTimeout = 3 * time.Second
RequestTimeout = 30 * time.Second
)
var reservedIPRanges []*net.IPNet
// IsReservedIP checks whether the target IP belongs to reserved IP address ranges to avoid SSRF attacks to the internal
// network of the Mattermost server
func IsReservedIP(ip net.IP) bool {
for _, ipRange := range reservedIPRanges {
if ipRange.Contains(ip) {
return true
}
}
return false
}
// IsOwnIP handles the special case that a request might be made to the public IP of the host which on Linux is routed
// directly via the loopback IP to any listening sockets, effectively bypassing host-based firewalls such as firewalld
func IsOwnIP(ip net.IP) (bool, error) {
interfaces, err := net.Interfaces()
if err != nil {
return false, err
}
for _, interf := range interfaces {
addresses, err := interf.Addrs()
if err != nil {
return false, err
}
for _, addr := range addresses {
var selfIP net.IP
switch v := addr.(type) {
case *net.IPNet:
selfIP = v.IP
case *net.IPAddr:
selfIP = v.IP
}
if ip.Equal(selfIP) {
return true, nil
}
}
}
return false, nil
}
var defaultUserAgent string
func init() {
for _, cidr := range []string{
// See https://tools.ietf.org/html/rfc6890
"0.0.0.0/8", // This host on this network
"10.0.0.0/8", // Private-Use
"127.0.0.0/8", // Loopback
"169.254.0.0/16", // Link Local
"172.16.0.0/12", // Private-Use Networks
"192.168.0.0/16", // Private-Use Networks
"::/128", // Unspecified Address
"::1/128", // Loopback Address
"fc00::/7", // Unique-Local
"fe80::/10", // Linked-Scoped Unicast
} {
_, parsed, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
reservedIPRanges = append(reservedIPRanges, parsed)
}
defaultUserAgent = "Mattermost-Bot/1.1"
}
type DialContextFunction func(ctx context.Context, network, addr string) (net.Conn, error)
var AddressForbidden error = errors.New("address forbidden, you may need to set AllowedUntrustedInternalConnections to allow an integration access to your internal network")
func dialContextFilter(dial DialContextFunction, allowHost func(host string) bool, allowIP func(ip net.IP) bool) DialContextFunction {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if allowHost != nil && allowHost(host) {
return dial(ctx, network, addr)
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
var firstErr error
for _, ip := range ips {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
if allowIP == nil || !allowIP(ip) {
continue
}
conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port))
if err == nil {
return conn, nil
}
if firstErr == nil {
firstErr = err
}
}
if firstErr == nil {
return nil, AddressForbidden
}
return nil, firstErr
}
}
func NewTransport(enableInsecureConnections bool, allowHost func(host string) bool, allowIP func(ip net.IP) bool) *MattermostTransport {
dialContext := (&net.Dialer{
Timeout: ConnectTimeout,
KeepAlive: 30 * time.Second,
}).DialContext
if allowHost != nil || allowIP != nil {
dialContext = dialContextFilter(dialContext, allowHost, allowIP)
}
return &MattermostTransport{
&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: ConnectTimeout,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: enableInsecureConnections,
},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"net"
"net/http"
"strings"
"time"
"unicode"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
)
// HTTPService wraps the functionality for making http requests to provide some improvements to the default client
// behaviour.
type HTTPService interface {
// MakeClient returns an http client constructed with a RoundTripper as returned by MakeTransport.
MakeClient(trustURLs bool) *http.Client
// MakeTransport returns a RoundTripper that is suitable for making requests to external resources. The default
// implementation provides:
// - A shorter timeout for dial and TLS handshake (defined as constant "ConnectTimeout")
// - A timeout for end-to-end requests
// - A Mattermost-specific user agent header
// - Additional security for untrusted and insecure connections
MakeTransport(trustURLs bool) *MattermostTransport
}
type HTTPServiceImpl struct {
configService configservice.ConfigService
RequestTimeout time.Duration
}
func splitFields(c rune) bool {
return unicode.IsSpace(c) || c == ','
}
func MakeHTTPService(configService configservice.ConfigService) HTTPService {
return &HTTPServiceImpl{
configService,
RequestTimeout,
}
}
func (h *HTTPServiceImpl) MakeClient(trustURLs bool) *http.Client {
return &http.Client{
Transport: h.MakeTransport(trustURLs),
Timeout: h.RequestTimeout,
}
}
func (h *HTTPServiceImpl) MakeTransport(trustURLs bool) *MattermostTransport {
insecure := h.configService.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *h.configService.Config().ServiceSettings.EnableInsecureOutgoingConnections
if trustURLs {
return NewTransport(insecure, nil, nil)
}
allowHost := func(host string) bool {
if h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
for _, allowed := range strings.FieldsFunc(*h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if host == allowed {
return true
}
}
return false
}
allowIP := func(ip net.IP) bool {
reservedIP := IsReservedIP(ip)
ownIP, err := IsOwnIP(ip)
// If there is an error getting the self-assigned IPs, default to the secure option
if err != nil {
return false
}
// If it's not a reserved IP and it's not self-assigned IP, accept the IP
if !reservedIP && !ownIP {
return true
}
if h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
// In the case it's the self-assigned IP, enforce that it needs to be explicitly added to the AllowedUntrustedInternalConnections
for _, allowed := range strings.FieldsFunc(*h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) {
return true
}
}
return false
}
return NewTransport(insecure, allowHost, allowIP)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"net/http"
)
// MattermostTransport is an implementation of http.RoundTripper that ensures each request contains a custom user agent
// string to indicate that the request is coming from a Mattermost instance.
type MattermostTransport struct {
// Transport is the underlying http.RoundTripper that is actually used to make the request
Transport http.RoundTripper
}
func (t *MattermostTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", defaultUserAgent)
return t.Transport.RoundTrip(req)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"io"
"net/http"
"net/url"
)
type AtmosCamoBackend struct {
proxy *ImageProxy
siteURL *url.URL
remoteURL *url.URL
client *http.Client
}
func makeAtmosCamoBackend(proxy *ImageProxy) *AtmosCamoBackend {
// We deliberately ignore the error because it's from config.json.
// The function returns a nil pointer in case of error, and we handle it when it's used.
siteURL, _ := url.Parse(*proxy.ConfigService.Config().ServiceSettings.SiteURL)
remoteURL, _ := url.Parse(*proxy.ConfigService.Config().ImageProxySettings.RemoteImageProxyURL)
return &AtmosCamoBackend{
proxy: proxy,
siteURL: siteURL,
remoteURL: remoteURL,
client: proxy.HTTPService.MakeClient(false),
}
}
func (backend *AtmosCamoBackend) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
http.Redirect(w, r, backend.getAtmosCamoImageURL(imageURL), http.StatusFound)
}
func (backend *AtmosCamoBackend) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
req, err := http.NewRequest("GET", backend.getAtmosCamoImageURL(imageURL), nil)
if err != nil {
return nil, "", Error{err}
}
resp, err := backend.client.Do(req)
if err != nil {
return nil, "", Error{err}
}
// Note that we don't do any additional validation of the received data since we expect the image proxy to do that
return resp.Body, resp.Header.Get("Content-Type"), nil
}
func (backend *AtmosCamoBackend) getAtmosCamoImageURL(imageURL string) string {
cfg := *backend.proxy.ConfigService.Config()
options := *cfg.ImageProxySettings.RemoteImageProxyOptions
if imageURL == "" || backend.siteURL == nil {
return imageURL
}
// Parse url, return siteURL in case of failure.
// Also if the URL is opaque.
parsedURL, err := url.Parse(imageURL)
if err != nil || parsedURL.Opaque != "" {
return backend.siteURL.String()
}
// If host is same as siteURL host/ remoteURL host, return.
if parsedURL.Host == backend.siteURL.Host || parsedURL.Host == backend.remoteURL.Host {
return parsedURL.String()
}
// Handle protocol-relative URLs.
if parsedURL.Scheme == "" {
parsedURL.Scheme = backend.siteURL.Scheme
}
// If it's a relative URL, fill up the hostname and scheme and return.
if parsedURL.Host == "" {
parsedURL.Host = backend.siteURL.Host
return parsedURL.String()
}
urlBytes := []byte(parsedURL.String())
mac := hmac.New(sha1.New, []byte(options))
mac.Write(urlBytes)
digest := hex.EncodeToString(mac.Sum(nil))
return backend.remoteURL.String() + "/" + digest + "/" + hex.EncodeToString(urlBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"errors"
"io"
"net/http"
"net/url"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrNotEnabled = Error{errors.New("imageproxy.ImageProxy: image proxy not enabled")}
// An ImageProxy is the public interface for Mattermost's image proxy. An instance of ImageProxy should be created
// using MakeImageProxy which requires a configService and an HTTPService provided by the server.
type ImageProxy struct {
ConfigService configservice.ConfigService
configListenerID string
HTTPService httpservice.HTTPService
Logger *mlog.Logger
siteURL *url.URL
lock sync.RWMutex
backend ImageProxyBackend
}
// An ImageProxyBackend provides the functionality for different types of image proxies. An ImageProxy will construct
// the required backend depending on the ImageProxySettings provided by the ConfigService.
type ImageProxyBackend interface {
// GetImage provides a proxied image in response to an HTTP request.
GetImage(w http.ResponseWriter, r *http.Request, imageURL string)
// GetImageDirect returns a proxied image along with its content type.
GetImageDirect(imageURL string) (io.ReadCloser, string, error)
}
func MakeImageProxy(configService configservice.ConfigService, httpService httpservice.HTTPService, logger *mlog.Logger) *ImageProxy {
proxy := &ImageProxy{
ConfigService: configService,
HTTPService: httpService,
Logger: logger,
}
// We deliberately ignore the error because it's from config.json.
// The function returns a nil pointer in case of error, and we handle it when it's used.
siteURL, _ := url.Parse(*configService.Config().ServiceSettings.SiteURL)
proxy.siteURL = siteURL
proxy.configListenerID = proxy.ConfigService.AddConfigListener(proxy.OnConfigChange)
config := proxy.ConfigService.Config()
proxy.backend = proxy.makeBackend(*config.ImageProxySettings.Enable, *config.ImageProxySettings.ImageProxyType)
return proxy
}
func (proxy *ImageProxy) makeBackend(enable bool, proxyType string) ImageProxyBackend {
if !enable {
return nil
}
switch proxyType {
case model.ImageProxyTypeLocal:
return makeLocalBackend(proxy)
case model.ImageProxyTypeAtmosCamo:
return makeAtmosCamoBackend(proxy)
default:
return nil
}
}
func (proxy *ImageProxy) Close() {
proxy.lock.Lock()
defer proxy.lock.Unlock()
proxy.ConfigService.RemoveConfigListener(proxy.configListenerID)
}
func (proxy *ImageProxy) OnConfigChange(oldConfig, newConfig *model.Config) {
if *oldConfig.ImageProxySettings.Enable != *newConfig.ImageProxySettings.Enable ||
*oldConfig.ImageProxySettings.ImageProxyType != *newConfig.ImageProxySettings.ImageProxyType {
proxy.lock.Lock()
defer proxy.lock.Unlock()
proxy.backend = proxy.makeBackend(*newConfig.ImageProxySettings.Enable, *newConfig.ImageProxySettings.ImageProxyType)
}
}
// GetImage takes an HTTP request for an image and requests that image using the image proxy.
func (proxy *ImageProxy) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
proxy.lock.RLock()
defer proxy.lock.RUnlock()
if proxy.backend == nil {
w.WriteHeader(http.StatusNotImplemented)
return
}
proxy.backend.GetImage(w, r, imageURL)
}
// GetImageDirect takes the URL of an image and returns the image along with its content type.
func (proxy *ImageProxy) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
proxy.lock.RLock()
defer proxy.lock.RUnlock()
if proxy.backend == nil {
return nil, "", ErrNotEnabled
}
return proxy.backend.GetImageDirect(imageURL)
}
// GetProxiedImageURL takes the URL of an image and returns a URL that can be used to view that image through the
// image proxy.
func (proxy *ImageProxy) GetProxiedImageURL(imageURL string) string {
if imageURL == "" || proxy.siteURL == nil {
return imageURL
}
// Parse url, return siteURL in case of failure.
// Also if the URL is opaque.
parsedURL, err := url.Parse(imageURL)
if err != nil || parsedURL.Opaque != "" {
return proxy.siteURL.String()
}
// If host is same as siteURL host, return.
if parsedURL.Host == proxy.siteURL.Host {
return parsedURL.String()
}
// Handle protocol-relative URLs.
if parsedURL.Scheme == "" {
parsedURL.Scheme = proxy.siteURL.Scheme
}
// If it's a relative URL, fill up the hostname and return.
if parsedURL.Host == "" {
parsedURL.Host = proxy.siteURL.Host
return parsedURL.String()
}
return proxy.siteURL.String() + "/api/v4/image?url=" + url.QueryEscape(parsedURL.String())
}
// GetUnproxiedImageURL takes the URL of an image on the image proxy and returns the original URL of the image.
func (proxy *ImageProxy) GetUnproxiedImageURL(proxiedURL string) string {
return getUnproxiedImageURL(proxiedURL, *proxy.ConfigService.Config().ServiceSettings.SiteURL)
}
func getUnproxiedImageURL(proxiedURL, siteURL string) string {
if !strings.HasPrefix(proxiedURL, siteURL+"/api/v4/image?url=") {
return proxiedURL
}
parsed, err := url.Parse(proxiedURL)
if err != nil {
return proxiedURL
}
u := parsed.Query()["url"]
if len(u) == 0 {
return proxiedURL
}
return u[0]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"bufio"
"errors"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var imageContentTypes = []string{
"image/bmp", "image/cgm", "image/g3fax", "image/gif", "image/ief", "image/jp2",
"image/jpeg", "image/jpg", "image/pict", "image/png", "image/prs.btif", "image/svg+xml",
"image/tiff", "image/vnd.adobe.photoshop", "image/vnd.djvu", "image/vnd.dwg",
"image/vnd.dxf", "image/vnd.fastbidsheet", "image/vnd.fpx", "image/vnd.fst",
"image/vnd.fujixerox.edmics-mmr", "image/vnd.fujixerox.edmics-rlc",
"image/vnd.microsoft.icon", "image/vnd.ms-modi", "image/vnd.net-fpx", "image/vnd.wap.wbmp",
"image/vnd.xiff", "image/webp", "image/x-cmu-raster", "image/x-cmx", "image/x-icon",
"image/x-macpaint", "image/x-pcx", "image/x-pict", "image/x-portable-anymap",
"image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap",
"image/x-quicktime", "image/x-rgb", "image/x-xbitmap", "image/x-xpixmap", "image/x-xwindowdump",
}
var msgNotAllowed = "requested URL is not allowed"
var ErrLocalRequestFailed = Error{errors.New("imageproxy.LocalBackend: failed to request proxied image")}
type LocalBackend struct {
proxy *ImageProxy
client *http.Client
baseURL *url.URL
}
// URLError reports a malformed URL error.
type URLError struct {
Message string
URL *url.URL
}
func (e URLError) Error() string {
return fmt.Sprintf("malformed URL %q: %s", e.URL, e.Message)
}
func makeLocalBackend(proxy *ImageProxy) *LocalBackend {
baseURL, err := url.Parse(*proxy.ConfigService.Config().ServiceSettings.SiteURL)
if err != nil {
mlog.Warn("Failed to set base URL for image proxy. Relative image links may not work.", mlog.Err(err))
}
client := proxy.HTTPService.MakeClient(false)
return &LocalBackend{
proxy: proxy,
client: client,
baseURL: baseURL,
}
}
type contentTypeRecorder struct {
http.ResponseWriter
filename string
}
func (rec *contentTypeRecorder) WriteHeader(code int) {
hdr := rec.ResponseWriter.Header()
contentType := hdr.Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
// The error is caused by a malformed input and there's not much use logging it.
// Therefore, even in the error case we set it to attachment mode to be safe.
if err != nil || mediaType == "image/svg+xml" {
hdr.Set("Content-Disposition", fmt.Sprintf("attachment;filename=%q", rec.filename))
}
rec.ResponseWriter.WriteHeader(code)
}
func (backend *LocalBackend) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
// The interface to the proxy only exposes a ServeHTTP method, so fake a request to it
req, err := http.NewRequest(http.MethodGet, "/"+imageURL, nil)
if err != nil {
// http.NewRequest should only return an error on an invalid URL
mlog.Debug("Failed to create request for proxied image", mlog.String("url", imageURL), mlog.Err(err))
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte{})
return
}
u, err := url.Parse(imageURL)
if err != nil {
mlog.Debug("Failed to parse URL for proxied image", mlog.String("url", imageURL), mlog.Err(err))
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte{})
return
}
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Security-Policy", "default-src 'none'; img-src data:; style-src 'unsafe-inline'")
rec := contentTypeRecorder{w, filepath.Base(u.Path)}
backend.ServeImage(&rec, req)
}
func (backend *LocalBackend) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
// The interface to the proxy only exposes a ServeHTTP method, so fake a request to it
req, err := http.NewRequest(http.MethodGet, "/"+imageURL, nil)
if err != nil {
return nil, "", Error{err}
}
recorder := httptest.NewRecorder()
backend.ServeImage(recorder, req)
if recorder.Code != http.StatusOK {
return nil, "", ErrLocalRequestFailed
}
return io.NopCloser(recorder.Body), recorder.Header().Get("Content-Type"), nil
}
func (backend *LocalBackend) ServeImage(w http.ResponseWriter, req *http.Request) {
proxyReq, err := newProxyRequest(req, backend.baseURL)
if err != nil {
http.Error(w, fmt.Sprintf("invalid request URL: %v", err), http.StatusBadRequest)
return
}
actualReq, err := http.NewRequest("GET", proxyReq.String(), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
actualReq.Header.Set("Accept", strings.Join(imageContentTypes, ", "))
resp, err := backend.client.Do(actualReq)
if err != nil {
msg := fmt.Sprintf("error fetching remote image: %v", err)
mlog.Warn(msg)
statusCode := http.StatusInternalServerError
if e, ok := err.(net.Error); ok && e.Timeout() {
statusCode = http.StatusGatewayTimeout
}
http.Error(w, msg, statusCode)
return
}
// close the original resp.Body, even if we wrap it in a NopCloser below
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header, "Cache-Control", "Last-Modified", "Expires", "Etag", "Link")
if should304(req, resp) {
w.WriteHeader(http.StatusNotModified)
return
}
contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if contentType == "" || contentType == "application/octet-stream" || contentType == "binary/octet-stream" {
// try to detect content type
b := bufio.NewReader(resp.Body)
resp.Body = io.NopCloser(b)
contentType = peekContentType(b)
}
if resp.ContentLength != 0 && !contentTypeMatches(imageContentTypes, contentType) {
http.Error(w, msgNotAllowed, http.StatusForbidden)
return
}
w.Header().Set("Content-Type", contentType)
copyHeader(w.Header(), resp.Header, "Content-Length")
// Enable CORS for 3rd party applications
w.Header().Set("Access-Control-Allow-Origin", "*")
// Add a Content-Security-Policy to prevent stored-XSS attacks via SVG files
w.Header().Set("Content-Security-Policy", "script-src 'none'")
// Disable Content-Type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Block potential XSS attacks especially in legacy browsers which do not support CSP
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
mlog.Warn("error copying response", mlog.Err(err))
}
}
// copyHeader copies header values from src to dst, adding to any existing
// values with the same header name. If keys is not empty, only those header
// keys will be copied.
func copyHeader(dst, src http.Header, keys ...string) {
if len(keys) == 0 {
for k := range src {
keys = append(keys, k)
}
}
for _, key := range keys {
k := http.CanonicalHeaderKey(key)
for _, v := range src[k] {
dst.Add(k, v)
}
}
}
func should304(req *http.Request, resp *http.Response) bool {
etag := resp.Header.Get("Etag")
if etag != "" && etag == req.Header.Get("If-None-Match") {
return true
}
lastModified, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified"))
if err != nil {
return false
}
ifModSince, err := time.Parse(time.RFC1123, req.Header.Get("If-Modified-Since"))
if err != nil {
return false
}
if lastModified.Before(ifModSince) || lastModified.Equal(ifModSince) {
return true
}
return false
}
// peekContentType peeks at the first 512 bytes of p, and attempts to detect
// the content type. Returns empty string if error occurs.
func peekContentType(p *bufio.Reader) string {
byt, err := p.Peek(512)
if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
return ""
}
return http.DetectContentType(byt)
}
// contentTypeMatches returns whether contentType matches one of the allowed patterns.
func contentTypeMatches(patterns []string, contentType string) bool {
if len(patterns) == 0 {
return true
}
for _, pattern := range patterns {
if ok, err := path.Match(pattern, contentType); ok && err == nil {
return true
}
}
return false
}
// proxyRequest is an imageproxy request which includes a remote URL of an image to
// proxy.
type proxyRequest struct {
URL *url.URL // URL of the image to proxy
Original *http.Request // The original HTTP request
}
// String returns the request URL as a string, with r.Options encoded in the
// URL fragment.
func (r proxyRequest) String() string {
return r.URL.String()
}
func newProxyRequest(r *http.Request, baseURL *url.URL) (*proxyRequest, error) {
var err error
req := &proxyRequest{Original: r}
path := r.URL.EscapedPath()[1:] // strip leading slash
req.URL, err = parseURL(path)
if err != nil || !req.URL.IsAbs() {
// first segment should be options
parts := strings.SplitN(path, "/", 2)
if len(parts) != 2 {
return nil, URLError{"too few path segments", r.URL}
}
var err error
req.URL, err = parseURL(parts[1])
if err != nil {
return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL}
}
}
if baseURL != nil {
req.URL = baseURL.ResolveReference(req.URL)
}
if !req.URL.IsAbs() {
return nil, URLError{"must provide absolute remote URL", r.URL}
}
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return nil, URLError{"remote URL must have http or https scheme", r.URL}
}
// query string is always part of the remote URL
req.URL.RawQuery = r.URL.RawQuery
return req, nil
}
var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`)
// parseURL parses s as a URL, handling URLs that have been munged by
// path.Clean or a webserver that collapses multiple slashes.
func parseURL(s string) (*url.URL, error) {
s = reCleanedURL.ReplaceAllString(s, "$1://$2")
return url.Parse(s)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package marketplace
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
)
// Client is the programmatic interface to the marketplace server API.
type Client struct {
address string
httpClient *http.Client
}
// NewClient creates a client to the marketplace server at the given address.
func NewClient(address string, httpService httpservice.HTTPService) (*Client, error) {
var httpClient *http.Client
addressURL, err := url.Parse(address)
if err != nil {
return nil, errors.Wrap(err, "failed to parse marketplace address")
}
if addressURL.Hostname() == "localhost" || addressURL.Hostname() == "127.0.0.1" {
httpClient = httpService.MakeClient(true)
} else {
httpClient = httpService.MakeClient(false)
}
return &Client{
address: address,
httpClient: httpClient,
}, nil
}
// GetPlugins fetches the list of plugins from the configured server.
func (c *Client) GetPlugins(request *model.MarketplacePluginFilter) ([]*model.BaseMarketplacePlugin, error) {
u, err := url.Parse(c.buildURL("/api/v1/plugins"))
if err != nil {
return nil, err
}
request.ApplyToURL(u)
resp, err := c.doGet(u.String())
if err != nil {
return nil, err
}
defer closeBody(resp)
switch resp.StatusCode {
case http.StatusOK:
return model.BaseMarketplacePluginsFromReader(resp.Body)
default:
return nil, errors.Errorf("failed with status code %d", resp.StatusCode)
}
}
func (c *Client) GetPlugin(filter *model.MarketplacePluginFilter, pluginVersion string) (*model.BaseMarketplacePlugin, error) {
filter.ReturnAllVersions = true
if filter.PluginId == "" {
return nil, errors.New("missing pluginID")
}
if pluginVersion == "" {
return nil, errors.New("missing pluginVersion")
}
plugins, err := c.GetPlugins(filter)
if err != nil {
return nil, err
}
for _, plugin := range plugins {
if plugin.Manifest.Version == pluginVersion {
return plugin, nil
}
}
return nil, errors.New("plugin not found")
}
func (c *Client) GetLatestPlugin(filter *model.MarketplacePluginFilter) (*model.BaseMarketplacePlugin, error) {
filter.ReturnAllVersions = false
if filter.PluginId == "" {
return nil, errors.New("no pluginID provided")
}
plugins, err := c.GetPlugins(filter)
if err != nil {
return nil, err
}
if len(plugins) == 0 {
return nil, errors.New("plugin not found")
}
if len(plugins) > 1 {
return nil, errors.Errorf("unexpectedly more then one plugin was returned from the marketplace")
}
return plugins[0], nil
}
// closeBody ensures the Body of an http.Response is properly closed.
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
func (c *Client) buildURL(urlPath string, args ...any) string {
return fmt.Sprintf("%s/%s", strings.TrimRight(c.address, "/"), strings.TrimLeft(fmt.Sprintf(urlPath, args...), "/"))
}
func (c *Client) doGet(u string) (*http.Response, error) {
return c.httpClient.Get(u)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import "fmt"
type BufferFullError struct {
capacity int
}
func NewBufferFullError(capacity int) BufferFullError {
return BufferFullError{
capacity: capacity,
}
}
func (e BufferFullError) Capacity() int {
return e.capacity
}
func (e BufferFullError) Error() string {
return fmt.Sprintf("buffer capacity (%d) exceeded", e.capacity)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
)
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
rc := &model.RemoteCluster{
RemoteId: invite.RemoteId,
RemoteTeamId: invite.RemoteTeamId,
Name: name,
DisplayName: displayName,
Token: model.NewId(),
RemoteToken: invite.Token,
SiteURL: invite.SiteURL,
CreatorId: creatorId,
}
rcSaved, err := rcs.server.GetStore().RemoteCluster().Save(rc)
if err != nil {
return nil, err
}
// confirm the invitation with the originating site
frame, err := makeConfirmFrame(rcSaved, teamId, siteURL)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/%s", rcSaved.SiteURL, ConfirmInviteURL)
resp, err := rcs.sendFrameToRemote(PingTimeout, rc, frame, url)
if err != nil {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, err
}
var response Response
err = json.Unmarshal(resp, &response)
if err != nil {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, fmt.Errorf("invalid response from remote server: %w", err)
}
if !response.IsSuccess() {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, errors.New(response.Err)
}
// issue the first ping right away. The goroutine will exit when ping completes or PingTimeout exceeded.
go rcs.pingRemote(rcSaved)
return rcSaved, nil
}
func makeConfirmFrame(rc *model.RemoteCluster, teamId string, siteURL string) (*model.RemoteClusterFrame, error) {
confirm := model.RemoteClusterInvite{
RemoteId: rc.RemoteId,
RemoteTeamId: teamId,
SiteURL: siteURL,
Token: rc.Token,
}
confirmRaw, err := json.Marshal(confirm)
if err != nil {
return nil, err
}
msg := model.NewRemoteClusterMsg(InvitationTopic, confirmRaw)
frame := &model.RemoteClusterFrame{
RemoteId: rc.RemoteId,
Msg: msg,
}
return frame, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// pingLoop periodically sends a ping to all remote clusters.
func (rcs *Service) pingLoop(done <-chan struct{}) {
pingChan := make(chan *model.RemoteCluster, MaxConcurrentSends*2)
// create a thread pool to send pings concurrently to remotes.
for i := 0; i < MaxConcurrentSends; i++ {
go rcs.pingEmitter(pingChan, done)
}
go rcs.pingGenerator(pingChan, done)
}
func (rcs *Service) pingGenerator(pingChan chan *model.RemoteCluster, done <-chan struct{}) {
defer close(pingChan)
for {
start := time.Now()
// get all remotes, including any previously offline.
remotes, err := rcs.server.GetStore().RemoteCluster().GetAll(model.RemoteClusterQueryFilter{})
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Ping remote cluster failed (could not get list of remotes)", mlog.Err(err))
select {
case <-time.After(PingFreq):
continue
case <-done:
return
}
}
for _, rc := range remotes {
if rc.SiteURL != "" { // filter out unconfirmed invites
pingChan <- rc
}
}
// try to maintain frequency
elapsed := time.Since(start)
if elapsed < PingFreq {
sleep := time.Until(start.Add(PingFreq))
select {
case <-time.After(sleep):
case <-done:
return
}
}
}
}
// pingEmitter pulls Remotes from the ping queue (pingChan) and pings them.
// Pinging a remote cannot take longer than PingTimeoutMillis.
func (rcs *Service) pingEmitter(pingChan <-chan *model.RemoteCluster, done <-chan struct{}) {
for {
select {
case rc := <-pingChan:
if rc == nil {
return
}
online := rc.IsOnline()
if err := rcs.pingRemote(rc); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceWarn, "Remote cluster ping failed",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
}
if online != rc.IsOnline() {
if metrics := rcs.server.GetMetrics(); metrics != nil {
metrics.IncrementRemoteClusterConnStateChangeCounter(rc.RemoteId, rc.IsOnline())
}
rcs.fireConnectionStateChgEvent(rc)
}
case <-done:
return
}
}
}
// pingRemote make a synchronous ping to a remote cluster. Return is error if ping is
// unsuccessful and nil on success.
func (rcs *Service) pingRemote(rc *model.RemoteCluster) error {
frame, err := makePingFrame(rc)
if err != nil {
return err
}
url := fmt.Sprintf("%s/%s", rc.SiteURL, PingURL)
resp, err := rcs.sendFrameToRemote(PingTimeout, rc, frame, url)
if err != nil {
return err
}
ping := model.RemoteClusterPing{}
err = json.Unmarshal(resp, &ping)
if err != nil {
return err
}
if err := rcs.server.GetStore().RemoteCluster().SetLastPingAt(rc.RemoteId); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Failed to update LastPingAt for remote cluster",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
}
rc.LastPingAt = model.GetMillis()
if metrics := rcs.server.GetMetrics(); metrics != nil {
sentAt := time.Unix(0, ping.SentAt*int64(time.Millisecond))
elapsed := time.Since(sentAt).Seconds()
metrics.ObserveRemoteClusterPingDuration(rc.RemoteId, elapsed)
// we approximate clock skew between remotes.
skew := elapsed/2 - float64(ping.RecvAt-ping.SentAt)/1000
metrics.ObserveRemoteClusterClockSkew(rc.RemoteId, skew)
}
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote cluster ping",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Int64("SentAt", ping.SentAt),
mlog.Int64("RecvAt", ping.RecvAt),
mlog.Int64("Diff", ping.RecvAt-ping.SentAt),
)
return nil
}
func makePingFrame(rc *model.RemoteCluster) (*model.RemoteClusterFrame, error) {
ping := model.RemoteClusterPing{
SentAt: model.GetMillis(),
}
pingRaw, err := json.Marshal(ping)
if err != nil {
return nil, err
}
msg := model.NewRemoteClusterMsg(PingTopic, pingRaw)
frame := &model.RemoteClusterFrame{
RemoteId: rc.RemoteId,
Msg: msg,
}
return frame, nil
}
func (rcs *Service) fireConnectionStateChgEvent(rc *model.RemoteCluster) {
rcs.mux.RLock()
listeners := make([]ConnectionStateListener, 0, len(rcs.connectionStateListeners))
for _, l := range rcs.connectionStateListeners {
listeners = append(listeners, l)
}
rcs.mux.RUnlock()
for _, l := range listeners {
l(rc, rc.IsOnline())
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ReceiveIncomingMsg is called by the Rest API layer, or websocket layer (future), when a Remote Cluster
// message is received. Here we route the message to any topic listeners.
// `rc` and `msg` cannot be nil.
func (rcs *Service) ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response {
rcs.mux.RLock()
defer rcs.mux.RUnlock()
if metrics := rcs.server.GetMetrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
rcSanitized := *rc
rcSanitized.Token = ""
rcSanitized.RemoteToken = ""
var response Response
response.Status = ResponseStatusOK
listeners := rcs.getTopicListeners(msg.Topic)
for _, l := range listeners {
if err := callback(l, msg, &rcSanitized, &response); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Error from remote cluster message listener",
mlog.String("msgId", msg.Id), mlog.String("topic", msg.Topic), mlog.String("remote", rc.DisplayName), mlog.Err(err))
response.Status = ResponseStatusFail
response.Err = err.Error()
}
}
return response
}
func callback(listener TopicListener, msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
err = listener(msg, rc, resp)
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
)
// Response represents the bytes replied from a remote server when a message is sent.
type Response struct {
Status string `json:"status"`
Err string `json:"err"`
Payload json.RawMessage `json:"payload"`
}
// IsSuccess returns true if the response status indicates success.
func (r *Response) IsSuccess() bool {
return r.Status == ResponseStatusOK
}
// SetPayload serializes an arbitrary struct as a RawMessage.
func (r *Response) SetPayload(v any) error {
raw, err := json.Marshal(v)
if err != nil {
return err
}
r.Payload = raw
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"hash/fnv"
)
// enqueueTask adds a task to one of the send channels based on remoteId.
//
// There are a number of send channels (`MaxConcurrentSends`) to allow for sending to multiple
// remotes concurrently, while preserving message order for each remote.
func (rcs *Service) enqueueTask(ctx context.Context, remoteId string, task any) error {
if ctx == nil {
ctx = context.Background()
}
h := hash(remoteId)
idx := h % uint32(len(rcs.send))
select {
case rcs.send[idx] <- task:
return nil
case <-ctx.Done():
return NewBufferFullError(cap(rcs.send))
}
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
// sendLoop is called by each goroutine created for the send pool and waits for sendTask's until the
// done channel is signalled.
//
// Each goroutine in the pool is assigned a specific channel, and tasks are placed in the
// channel corresponding to the remoteId.
func (rcs *Service) sendLoop(idx int, done chan struct{}) {
for {
select {
case t := <-rcs.send[idx]:
switch task := t.(type) {
case sendMsgTask:
rcs.sendMsg(task)
case sendFileTask:
rcs.sendFile(task)
case sendProfileImageTask:
rcs.sendProfileImage(task)
}
case <-done:
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendFileResultFunc func(us *model.UploadSession, rc *model.RemoteCluster, resp *Response, err error)
type sendFileTask struct {
rc *model.RemoteCluster
us *model.UploadSession
fi *model.FileInfo
rp ReaderProvider
f SendFileResultFunc
}
type ReaderProvider interface {
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
}
// SendFile asynchronously sends a file to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the task cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of task enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding file delivery only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error {
task := sendFileTask{
rc: rc,
us: us,
fi: fi,
rp: rp,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendFile is called when a sendFileTask is popped from the send channel.
func (rcs *Service) sendFile(task sendFileTask) {
fi, err := rcs.sendFileToRemote(SendTimeout, task)
var response Response
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send file failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
mlog.Err(err),
)
response.Status = ResponseStatusFail
response.Err = err.Error()
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster file sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
)
response.Status = ResponseStatusOK
response.SetPayload(fi)
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.us, task.rc, &response, err)
}
}
func (rcs *Service) sendFileToRemote(timeout time.Duration, task sendFileTask) (*model.FileInfo, error) {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "sending file to remote...",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
mlog.String("file_path", task.us.Path),
)
r, appErr := task.rp.FileReader(task.fi.Path) // get Reader for the file
if appErr != nil {
return nil, fmt.Errorf("error opening file while sending file to remote %s: %w", task.rc.RemoteId, appErr)
}
defer r.Close()
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
return nil, fmt.Errorf("invalid siteURL while sending file to remote %s: %w", task.rc.RemoteId, err)
}
u.Path = path.Join(u.Path, model.APIURLSuffix, "remotecluster", "upload", task.us.Id)
req, err := http.NewRequest("POST", u.String(), r)
if err != nil {
return nil, err
}
req.Header.Set(model.HeaderRemoteclusterId, task.rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, task.rc.RemoteToken)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
// body should be a FileInfo
var fi model.FileInfo
if err := json.Unmarshal(body, &fi); err != nil {
return nil, fmt.Errorf("unexpected response body: %w", err)
}
return &fi, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendMsgResultFunc func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response, err error)
type sendMsgTask struct {
rc *model.RemoteCluster
msg model.RemoteClusterMsg
f SendMsgResultFunc
}
// BroadcastMsg asynchronously sends a message to all remote clusters interested in the message's topic.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the message cannot be enqueued before the timeout. A background context will block indefinitely.
//
// An optional callback can be provided that receives the success or fail result of sending to each remote cluster.
// Success or fail is regarding message delivery only. If a callback is provided it should return quickly.
func (rcs *Service) BroadcastMsg(ctx context.Context, msg model.RemoteClusterMsg, f SendMsgResultFunc) error {
// get list of interested remotes.
filter := model.RemoteClusterQueryFilter{
Topic: msg.Topic,
}
list, err := rcs.server.GetStore().RemoteCluster().GetAll(filter)
if err != nil {
return err
}
errs := merror.New()
for _, rc := range list {
if err := rcs.SendMsg(ctx, msg, rc, f); err != nil {
errs.Append(err)
}
}
return errs.ErrorOrNil()
}
// SendMsg asynchronously sends a message to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the message cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of message enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding response decoding only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error {
task := sendMsgTask{
rc: rc,
msg: msg,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendMsg is called when a sendMsgTask is popped from the send channel.
func (rcs *Service) sendMsg(task sendMsgTask) {
var errResp error
var response Response
// Ensure a panic from the callback does not exit the pool goroutine.
defer func() {
if errResp != nil {
response.Err = errResp.Error()
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.msg, task.rc, &response, errResp)
}
}()
frame := &model.RemoteClusterFrame{
RemoteId: task.rc.RemoteId,
Msg: task.msg,
}
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Invalid siteURL while sending message to remote",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
mlog.Err(err),
)
errResp = err
return
}
u.Path = path.Join(u.Path, SendMsgURL)
respJSON, err := rcs.sendFrameToRemote(SendTimeout, task.rc, frame, u.String())
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send message failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
mlog.Err(err),
)
errResp = err
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster message sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
)
if err = json.Unmarshal(respJSON, &response); err != nil {
rcs.server.Log().Error("Invalid response sending message to remote cluster",
mlog.String("remote", task.rc.DisplayName),
mlog.Err(err),
)
errResp = err
}
}
}
func (rcs *Service) sendFrameToRemote(timeout time.Duration, rc *model.RemoteCluster, frame *model.RemoteClusterFrame, url string) ([]byte, error) {
body, err := json.Marshal(frame)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set(model.HeaderRemoteclusterId, rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, rc.RemoteToken)
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if metrics := rcs.server.GetMetrics(); metrics != nil {
if err != nil || resp.StatusCode != http.StatusOK {
metrics.IncrementRemoteClusterMsgErrorsCounter(frame.RemoteId, os.IsTimeout(err))
} else {
metrics.IncrementRemoteClusterMsgSentCounter(frame.RemoteId)
}
}
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return body, fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
return body, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendProfileImageResultFunc func(userId string, rc *model.RemoteCluster, resp *Response, err error)
type sendProfileImageTask struct {
rc *model.RemoteCluster
userID string
provider ProfileImageProvider
f SendProfileImageResultFunc
}
type ProfileImageProvider interface {
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
}
// SendProfileImage asynchronously sends a user's profile image to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the task cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of task enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding image delivery only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error {
task := sendProfileImageTask{
rc: rc,
userID: userID,
provider: provider,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendProfileImage is called when a sendProfileImageTask is popped from the send channel.
func (rcs *Service) sendProfileImage(task sendProfileImageTask) {
err := rcs.sendProfileImageToRemote(SendTimeout, task)
var response Response
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send profile image failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
mlog.Err(err),
)
response.Status = ResponseStatusFail
response.Err = err.Error()
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster profile image sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
)
response.Status = ResponseStatusOK
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.userID, task.rc, &response, err)
}
}
func (rcs *Service) sendProfileImageToRemote(timeout time.Duration, task sendProfileImageTask) error {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "sending profile image to remote...",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
)
user, err := rcs.server.GetStore().User().Get(context.Background(), task.userID)
if err != nil {
return fmt.Errorf("error fetching user while sending profile image to remote %s: %w", task.rc.RemoteId, err)
}
img, _, appErr := task.provider.GetProfileImage(user) // get Reader for the file
if appErr != nil {
return fmt.Errorf("error fetching profile image for user (%s) while sending to remote %s: %w", task.userID, task.rc.RemoteId, appErr)
}
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
return fmt.Errorf("invalid siteURL while sending file to remote %s: %w", task.rc.RemoteId, err)
}
u.Path = path.Join(u.Path, model.APIURLSuffix, "remotecluster", task.userID, "image")
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "profile.png")
if err != nil {
return err
}
if _, err = io.Copy(part, bytes.NewBuffer(img)); err != nil {
return err
}
if err = writer.Close(); err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set(model.HeaderRemoteclusterId, task.rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, task.rc.RemoteToken)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"net"
"net/http"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
SendChanBuffer = 50
RecvChanBuffer = 50
ResultsChanBuffer = 50
ResultQueueDrainTimeoutMillis = 10000
MaxConcurrentSends = 10
SendMsgURL = "api/v4/remotecluster/msg"
SendTimeout = time.Minute
SendFileTimeout = time.Minute * 5
PingURL = "api/v4/remotecluster/ping"
PingFreq = time.Minute
PingTimeout = time.Second * 15
ConfirmInviteURL = "api/v4/remotecluster/confirm_invite"
InvitationTopic = "invitation"
PingTopic = "ping"
ResponseStatusOK = model.StatusOk
ResponseStatusFail = model.StatusFail
InviteExpiresAfter = time.Hour * 48
)
var (
disablePing bool // override for testing
)
type ServerIface interface {
Config() *model.Config
IsLeader() bool
AddClusterLeaderChangedListener(listener func()) string
RemoveClusterLeaderChangedListener(id string)
GetStore() store.Store
Log() *mlog.Logger
GetMetrics() einterfaces.MetricsInterface
}
// RemoteClusterServiceIFace is used to allow mocking where a remote cluster service is used (for testing).
// Unfortunately it lives here because the shared channel service, app layer, and server interface all need it.
// Putting it in app layer means shared channel service must import app package.
type RemoteClusterServiceIFace interface {
Shutdown() error
Start() error
Active() bool
AddTopicListener(topic string, listener TopicListener) string
RemoveTopicListener(listenerId string)
AddConnectionStateListener(listener ConnectionStateListener) string
RemoveConnectionStateListener(listenerId string)
SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error
SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error
SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error)
ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response
}
// TopicListener is a callback signature used to listen for incoming messages for
// a specific topic.
type TopicListener func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response) error
// ConnectionStateListener is used to listen to remote cluster connection state changes.
type ConnectionStateListener func(rc *model.RemoteCluster, online bool)
// Service provides inter-cluster communication via topic based messages. In product these are called "Secured Connections".
type Service struct {
server ServerIface
httpClient *http.Client
send []chan any
// everything below guarded by `mux`
mux sync.RWMutex
active bool
leaderListenerId string
topicListeners map[string]map[string]TopicListener // maps topic id to a map of listenerid->listener
connectionStateListeners map[string]ConnectionStateListener // maps listener id to listener
done chan struct{}
}
// NewRemoteClusterService creates a RemoteClusterService instance. In product this is called a "Secured Connection".
func NewRemoteClusterService(server ServerIface) (*Service, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: false,
}
client := &http.Client{
Transport: transport,
Timeout: SendTimeout,
}
service := &Service{
server: server,
httpClient: client,
topicListeners: make(map[string]map[string]TopicListener),
connectionStateListeners: make(map[string]ConnectionStateListener),
}
service.send = make([]chan any, MaxConcurrentSends)
for i := range service.send {
service.send[i] = make(chan any, SendChanBuffer)
}
return service, nil
}
// Start is called by the server on server start-up.
func (rcs *Service) Start() error {
rcs.mux.Lock()
rcs.leaderListenerId = rcs.server.AddClusterLeaderChangedListener(rcs.onClusterLeaderChange)
rcs.mux.Unlock()
rcs.onClusterLeaderChange()
return nil
}
// Shutdown is called by the server on server shutdown.
func (rcs *Service) Shutdown() error {
rcs.server.RemoveClusterLeaderChangedListener(rcs.leaderListenerId)
rcs.pause()
return nil
}
// Active returns true if this instance of the remote cluster service is active.
// The active instance is responsible for pinging and sending messages to remotes.
func (rcs *Service) Active() bool {
rcs.mux.Lock()
defer rcs.mux.Unlock()
return rcs.active
}
// AddTopicListener registers a callback
func (rcs *Service) AddTopicListener(topic string, listener TopicListener) string {
rcs.mux.Lock()
defer rcs.mux.Unlock()
id := model.NewId()
listeners, ok := rcs.topicListeners[topic]
if !ok || listeners == nil {
rcs.topicListeners[topic] = make(map[string]TopicListener)
}
rcs.topicListeners[topic][id] = listener
return id
}
func (rcs *Service) RemoveTopicListener(listenerId string) {
rcs.mux.Lock()
defer rcs.mux.Unlock()
for topic, listeners := range rcs.topicListeners {
if _, ok := listeners[listenerId]; ok {
delete(listeners, listenerId)
if len(listeners) == 0 {
delete(rcs.topicListeners, topic)
}
break
}
}
}
func (rcs *Service) getTopicListeners(topic string) []TopicListener {
rcs.mux.RLock()
defer rcs.mux.RUnlock()
listeners, ok := rcs.topicListeners[topic]
if !ok {
return nil
}
listenersCopy := make([]TopicListener, 0, len(listeners))
for _, l := range listeners {
listenersCopy = append(listenersCopy, l)
}
return listenersCopy
}
func (rcs *Service) AddConnectionStateListener(listener ConnectionStateListener) string {
id := model.NewId()
rcs.mux.Lock()
defer rcs.mux.Unlock()
rcs.connectionStateListeners[id] = listener
return id
}
func (rcs *Service) RemoveConnectionStateListener(listenerId string) {
rcs.mux.Lock()
defer rcs.mux.Unlock()
delete(rcs.connectionStateListeners, listenerId)
}
// onClusterLeaderChange is called whenever the cluster leader may have changed.
func (rcs *Service) onClusterLeaderChange() {
if rcs.server.IsLeader() {
rcs.resume()
} else {
rcs.pause()
}
}
func (rcs *Service) resume() {
rcs.mux.Lock()
defer rcs.mux.Unlock()
if rcs.active {
return // already active
}
rcs.active = true
rcs.done = make(chan struct{})
if !disablePing {
rcs.pingLoop(rcs.done)
}
// create thread pool for concurrent message sending.
for i := range rcs.send {
go rcs.sendLoop(i, rcs.done)
}
rcs.server.Log().Debug("Remote Cluster Service active")
}
func (rcs *Service) pause() {
rcs.mux.Lock()
defer rcs.mux.Unlock()
if !rcs.active {
return // already inactive
}
rcs.active = false
close(rcs.done)
rcs.done = nil
rcs.server.Log().Debug("Remote Cluster Service inactive")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"net/http"
"os"
"path/filepath"
"reflect"
"sync"
"sync/atomic"
"time"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EngineName = "bleve"
PostIndex = "posts"
FileIndex = "files"
UserIndex = "users"
ChannelIndex = "channels"
)
type BleveEngine struct {
PostIndex bleve.Index
FileIndex bleve.Index
UserIndex bleve.Index
ChannelIndex bleve.Index
Mutex sync.RWMutex
ready int32
cfg *model.Config
indexSync bool
}
var keywordMapping *mapping.FieldMapping
var standardMapping *mapping.FieldMapping
var dateMapping *mapping.FieldMapping
func init() {
keywordMapping = bleve.NewTextFieldMapping()
keywordMapping.Analyzer = keyword.Name
standardMapping = bleve.NewTextFieldMapping()
standardMapping.Analyzer = standard.Name
dateMapping = bleve.NewNumericFieldMapping()
}
func getChannelIndexMapping() *mapping.IndexMappingImpl {
channelMapping := bleve.NewDocumentMapping()
channelMapping.AddFieldMappingsAt("Id", keywordMapping)
channelMapping.AddFieldMappingsAt("Type", keywordMapping)
channelMapping.AddFieldMappingsAt("TeamId", keywordMapping)
channelMapping.AddFieldMappingsAt("NameSuggest", keywordMapping)
channelMapping.AddFieldMappingsAt("UserIDs", keywordMapping)
channelMapping.AddFieldMappingsAt("TeamMemberIDs", keywordMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", channelMapping)
return indexMapping
}
func getPostIndexMapping() *mapping.IndexMappingImpl {
postMapping := bleve.NewDocumentMapping()
postMapping.AddFieldMappingsAt("Id", keywordMapping)
postMapping.AddFieldMappingsAt("TeamId", keywordMapping)
postMapping.AddFieldMappingsAt("ChannelId", keywordMapping)
postMapping.AddFieldMappingsAt("UserId", keywordMapping)
postMapping.AddFieldMappingsAt("CreateAt", dateMapping)
postMapping.AddFieldMappingsAt("Message", standardMapping)
postMapping.AddFieldMappingsAt("Type", keywordMapping)
postMapping.AddFieldMappingsAt("Hashtags", standardMapping)
postMapping.AddFieldMappingsAt("Attachments", standardMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", postMapping)
return indexMapping
}
func getFileIndexMapping() *mapping.IndexMappingImpl {
fileMapping := bleve.NewDocumentMapping()
fileMapping.AddFieldMappingsAt("Id", keywordMapping)
fileMapping.AddFieldMappingsAt("CreatorId", keywordMapping)
fileMapping.AddFieldMappingsAt("ChannelId", keywordMapping)
fileMapping.AddFieldMappingsAt("CreateAt", dateMapping)
fileMapping.AddFieldMappingsAt("Name", standardMapping)
fileMapping.AddFieldMappingsAt("Content", standardMapping)
fileMapping.AddFieldMappingsAt("Extension", keywordMapping)
fileMapping.AddFieldMappingsAt("Content", standardMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", fileMapping)
return indexMapping
}
func getUserIndexMapping() *mapping.IndexMappingImpl {
userMapping := bleve.NewDocumentMapping()
userMapping.AddFieldMappingsAt("Id", keywordMapping)
userMapping.AddFieldMappingsAt("SuggestionsWithFullname", keywordMapping)
userMapping.AddFieldMappingsAt("SuggestionsWithoutFullname", keywordMapping)
userMapping.AddFieldMappingsAt("TeamsIds", keywordMapping)
userMapping.AddFieldMappingsAt("ChannelsIds", keywordMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", userMapping)
return indexMapping
}
func NewBleveEngine(cfg *model.Config) *BleveEngine {
return &BleveEngine{
cfg: cfg,
}
}
func (b *BleveEngine) getIndexDir(indexName string) string {
return filepath.Join(*b.cfg.BleveSettings.IndexDir, indexName+".bleve")
}
func (b *BleveEngine) createOrOpenIndex(indexName string, mapping *mapping.IndexMappingImpl) (bleve.Index, error) {
indexPath := b.getIndexDir(indexName)
if index, err := bleve.Open(indexPath); err == nil {
return index, nil
}
index, err := bleve.NewUsing(indexPath, mapping, "scorch", "scorch", map[string]any{
"forceSegmentType": "zap",
"forceSegmentVersion": 15,
})
if err != nil {
return nil, err
}
return index, nil
}
func (b *BleveEngine) openIndexes() *model.AppError {
if atomic.LoadInt32(&b.ready) != 0 {
return model.NewAppError("Bleveengine.Start", "bleveengine.already_started.error", nil, "", http.StatusInternalServerError)
}
var err error
b.PostIndex, err = b.createOrOpenIndex(PostIndex, getPostIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.FileIndex, err = b.createOrOpenIndex(FileIndex, getFileIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.UserIndex, err = b.createOrOpenIndex(UserIndex, getUserIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.ChannelIndex, err = b.createOrOpenIndex(ChannelIndex, getChannelIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
atomic.StoreInt32(&b.ready, 1)
return nil
}
func (b *BleveEngine) Start() *model.AppError {
if !*b.cfg.BleveSettings.EnableIndexing || *b.cfg.BleveSettings.IndexDir == "" {
return nil
}
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("EXPERIMENTAL: Starting Bleve")
return b.openIndexes()
}
func (b *BleveEngine) closeIndexes() *model.AppError {
if b.IsActive() {
if err := b.PostIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.FileIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.UserIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.ChannelIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
atomic.StoreInt32(&b.ready, 0)
return nil
}
func (b *BleveEngine) Stop() *model.AppError {
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("Stopping Bleve")
return b.closeIndexes()
}
func (b *BleveEngine) IsActive() bool {
return atomic.LoadInt32(&b.ready) == 1
}
func (b *BleveEngine) IsIndexingSync() bool {
return b.indexSync
}
func (b *BleveEngine) RefreshIndexes() *model.AppError {
return nil
}
func (b *BleveEngine) GetVersion() int {
return 0
}
func (b *BleveEngine) GetFullVersion() string {
return "0"
}
func (b *BleveEngine) GetPlugins() []string {
return []string{}
}
func (b *BleveEngine) GetName() string {
return EngineName
}
func (b *BleveEngine) TestConfig(cfg *model.Config) *model.AppError {
return nil
}
func (b *BleveEngine) deleteIndexes() *model.AppError {
if err := os.RemoveAll(b.getIndexDir(PostIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(UserIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(ChannelIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(FileIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) PurgeIndexes() *model.AppError {
if *b.cfg.BleveSettings.IndexDir == "" {
return nil
}
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("PurgeIndexes Bleve")
if err := b.closeIndexes(); err != nil {
return err
}
if err := b.deleteIndexes(); err != nil {
return err
}
return b.openIndexes()
}
func (b *BleveEngine) DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError {
return nil
}
func (b *BleveEngine) IsAutocompletionEnabled() bool {
return *b.cfg.BleveSettings.EnableAutocomplete
}
func (b *BleveEngine) IsIndexingEnabled() bool {
return *b.cfg.BleveSettings.EnableIndexing
}
func (b *BleveEngine) IsSearchEnabled() bool {
return *b.cfg.BleveSettings.EnableSearching
}
func (b *BleveEngine) UpdateConfig(cfg *model.Config) {
b.Mutex.Lock()
defer b.Mutex.Unlock()
if reflect.DeepEqual(cfg.BleveSettings, b.cfg.BleveSettings) {
return
}
mlog.Info("UpdateConf Bleve")
if *cfg.BleveSettings.EnableIndexing != *b.cfg.BleveSettings.EnableIndexing || *cfg.BleveSettings.IndexDir != *b.cfg.BleveSettings.IndexDir {
if err := b.closeIndexes(); err != nil {
mlog.Error("Error closing Bleve indexes to update the config", mlog.Err(err))
return
}
b.cfg = cfg
if err := b.openIndexes(); err != nil {
mlog.Error("Error opening Bleve indexes after updating the config", mlog.Err(err))
}
return
}
b.cfg = cfg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
type BLVChannel struct {
Id string
Type model.ChannelType
UserIDs []string
TeamId []string
TeamMemberIDs []string
NameSuggest []string
}
type BLVUser struct {
Id string
SuggestionsWithFullname []string
SuggestionsWithoutFullname []string
TeamsIds []string
ChannelsIds []string
}
type BLVPost struct {
Id string
TeamId string
ChannelId string
UserId string
CreateAt int64
Message string
Type string
Hashtags []string
Attachments string
}
type BLVFile struct {
Id string
CreatorId string
ChannelId string
CreateAt int64
Name string
Content string
Extension string
}
func BLVChannelFromChannel(channel *model.Channel, userIDs, teamMemberIDs []string) *BLVChannel {
displayNameInputs := searchengine.GetSuggestionInputsSplitBy(channel.DisplayName, " ")
nameInputs := searchengine.GetSuggestionInputsSplitByMultiple(channel.Name, []string{"-", "_"})
return &BLVChannel{
Id: channel.Id,
Type: channel.Type,
TeamId: []string{channel.TeamId},
NameSuggest: append(displayNameInputs, nameInputs...),
UserIDs: userIDs,
TeamMemberIDs: teamMemberIDs,
}
}
func BLVUserFromUserAndTeams(user *model.User, teamsIds, channelsIds []string) *BLVUser {
usernameSuggestions := searchengine.GetSuggestionInputsSplitByMultiple(user.Username, []string{".", "-", "_"})
fullnameStrings := []string{}
if user.FirstName != "" {
fullnameStrings = append(fullnameStrings, user.FirstName)
}
if user.LastName != "" {
fullnameStrings = append(fullnameStrings, user.LastName)
}
fullnameSuggestions := []string{}
if len(fullnameStrings) > 0 {
fullname := strings.Join(fullnameStrings, " ")
fullnameSuggestions = searchengine.GetSuggestionInputsSplitBy(fullname, " ")
}
nicknameSuggestions := []string{}
if user.Nickname != "" {
nicknameSuggestions = searchengine.GetSuggestionInputsSplitBy(user.Nickname, " ")
}
usernameAndNicknameSuggestions := append(usernameSuggestions, nicknameSuggestions...)
return &BLVUser{
Id: user.Id,
SuggestionsWithFullname: append(usernameAndNicknameSuggestions, fullnameSuggestions...),
SuggestionsWithoutFullname: usernameAndNicknameSuggestions,
TeamsIds: teamsIds,
ChannelsIds: channelsIds,
}
}
func BLVUserFromUserForIndexing(userForIndexing *model.UserForIndexing) *BLVUser {
user := &model.User{
Id: userForIndexing.Id,
Username: userForIndexing.Username,
Nickname: userForIndexing.Nickname,
FirstName: userForIndexing.FirstName,
LastName: userForIndexing.LastName,
CreateAt: userForIndexing.CreateAt,
DeleteAt: userForIndexing.DeleteAt,
}
return BLVUserFromUserAndTeams(user, userForIndexing.TeamsIds, userForIndexing.ChannelsIds)
}
func BLVPostFromPost(post *model.Post, teamId string) *BLVPost {
p := &model.PostForIndexing{
TeamId: teamId,
}
post.ShallowCopy(&p.Post)
return BLVPostFromPostForIndexing(p)
}
func BLVPostFromPostForIndexing(post *model.PostForIndexing) *BLVPost {
return &BLVPost{
Id: post.Id,
TeamId: post.TeamId,
ChannelId: post.ChannelId,
UserId: post.UserId,
CreateAt: post.CreateAt,
Message: post.Message,
Type: post.Type,
Hashtags: strings.Fields(post.Hashtags),
}
}
func splitFilenameWords(name string) string {
result := name
result = strings.ReplaceAll(result, "-", " ")
result = strings.ReplaceAll(result, ".", " ")
return result
}
func BLVFileFromFileInfo(fileInfo *model.FileInfo, channelId string) *BLVFile {
return &BLVFile{
Id: fileInfo.Id,
ChannelId: channelId,
CreatorId: fileInfo.CreatorId,
CreateAt: fileInfo.CreateAt,
Content: fileInfo.Content,
Extension: fileInfo.Extension,
Name: fileInfo.Name + " " + splitFilenameWords(fileInfo.Name),
}
}
func BLVFileFromFileForIndexing(file *model.FileForIndexing) *BLVFile {
return &BLVFile{
Id: file.Id,
ChannelId: file.ChannelId,
CreatorId: file.CreatorId,
CreateAt: file.CreateAt,
Content: file.Content,
Extension: file.Extension,
Name: file.Name + " " + splitFilenameWords(file.Name),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package indexer
import (
"context"
"net/http"
"strconv"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
timeBetweenBatches = 100 * time.Millisecond
estimatedPostCount = 10000000
estimatedFilesCount = 100000
estimatedChannelCount = 100000
estimatedUserCount = 10000
)
type BleveIndexerWorker struct {
name string
stop chan struct{}
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
engine *bleveengine.BleveEngine
closed int32
}
func MakeWorker(jobServer *jobs.JobServer, engine *bleveengine.BleveEngine) model.Worker {
if engine == nil {
return nil
}
return &BleveIndexerWorker{
name: "BleveIndexer",
stop: make(chan struct{}),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
engine: engine,
}
}
type IndexingProgress struct {
Now time.Time
StartAtTime int64
EndAtTime int64
LastEntityTime int64
TotalPostsCount int64
DonePostsCount int64
DonePosts bool
LastPostID string
TotalFilesCount int64
DoneFilesCount int64
DoneFiles bool
LastFileID string
TotalChannelsCount int64
DoneChannelsCount int64
DoneChannels bool
LastChannelID string
TotalUsersCount int64
DoneUsersCount int64
DoneUsers bool
LastUserID string
}
func (ip *IndexingProgress) CurrentProgress() int64 {
return (ip.DonePostsCount + ip.DoneChannelsCount + ip.DoneUsersCount + ip.DoneFilesCount) * 100 / (ip.TotalPostsCount + ip.TotalChannelsCount + ip.TotalUsersCount + ip.TotalFilesCount)
}
func (ip *IndexingProgress) IsDone() bool {
return ip.DonePosts && ip.DoneChannels && ip.DoneUsers && ip.DoneFiles
}
func (worker *BleveIndexerWorker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *BleveIndexerWorker) IsEnabled(cfg *model.Config) bool {
return true
}
func (worker *BleveIndexerWorker) Run() {
// Set to open if closed before. We are not bothered about multiple opens.
if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) {
worker.stop = make(chan struct{})
}
mlog.Debug("Worker Started", mlog.String("workername", worker.name))
defer func() {
mlog.Debug("Worker: Finished", mlog.String("workername", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker: Received stop signal", mlog.String("workername", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker: Received a new candidate job.", mlog.String("workername", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *BleveIndexerWorker) Stop() {
// Set to close, and if already closed before, then return.
if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) {
return
}
mlog.Debug("Worker Stopping", mlog.String("workername", worker.name))
close(worker.stop)
<-worker.stopped
}
func (worker *BleveIndexerWorker) DoJob(job *model.Job) {
claimed, err := worker.jobServer.ClaimJob(job)
if err != nil {
mlog.Warn("Worker: Error occurred while trying to claim job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
return
}
if !claimed {
return
}
mlog.Info("Worker: Indexing job claimed by worker", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if !worker.engine.IsActive() {
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.engine_inactive", nil, "", http.StatusInternalServerError)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to run job as ")
}
return
}
progress := IndexingProgress{
Now: time.Now(),
DonePosts: false,
DoneChannels: false,
DoneUsers: false,
DoneFiles: false,
StartAtTime: 0,
EndAtTime: model.GetMillis(),
}
// Extract the start and end times, if they are set.
if startString, ok := job.Data["start_time"]; ok {
startInt, err := strconv.ParseInt(startString, 10, 64)
if err != nil {
mlog.Error("Worker: Failed to parse start_time for job", mlog.String("workername", worker.name), mlog.String("start_time", startString), mlog.String("job_id", job.Id), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_start_time.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.StartAtTime = startInt
} else {
// Set start time to oldest entity in the database.
// A user or a channel may be created before any post.
oldestEntityCreationTime, err := worker.jobServer.Store.Post().GetOldestEntityCreationTime()
if err != nil {
mlog.Error("Worker: Failed to fetch oldest entity for job.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("start_time", startString), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.get_oldest_entity.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.StartAtTime = oldestEntityCreationTime
}
progress.LastEntityTime = progress.StartAtTime
if endString, ok := job.Data["end_time"]; ok {
endInt, err := strconv.ParseInt(endString, 10, 64)
if err != nil {
mlog.Error("Worker: Failed to parse end_time for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("end_time", endString), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_end_time.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job errorv", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.EndAtTime = endInt
}
if id, ok := job.Data["start_post_id"]; ok {
progress.LastPostID = id
}
if id, ok := job.Data["start_channel_id"]; ok {
progress.LastChannelID = id
}
if id, ok := job.Data["start_user_id"]; ok {
progress.LastUserID = id
}
if id, ok := job.Data["start_file_id"]; ok {
progress.LastFileID = id
}
// Counting all posts may fail or timeout when the posts table is large. If this happens, log a warning, but carry
// on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate.
if count, err := worker.jobServer.Store.Post().AnalyticsPostCount(&model.PostCountOptions{}); err != nil {
mlog.Warn("Worker: Failed to fetch total post count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalPostsCount = estimatedPostCount
} else {
progress.TotalPostsCount = count
}
// Same possible fail as above can happen when counting channels
if count, err := worker.jobServer.Store.Channel().AnalyticsTypeCount("", ""); err != nil {
mlog.Warn("Worker: Failed to fetch total channel count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalChannelsCount = estimatedChannelCount
} else {
progress.TotalChannelsCount = count
}
// Same possible fail as above can happen when counting users
if count, err := worker.jobServer.Store.User().Count(model.UserCountOptions{
IncludeBotAccounts: true, // This actually doesn't join with the bots table
// since ExcludeRegularUsers is set to false
}); err != nil {
mlog.Warn("Worker: Failed to fetch total user count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalUsersCount = estimatedUserCount
} else {
progress.TotalUsersCount = count
}
// Counting all files may fail or timeout when the file_info table is large. If this happens, log a warning, but carry
// on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate.
if count, err := worker.jobServer.Store.FileInfo().CountAll(); err != nil {
mlog.Warn("Worker: Failed to fetch total file info count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalFilesCount = estimatedFilesCount
} else {
progress.TotalFilesCount = count
}
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
cancelWatcherChan := make(chan struct{}, 1)
go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
defer cancelCancelWatcher()
for {
select {
case <-cancelWatcherChan:
mlog.Info("Worker: Indexing job has been canceled via CancellationWatcher", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as cancelled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
return
case <-worker.stop:
mlog.Info("Worker: Indexing has been canceled via Worker Stop", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as canceled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
return
case <-time.After(timeBetweenBatches):
var err *model.AppError
if progress, err = worker.IndexBatch(progress); err != nil {
mlog.Error("Worker: Failed to index batch for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
return
}
// Storing the batch progress in metadata.
if job.Data == nil {
job.Data = make(model.StringMap)
}
job.Data["start_time"] = strconv.FormatInt(progress.LastEntityTime, 10)
job.Data["start_post_id"] = progress.LastPostID
job.Data["start_channel_id"] = progress.LastChannelID
job.Data["start_user_id"] = progress.LastUserID
job.Data["start_file_id"] = progress.LastFileID
job.Data["original_start_time"] = strconv.FormatInt(progress.StartAtTime, 10)
job.Data["end_time"] = strconv.FormatInt(progress.EndAtTime, 10)
if err := worker.jobServer.SetJobProgress(job, progress.CurrentProgress()); err != nil {
mlog.Error("Worker: Failed to set progress for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
return
}
if progress.IsDone() {
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
}
mlog.Info("Worker: Indexing job finished successfully", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
return
}
}
}
}
func (worker *BleveIndexerWorker) IndexBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
if !progress.DonePosts {
return worker.IndexPostsBatch(progress)
}
if !progress.DoneChannels {
return worker.IndexChannelsBatch(progress)
}
if !progress.DoneUsers {
return worker.IndexUsersBatch(progress)
}
if !progress.DoneFiles {
return worker.IndexFilesBatch(progress)
}
return progress, model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.index_batch.nothing_left_to_index.error", nil, "", http.StatusInternalServerError)
}
func (worker *BleveIndexerWorker) IndexPostsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var posts []*model.PostForIndexing
tries := 0
for posts == nil {
var err error
posts, err = worker.jobServer.Store.Post().GetPostsBatchForIndexing(progress.LastEntityTime, progress.LastPostID, *worker.jobServer.Config().BleveSettings.BatchSize)
if err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexPostsBatch", "app.post.get_posts_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get posts batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
// Handle zero messages.
if len(posts) == 0 {
progress.DonePosts = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastPost, err := worker.BulkIndexPosts(posts, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last post's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastPost.CreateAt {
progress.DonePosts = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastPost.CreateAt
}
progress.LastPostID = lastPost.Id
progress.DonePostsCount += int64(len(posts))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexPosts(posts []*model.PostForIndexing, progress IndexingProgress) (*model.Post, *model.AppError) {
batch := worker.engine.PostIndex.NewBatch()
for _, post := range posts {
if post.DeleteAt == 0 {
searchPost := bleveengine.BLVPostFromPostForIndexing(post)
batch.Index(searchPost.Id, searchPost)
} else {
batch.Delete(post.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.PostIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexPosts", "bleveengine.indexer.do_job.bulk_index_posts.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &posts[len(posts)-1].Post, nil
}
func (worker *BleveIndexerWorker) IndexFilesBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var files []*model.FileForIndexing
tries := 0
for files == nil {
var err error
files, err = worker.jobServer.Store.FileInfo().GetFilesBatchForIndexing(progress.LastEntityTime, progress.LastFileID, *worker.jobServer.Config().BleveSettings.BatchSize)
if err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexFilesBatch", "app.post.get_files_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get files batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
if len(files) == 0 {
progress.DoneFiles = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastFile, err := worker.BulkIndexFiles(files, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last file's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastFile.CreateAt {
progress.DoneFiles = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastFile.CreateAt
}
progress.LastFileID = lastFile.Id
progress.DoneFilesCount += int64(len(files))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexFiles(files []*model.FileForIndexing, progress IndexingProgress) (*model.FileInfo, *model.AppError) {
batch := worker.engine.FileIndex.NewBatch()
for _, file := range files {
if file.DeleteAt == 0 {
searchFile := bleveengine.BLVFileFromFileForIndexing(file)
batch.Index(searchFile.Id, searchFile)
} else {
batch.Delete(file.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.FileIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexPosts", "bleveengine.indexer.do_job.bulk_index_files.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &files[len(files)-1].FileInfo, nil
}
func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var channels []*model.Channel
tries := 0
for channels == nil {
var nErr error
channels, nErr = worker.jobServer.Store.Channel().GetChannelsBatchForIndexing(progress.LastEntityTime, progress.LastChannelID, *worker.jobServer.Config().BleveSettings.BatchSize)
if nErr != nil {
if tries >= 10 {
return progress, model.NewAppError("BleveIndexerWorker.IndexChannelsBatch", "app.channel.get_channels_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
mlog.Warn("Failed to get channels batch for indexing. Retrying.", mlog.Err(nErr))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
if len(channels) == 0 {
progress.DoneChannels = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastChannel, err := worker.BulkIndexChannels(channels, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last channel's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastChannel.CreateAt {
progress.DoneChannels = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastChannel.CreateAt
}
progress.LastChannelID = lastChannel.Id
progress.DoneChannelsCount += int64(len(channels))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexChannels(channels []*model.Channel, progress IndexingProgress) (*model.Channel, *model.AppError) {
batch := worker.engine.ChannelIndex.NewBatch()
for _, channel := range channels {
if channel.DeleteAt == 0 {
var userIDs []string
var err error
if channel.Type == model.ChannelTypePrivate {
userIDs, err = worker.jobServer.Store.Channel().GetAllChannelMembersById(channel.Id)
if err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Get teamMember ids from channelid
teamMemberIDs, err := worker.jobServer.Store.Channel().GetTeamMembersForChannel(channel.Id)
if err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
searchChannel := bleveengine.BLVChannelFromChannel(channel, userIDs, teamMemberIDs)
batch.Index(searchChannel.Id, searchChannel)
} else {
batch.Delete(channel.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.ChannelIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels[len(channels)-1], nil
}
func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var users []*model.UserForIndexing
tries := 0
for users == nil {
if usersBatch, err := worker.jobServer.Store.User().GetUsersBatchForIndexing(progress.LastEntityTime, progress.LastUserID, *worker.jobServer.Config().BleveSettings.BatchSize); err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexUsersBatch", "app.user.get_users_batch_for_indexing.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get users batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
} else {
users = usersBatch
}
tries++
}
if len(users) == 0 {
progress.DoneUsers = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastUser, err := worker.BulkIndexUsers(users, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last user's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastUser.CreateAt {
progress.DoneUsers = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastUser.CreateAt
}
progress.LastUserID = lastUser.Id
progress.DoneUsersCount += int64(len(users))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexUsers(users []*model.UserForIndexing, progress IndexingProgress) (*model.UserForIndexing, *model.AppError) {
batch := worker.engine.UserIndex.NewBatch()
for _, user := range users {
if user.DeleteAt == 0 {
searchUser := bleveengine.BLVUserFromUserForIndexing(user)
batch.Index(searchUser.Id, searchUser)
} else {
batch.Delete(user.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.UserIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexUsers", "bleveengine.indexer.do_job.bulk_index_users.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users[len(users)-1], nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"net/http"
"strings"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/search/query"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const DeletePostsBatchSize = 500
const DeleteFilesBatchSize = 500
func (b *BleveEngine) IndexPost(post *model.Post, teamId string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvPost := BLVPostFromPost(post, teamId)
if err := b.PostIndex.Index(blvPost.Id, blvPost); err != nil {
return model.NewAppError("Bleveengine.IndexPost", "bleveengine.index_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchPosts(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, model.PostSearchMatches, *model.AppError) {
channelQueries := []query.Query{}
for _, channel := range channels {
channelIdQ := bleve.NewTermQuery(channel.Id)
channelIdQ.SetField("ChannelId")
channelQueries = append(channelQueries, channelIdQ)
}
channelDisjunctionQ := bleve.NewDisjunctionQuery(channelQueries...)
var termQueries []query.Query
var notTermQueries []query.Query
var filters []query.Query
var notFilters []query.Query
typeQ := bleve.NewTermQuery("")
typeQ.SetField("Type")
filters = append(filters, typeQ)
for i, params := range searchParams {
var termOperator query.MatchQueryOperator = query.MatchQueryOperatorAnd
if searchParams[0].OrTerms {
termOperator = query.MatchQueryOperatorOr
}
// Date, channels and FromUsers filters come in all
// searchParams iteration, and as they are global to the
// query, we only need to process them once
if i == 0 {
if len(params.InChannels) > 0 {
inChannels := []query.Query{}
for _, channelId := range params.InChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
inChannels = append(inChannels, channelQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(inChannels...))
}
if len(params.ExcludedChannels) > 0 {
excludedChannels := []query.Query{}
for _, channelId := range params.ExcludedChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
excludedChannels = append(excludedChannels, channelQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedChannels...))
}
if len(params.FromUsers) > 0 {
fromUsers := []query.Query{}
for _, userId := range params.FromUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("UserId")
fromUsers = append(fromUsers, userQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(fromUsers...))
}
if len(params.ExcludedUsers) > 0 {
excludedUsers := []query.Query{}
for _, userId := range params.ExcludedUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("UserId")
excludedUsers = append(excludedUsers, userQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedUsers...))
}
if params.OnDate != "" {
before, after := params.GetOnDateMillis()
beforeFloat64 := float64(before)
afterFloat64 := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforeFloat64, &afterFloat64)
onDateQ.SetField("CreateAt")
filters = append(filters, onDateQ)
} else {
if params.AfterDate != "" || params.BeforeDate != "" {
var min, max *float64
if params.AfterDate != "" {
minf := float64(params.GetAfterDateMillis())
min = &minf
}
if params.BeforeDate != "" {
maxf := float64(params.GetBeforeDateMillis())
max = &maxf
}
dateQ := bleve.NewNumericRangeQuery(min, max)
dateQ.SetField("CreateAt")
filters = append(filters, dateQ)
}
if params.ExcludedAfterDate != "" {
minf := float64(params.GetExcludedAfterDateMillis())
dateQ := bleve.NewNumericRangeQuery(&minf, nil)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedBeforeDate != "" {
maxf := float64(params.GetExcludedBeforeDateMillis())
dateQ := bleve.NewNumericRangeQuery(nil, &maxf)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedDate != "" {
before, after := params.GetExcludedDateMillis()
beforef := float64(before)
afterf := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforef, &afterf)
onDateQ.SetField("CreateAt")
notFilters = append(notFilters, onDateQ)
}
}
}
if params.IsHashtag {
if params.Terms != "" {
hashtagQ := bleve.NewMatchQuery(params.Terms)
hashtagQ.SetField("Hashtags")
hashtagQ.SetOperator(termOperator)
termQueries = append(termQueries, hashtagQ)
} else if params.ExcludedTerms != "" {
hashtagQ := bleve.NewMatchQuery(params.ExcludedTerms)
hashtagQ.SetField("Hashtags")
hashtagQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, hashtagQ)
}
} else {
if params.Terms != "" {
terms := []string{}
for _, term := range strings.Split(params.Terms, " ") {
if strings.HasSuffix(term, "*") {
messageQ := bleve.NewWildcardQuery(term)
messageQ.SetField("Message")
termQueries = append(termQueries, messageQ)
} else {
terms = append(terms, term)
}
}
if len(terms) > 0 {
messageQ := bleve.NewMatchQuery(strings.Join(terms, " "))
messageQ.SetField("Message")
messageQ.SetOperator(termOperator)
termQueries = append(termQueries, messageQ)
}
}
if params.ExcludedTerms != "" {
messageQ := bleve.NewMatchQuery(params.ExcludedTerms)
messageQ.SetField("Message")
messageQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, messageQ)
}
}
}
allTermsQ := bleve.NewBooleanQuery()
allTermsQ.AddMustNot(notTermQueries...)
if searchParams[0].OrTerms {
allTermsQ.AddShould(termQueries...)
} else {
allTermsQ.AddMust(termQueries...)
}
query := bleve.NewBooleanQuery()
query.AddMust(channelDisjunctionQ)
if len(termQueries) > 0 || len(notTermQueries) > 0 {
query.AddMust(allTermsQ)
}
if len(filters) > 0 {
query.AddMust(bleve.NewConjunctionQuery(filters...))
}
if len(notFilters) > 0 {
query.AddMustNot(notFilters...)
}
search := bleve.NewSearchRequestOptions(query, perPage, page*perPage, false)
search.SortBy([]string{"-CreateAt"})
results, err := b.PostIndex.Search(search)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchPosts", "bleveengine.search_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
postIds := []string{}
matches := model.PostSearchMatches{}
for _, r := range results.Hits {
postIds = append(postIds, r.ID)
}
return postIds, matches, nil
}
func (b *BleveEngine) deletePosts(searchRequest *bleve.SearchRequest, batchSize int) (int64, error) {
resultsCount := int64(0)
for {
// As we are deleting the posts after fetching them, we need to keep
// From fixed always to 0
searchRequest.From = 0
searchRequest.Size = batchSize
results, err := b.PostIndex.Search(searchRequest)
if err != nil {
return -1, err
}
batch := b.PostIndex.NewBatch()
for _, post := range results.Hits {
batch.Delete(post.ID)
}
if err := b.PostIndex.Batch(batch); err != nil {
return -1, err
}
resultsCount += int64(results.Hits.Len())
if results.Hits.Len() < batchSize {
break
}
}
return resultsCount, nil
}
func (b *BleveEngine) DeleteChannelPosts(channelID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(channelID)
query.SetField("ChannelId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deletePosts(search, DeletePostsBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteChannelPosts",
"bleveengine.delete_channel_posts.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Posts for channel deleted", mlog.String("channel_id", channelID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeleteUserPosts(userID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(userID)
query.SetField("UserId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deletePosts(search, DeletePostsBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteUserPosts",
"bleveengine.delete_user_posts.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Posts for user deleted", mlog.String("user_id", userID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeletePost(post *model.Post) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.PostIndex.Delete(post.Id); err != nil {
return model.NewAppError("Bleveengine.DeletePost", "bleveengine.delete_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexChannel(channel *model.Channel, userIDs, teamMemberIDs []string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvChannel := BLVChannelFromChannel(channel, userIDs, teamMemberIDs)
if err := b.ChannelIndex.Index(blvChannel.Id, blvChannel); err != nil {
return model.NewAppError("Bleveengine.IndexChannel", "bleveengine.index_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchChannels(teamId, userID, term string, isGuest bool) ([]string, *model.AppError) {
// This query essentially boils down to (if teamID is passed):
// match teamID == <>
// AND
// match term == <>
// AND
// match (channelType != 'P' || (<> in userIDs && channelType == 'P'))
// (or if teamID is not passed)
// <> in teamMemberIds
// AND
// match term == <>
// AND
// match (channelType != 'P' || (<> in userIDs && channelType == 'P'))
// (or if isGuest is true)
// <> in teamMemberIds
// AND
// match term == <>
// AND
// match (<> in userIDs)
queries := []query.Query{}
if teamId != "" {
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamId")
queries = append(queries, teamIdQ)
} else {
teamMemberQ := bleve.NewTermQuery(userID)
teamMemberQ.SetField("TeamMemberIDs")
queries = append(queries, teamMemberQ)
}
if isGuest {
userQ := bleve.NewBooleanQuery()
userIDQ := bleve.NewTermQuery(userID)
userIDQ.SetField("UserIDs")
userQ.AddMust(userIDQ)
queries = append(queries, userIDQ)
} else {
boolNotPrivate := bleve.NewBooleanQuery()
privateQ := bleve.NewTermQuery(string(model.ChannelTypePrivate))
privateQ.SetField("Type")
boolNotPrivate.AddMustNot(privateQ)
userQ := bleve.NewBooleanQuery()
userIDQ := bleve.NewTermQuery(userID)
userIDQ.SetField("UserIDs")
userQ.AddMust(userIDQ)
userQ.AddMust(privateQ)
channelTypeQ := bleve.NewDisjunctionQuery()
channelTypeQ.AddQuery(boolNotPrivate)
channelTypeQ.AddQuery(userQ) // userID && 'p'
queries = append(queries, channelTypeQ)
}
if term != "" {
nameSuggestQ := bleve.NewPrefixQuery(strings.ToLower(term))
nameSuggestQ.SetField("NameSuggest")
queries = append(queries, nameSuggestQ)
}
query := bleve.NewSearchRequest(bleve.NewConjunctionQuery(queries...))
query.Size = model.ChannelSearchDefaultLimit
results, err := b.ChannelIndex.Search(query)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchChannels", "bleveengine.search_channels.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channelIds := []string{}
for _, result := range results.Hits {
channelIds = append(channelIds, result.ID)
}
return channelIds, nil
}
func (b *BleveEngine) DeleteChannel(channel *model.Channel) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.ChannelIndex.Delete(channel.Id); err != nil {
return model.NewAppError("Bleveengine.DeleteChannel", "bleveengine.delete_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexUser(user *model.User, teamsIds, channelsIds []string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvUser := BLVUserFromUserAndTeams(user, teamsIds, channelsIds)
if err := b.UserIndex.Index(blvUser.Id, blvUser); err != nil {
return model.NewAppError("Bleveengine.IndexUser", "bleveengine.index_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchUsersInChannel(teamId, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) {
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
return []string{}, []string{}, nil
}
// users in channel
var queries []query.Query
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
queries = append(queries, termQ)
}
channelIdQ := bleve.NewTermQuery(channelId)
channelIdQ.SetField("ChannelsIds")
queries = append(queries, channelIdQ)
query := bleve.NewConjunctionQuery(queries...)
uchanSearch := bleve.NewSearchRequest(query)
uchanSearch.Size = options.Limit
uchan, err := b.UserIndex.Search(uchanSearch)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchUsersInChannel", "bleveengine.search_users_in_channel.uchan.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// users not in channel
boolQ := bleve.NewBooleanQuery()
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
boolQ.AddMust(termQ)
}
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamsIds")
boolQ.AddMust(teamIdQ)
outsideChannelIdQ := bleve.NewTermQuery(channelId)
outsideChannelIdQ.SetField("ChannelsIds")
boolQ.AddMustNot(outsideChannelIdQ)
if len(restrictedToChannels) > 0 {
restrictedChannelsQ := bleve.NewDisjunctionQuery()
for _, channelId := range restrictedToChannels {
restrictedChannelQ := bleve.NewTermQuery(channelId)
restrictedChannelsQ.AddQuery(restrictedChannelQ)
}
boolQ.AddMust(restrictedChannelsQ)
}
nuchanSearch := bleve.NewSearchRequest(boolQ)
nuchanSearch.Size = options.Limit
nuchan, err := b.UserIndex.Search(nuchanSearch)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchUsersInChannel", "bleveengine.search_users_in_channel.nuchan.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
uchanIds := []string{}
for _, result := range uchan.Hits {
uchanIds = append(uchanIds, result.ID)
}
nuchanIds := []string{}
for _, result := range nuchan.Hits {
nuchanIds = append(nuchanIds, result.ID)
}
return uchanIds, nuchanIds, nil
}
func (b *BleveEngine) SearchUsersInTeam(teamId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, *model.AppError) {
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
return []string{}, nil
}
var rootQ query.Query
if term == "" && teamId == "" && restrictedToChannels == nil {
rootQ = bleve.NewMatchAllQuery()
} else {
boolQ := bleve.NewBooleanQuery()
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
boolQ.AddMust(termQ)
}
if len(restrictedToChannels) > 0 {
// restricted channels are already filtered by team, so we
// can search only those matches
restrictedChannelsQ := []query.Query{}
for _, channelId := range restrictedToChannels {
channelIdQ := bleve.NewTermQuery(channelId)
channelIdQ.SetField("ChannelsIds")
restrictedChannelsQ = append(restrictedChannelsQ, channelIdQ)
}
boolQ.AddMust(bleve.NewDisjunctionQuery(restrictedChannelsQ...))
} else {
// this means that we only need to restrict by team
if teamId != "" {
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamsIds")
boolQ.AddMust(teamIdQ)
}
}
rootQ = boolQ
}
search := bleve.NewSearchRequest(rootQ)
search.Size = options.Limit
results, err := b.UserIndex.Search(search)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchUsersInTeam", "bleveengine.search_users_in_team.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
usersIds := []string{}
for _, r := range results.Hits {
usersIds = append(usersIds, r.ID)
}
return usersIds, nil
}
func (b *BleveEngine) DeleteUser(user *model.User) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.UserIndex.Delete(user.Id); err != nil {
return model.NewAppError("Bleveengine.DeleteUser", "bleveengine.delete_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexFile(file *model.FileInfo, channelId string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvFile := BLVFileFromFileInfo(file, channelId)
if err := b.FileIndex.Index(blvFile.Id, blvFile); err != nil {
return model.NewAppError("Bleveengine.IndexFile", "bleveengine.index_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchFiles(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, *model.AppError) {
channelQueries := []query.Query{}
for _, channel := range channels {
channelIdQ := bleve.NewTermQuery(channel.Id)
channelIdQ.SetField("ChannelId")
channelQueries = append(channelQueries, channelIdQ)
}
channelDisjunctionQ := bleve.NewDisjunctionQuery(channelQueries...)
var termQueries []query.Query
var notTermQueries []query.Query
var filters []query.Query
var notFilters []query.Query
for i, params := range searchParams {
var termOperator query.MatchQueryOperator = query.MatchQueryOperatorAnd
if searchParams[0].OrTerms {
termOperator = query.MatchQueryOperatorOr
}
// Date, channels and FromUsers filters come in all
// searchParams iteration, and as they are global to the
// query, we only need to process them once
if i == 0 {
if len(params.InChannels) > 0 {
inChannels := []query.Query{}
for _, channelId := range params.InChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
inChannels = append(inChannels, channelQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(inChannels...))
}
if len(params.ExcludedChannels) > 0 {
excludedChannels := []query.Query{}
for _, channelId := range params.ExcludedChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
excludedChannels = append(excludedChannels, channelQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedChannels...))
}
if len(params.FromUsers) > 0 {
fromUsers := []query.Query{}
for _, userId := range params.FromUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("CreatorId")
fromUsers = append(fromUsers, userQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(fromUsers...))
}
if len(params.ExcludedUsers) > 0 {
excludedUsers := []query.Query{}
for _, userId := range params.ExcludedUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("CreatorId")
excludedUsers = append(excludedUsers, userQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedUsers...))
}
if len(params.Extensions) > 0 {
extensions := []query.Query{}
for _, extension := range params.Extensions {
extensionQ := bleve.NewTermQuery(extension)
extensionQ.SetField("Extension")
extensions = append(extensions, extensionQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(extensions...))
}
if len(params.ExcludedExtensions) > 0 {
excludedExtensions := []query.Query{}
for _, extension := range params.ExcludedExtensions {
extensionQ := bleve.NewTermQuery(extension)
extensionQ.SetField("Extension")
excludedExtensions = append(excludedExtensions, extensionQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedExtensions...))
}
if params.OnDate != "" {
before, after := params.GetOnDateMillis()
beforeFloat64 := float64(before)
afterFloat64 := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforeFloat64, &afterFloat64)
onDateQ.SetField("CreateAt")
filters = append(filters, onDateQ)
} else {
if params.AfterDate != "" || params.BeforeDate != "" {
var min, max *float64
if params.AfterDate != "" {
minf := float64(params.GetAfterDateMillis())
min = &minf
}
if params.BeforeDate != "" {
maxf := float64(params.GetBeforeDateMillis())
max = &maxf
}
dateQ := bleve.NewNumericRangeQuery(min, max)
dateQ.SetField("CreateAt")
filters = append(filters, dateQ)
}
if params.ExcludedAfterDate != "" {
minf := float64(params.GetExcludedAfterDateMillis())
dateQ := bleve.NewNumericRangeQuery(&minf, nil)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedBeforeDate != "" {
maxf := float64(params.GetExcludedBeforeDateMillis())
dateQ := bleve.NewNumericRangeQuery(nil, &maxf)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedDate != "" {
before, after := params.GetExcludedDateMillis()
beforef := float64(before)
afterf := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforef, &afterf)
onDateQ.SetField("CreateAt")
notFilters = append(notFilters, onDateQ)
}
}
}
if params.Terms != "" {
terms := []string{}
for _, term := range strings.Split(params.Terms, " ") {
if strings.HasSuffix(term, "*") {
nameQ := bleve.NewWildcardQuery(term)
nameQ.SetField("Name")
contentQ := bleve.NewWildcardQuery(term)
contentQ.SetField("Content")
termQueries = append(termQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
} else {
terms = append(terms, term)
}
}
if len(terms) > 0 {
nameQ := bleve.NewMatchQuery(strings.Join(terms, " "))
nameQ.SetField("Name")
nameQ.SetOperator(termOperator)
contentQ := bleve.NewMatchQuery(strings.Join(terms, " "))
contentQ.SetField("Content")
contentQ.SetOperator(termOperator)
termQueries = append(termQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
}
}
if params.ExcludedTerms != "" {
nameQ := bleve.NewMatchQuery(params.ExcludedTerms)
nameQ.SetField("Name")
nameQ.SetOperator(termOperator)
contentQ := bleve.NewMatchQuery(params.ExcludedTerms)
contentQ.SetField("Content")
contentQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
}
}
allTermsQ := bleve.NewBooleanQuery()
allTermsQ.AddMustNot(notTermQueries...)
if searchParams[0].OrTerms {
allTermsQ.AddShould(termQueries...)
} else {
allTermsQ.AddMust(termQueries...)
}
query := bleve.NewBooleanQuery()
query.AddMust(channelDisjunctionQ)
if len(termQueries) > 0 || len(notTermQueries) > 0 {
query.AddMust(allTermsQ)
}
if len(filters) > 0 {
query.AddMust(bleve.NewConjunctionQuery(filters...))
}
if len(notFilters) > 0 {
query.AddMustNot(notFilters...)
}
search := bleve.NewSearchRequestOptions(query, perPage, page*perPage, false)
search.SortBy([]string{"-CreateAt"})
results, err := b.FileIndex.Search(search)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchFiles", "bleveengine.search_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
fileIds := []string{}
for _, r := range results.Hits {
fileIds = append(fileIds, r.ID)
}
return fileIds, nil
}
func (b *BleveEngine) DeleteFile(fileID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.FileIndex.Delete(fileID); err != nil {
return model.NewAppError("Bleveengine.DeleteFile", "bleveengine.delete_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) deleteFiles(searchRequest *bleve.SearchRequest, batchSize int) (int64, error) {
resultsCount := int64(0)
for {
// As we are deleting the files after fetching them, we need to keep
// From fixed always to 0
searchRequest.From = 0
searchRequest.Size = batchSize
results, err := b.FileIndex.Search(searchRequest)
if err != nil {
return -1, err
}
batch := b.FileIndex.NewBatch()
for _, file := range results.Hits {
batch.Delete(file.ID)
}
if err := b.FileIndex.Batch(batch); err != nil {
return -1, err
}
resultsCount += int64(results.Hits.Len())
if results.Hits.Len() < batchSize {
break
}
}
return resultsCount, nil
}
func (b *BleveEngine) DeleteUserFiles(userID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(userID)
query.SetField("CreatorId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteUserFiles",
"bleveengine.delete_user_files.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files for user deleted", mlog.String("user_id", userID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeletePostFiles(postID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(postID)
query.SetField("PostId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeletePostFiles",
"bleveengine.delete_post_files.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files for post deleted", mlog.String("post_id", postID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeleteFilesBatch(endTime, limit int64) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
endTimeFloat := float64(endTime)
query := bleve.NewNumericRangeQuery(nil, &endTimeFloat)
query.SetField("CreateAt")
search := bleve.NewSearchRequestOptions(query, int(limit), 0, false)
search.SortBy([]string{"-CreateAt"})
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteFilesBatch",
"bleveengine.delete_files_batch.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files in batch deleted", mlog.Int64("endTime", endTime), mlog.Int64("limit", limit), mlog.Int64("deleted", deleted))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
)
func createPost(userId string, channelId string) *model.Post {
post := &model.Post{
Message: model.NewRandomString(15),
ChannelId: channelId,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: userId,
CreateAt: 1000000,
}
post.PreSave()
return post
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchengine
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func NewBroker(cfg *model.Config) *Broker {
return &Broker{
cfg: cfg,
}
}
func (seb *Broker) RegisterElasticsearchEngine(es SearchEngineInterface) {
seb.ElasticsearchEngine = es
}
func (seb *Broker) RegisterBleveEngine(be SearchEngineInterface) {
seb.BleveEngine = be
}
type Broker struct {
cfg *model.Config
ElasticsearchEngine SearchEngineInterface
BleveEngine SearchEngineInterface
}
func (seb *Broker) UpdateConfig(cfg *model.Config) *model.AppError {
seb.cfg = cfg
if seb.ElasticsearchEngine != nil {
seb.ElasticsearchEngine.UpdateConfig(cfg)
}
if seb.BleveEngine != nil {
seb.BleveEngine.UpdateConfig(cfg)
}
return nil
}
func (seb *Broker) GetActiveEngines() []SearchEngineInterface {
engines := []SearchEngineInterface{}
if seb.ElasticsearchEngine != nil && seb.ElasticsearchEngine.IsActive() {
engines = append(engines, seb.ElasticsearchEngine)
}
if seb.BleveEngine != nil && seb.BleveEngine.IsActive() {
engines = append(engines, seb.BleveEngine)
}
return engines
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchengine
import (
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
var EmailRegex = regexp.MustCompile(`^[^\s"]+@[^\s"]+$`)
func GetSuggestionInputsSplitBy(term, splitStr string) []string {
splitTerm := strings.Split(strings.ToLower(term), splitStr)
var initialSuggestionList []string
for i := range splitTerm {
initialSuggestionList = append(initialSuggestionList, strings.Join(splitTerm[i:], splitStr))
}
suggestionList := []string{}
// If splitStr is not an empty space, we create a suggestion with it at the beginning
if splitStr == " " {
suggestionList = initialSuggestionList
} else {
for i, suggestion := range initialSuggestionList {
if i == 0 {
suggestionList = append(suggestionList, suggestion)
} else {
suggestionList = append(suggestionList, splitStr+suggestion, suggestion)
}
}
}
return suggestionList
}
func GetSuggestionInputsSplitByMultiple(term string, splitStrs []string) []string {
suggestionList := []string{}
for _, splitStr := range splitStrs {
suggestionList = append(suggestionList, GetSuggestionInputsSplitBy(term, splitStr)...)
}
return utils.RemoveDuplicatesFromStringArray(suggestionList)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// postsToAttachments returns the file attachments for a slice of posts that need to be synchronized.
func (scs *Service) shouldSyncAttachment(fi *model.FileInfo, rc *model.RemoteCluster) bool {
sca, err := scs.server.GetStore().SharedChannel().GetAttachment(fi.Id, rc.RemoteId)
if err != nil {
if _, ok := err.(errNotFound); !ok {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error fetching shared channel attachment",
mlog.String("file_id", fi.Id),
mlog.String("remote_id", rc.RemoteId),
mlog.Err(err),
)
}
// no record so sync is needed
return true
}
return sca.LastSyncAt < fi.UpdateAt
}
// sendAttachmentForRemote asynchronously sends a file attachment to a remote cluster.
func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster for remote id %s; Remote Cluster Service not enabled", rc.RemoteId)
}
us := &model.UploadSession{
Id: model.NewId(),
Type: model.UploadTypeAttachment,
UserId: post.UserId,
ChannelId: post.ChannelId,
Filename: fi.Name,
FileSize: fi.Size,
RemoteId: rc.RemoteId,
ReqFileId: fi.Id,
}
payload, err := json.Marshal(us)
if err != nil {
return err
}
msg := model.NewRemoteClusterMsg(TopicUploadCreate, payload)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
var usResp model.UploadSession
var respErr error
var wg sync.WaitGroup
wg.Add(1)
// creating the upload session on the remote server needs to be done synchronously.
err = rcs.SendMsg(ctx, msg, rc, func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
defer wg.Done()
if err != nil {
respErr = err
return
}
if !resp.IsSuccess() {
respErr = errors.New(resp.Err)
return
}
respErr = json.Unmarshal(resp.Payload, &usResp)
})
if err != nil {
return fmt.Errorf("error sending create upload session to remote %s for post %s: %w", rc.RemoteId, post.Id, err)
}
wg.Wait()
if respErr != nil {
return fmt.Errorf("invalid create upload session response for remote %s and post %s: %w", rc.RemoteId, post.Id, respErr)
}
ctx2, cancel2 := context.WithTimeout(context.Background(), remotecluster.SendFileTimeout)
defer cancel2()
return rcs.SendFile(ctx2, &usResp, fi, rc, scs.app, func(us *model.UploadSession, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if err != nil {
return // this means the response could not be parsed; already logged
}
if !resp.IsSuccess() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "send file failed",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.String("err", resp.Err),
)
return
}
// response payload should be a model.FileInfo.
var fi model.FileInfo
if err2 := json.Unmarshal(resp.Payload, &fi); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "invalid file info response after send file",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.Err(err2),
)
return
}
// save file attachment record in SharedChannelAttachments table
sca := &model.SharedChannelAttachment{
FileId: fi.Id,
RemoteId: rc.RemoteId,
}
if _, err2 := scs.server.GetStore().SharedChannel().UpsertAttachment(sca); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error saving SharedChannelAttachment",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.Err(err2),
)
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "send file successful",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
)
})
}
// onReceiveUploadCreate is called when a message requesting to create an upload session is received. An upload session is
// created and the id returned in the response.
func (scs *Service) onReceiveUploadCreate(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
var us model.UploadSession
if err := json.Unmarshal(msg.Payload, &us); err != nil {
return fmt.Errorf("invalid upload session request: %w", err)
}
// make sure channel is shared for the remote sender
if _, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(us.ChannelId, rc.RemoteId); err != nil {
return fmt.Errorf("could not validate upload session for remote: %w", err)
}
us.RemoteId = rc.RemoteId // don't let remotes try to impersonate each other
// create upload session.
usSaved, appErr := scs.app.CreateUploadSession(request.EmptyContext(scs.server.Log()), &us)
if appErr != nil {
return appErr
}
response.SetPayload(usSaved)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// channelInviteMsg represents an invitation for a remote cluster to start sharing a channel.
type channelInviteMsg struct {
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
ReadOnly bool `json:"read_only"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
Type model.ChannelType `json:"type"`
DirectParticipantIDs []string `json:"direct_participant_ids"`
}
type InviteOption func(msg *channelInviteMsg)
func WithDirectParticipantID(participantID string) InviteOption {
return func(msg *channelInviteMsg) {
msg.DirectParticipantIDs = append(msg.DirectParticipantIDs, participantID)
}
}
// SendChannelInvite asynchronously sends a channel invite to a remote cluster. The remote cluster is
// expected to create a new channel with the same channel id, and respond with status OK.
// If an error occurs on the remote cluster then an ephemeral message is posted to in the channel for userId.
func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...InviteOption) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot invite remote cluster for channel id %s; Remote Cluster Service not enabled", channel.Id)
}
sc, err := scs.server.GetStore().SharedChannel().Get(channel.Id)
if err != nil {
return err
}
invite := channelInviteMsg{
ChannelId: channel.Id,
TeamId: rc.RemoteTeamId,
ReadOnly: sc.ReadOnly,
Name: sc.ShareName,
DisplayName: sc.ShareDisplayName,
Header: sc.ShareHeader,
Purpose: sc.SharePurpose,
Type: channel.Type,
}
for _, option := range options {
option(&invite)
}
json, err := json.Marshal(invite)
if err != nil {
return err
}
msg := model.NewRemoteClusterMsg(TopicChannelInvite, json)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
return rcs.SendMsg(ctx, msg, rc, func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if err != nil || !resp.IsSuccess() {
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error sending channel invite for %s: %s", rc.DisplayName, combineErrors(err, resp.Err)))
return
}
scr := &model.SharedChannelRemote{
ChannelId: sc.ChannelId,
CreatorId: userId,
RemoteId: rc.RemoteId,
IsInviteAccepted: true,
IsInviteConfirmed: true,
}
if _, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, err))
return
}
scs.NotifyChannelChanged(sc.ChannelId)
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("`%s` has been added to channel.", rc.DisplayName))
})
}
func combineErrors(err error, serror string) string {
var sb strings.Builder
if err != nil {
sb.WriteString(err.Error())
}
if serror != "" {
if sb.Len() > 0 {
sb.WriteString("; ")
}
sb.WriteString(serror)
}
return sb.String()
}
func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model.RemoteCluster, _ *remotecluster.Response) error {
if len(msg.Payload) == 0 {
return nil
}
var invite channelInviteMsg
if err := json.Unmarshal(msg.Payload, &invite); err != nil {
return fmt.Errorf("invalid channel invite: %w", err)
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Channel invite received",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", invite.ChannelId),
mlog.String("channel_name", invite.Name),
mlog.String("team_id", invite.TeamId),
)
// create channel if it doesn't exist; the channel may already exist, such as if it was shared then unshared at some point.
channel, err := scs.server.GetStore().Channel().Get(invite.ChannelId, true)
if err != nil {
if channel, err = scs.handleChannelCreation(invite, rc); err != nil {
return err
}
}
if invite.ReadOnly {
if err := scs.makeChannelReadOnly(channel); err != nil {
return fmt.Errorf("cannot make channel readonly `%s`: %w", invite.ChannelId, err)
}
}
sharedChannel := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: false,
ReadOnly: invite.ReadOnly,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: rc.CreatorId,
RemoteId: rc.RemoteId,
Type: channel.Type,
}
if _, err := scs.server.GetStore().SharedChannel().Save(sharedChannel); err != nil {
scs.app.PermanentDeleteChannel(request.EmptyContext(scs.server.Log()), channel)
return fmt.Errorf("cannot create shared channel (channel_id=%s): %w", invite.ChannelId, err)
}
sharedChannelRemote := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: channel.CreatorId,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: rc.RemoteId,
}
if _, err := scs.server.GetStore().SharedChannel().SaveRemote(sharedChannelRemote); err != nil {
scs.app.PermanentDeleteChannel(request.EmptyContext(scs.server.Log()), channel)
scs.server.GetStore().SharedChannel().Delete(sharedChannel.ChannelId)
return fmt.Errorf("cannot create shared channel remote (channel_id=%s): %w", invite.ChannelId, err)
}
return nil
}
func (scs *Service) handleChannelCreation(invite channelInviteMsg, rc *model.RemoteCluster) (*model.Channel, error) {
if invite.Type == model.ChannelTypeDirect {
return scs.createDirectChannel(invite)
}
channelNew := &model.Channel{
Id: invite.ChannelId,
TeamId: invite.TeamId,
Type: invite.Type,
DisplayName: invite.DisplayName,
Name: invite.Name,
Header: invite.Header,
Purpose: invite.Purpose,
CreatorId: rc.CreatorId,
Shared: model.NewBool(true),
}
// check user perms?
channel, appErr := scs.app.CreateChannelWithUser(request.EmptyContext(scs.server.Log()), channelNew, rc.CreatorId)
if appErr != nil {
return nil, fmt.Errorf("cannot create channel `%s`: %w", invite.ChannelId, appErr)
}
return channel, nil
}
func (scs *Service) createDirectChannel(invite channelInviteMsg) (*model.Channel, error) {
if len(invite.DirectParticipantIDs) != 2 {
return nil, fmt.Errorf("cannot create direct channel `%s` insufficient participant count `%d`", invite.ChannelId, len(invite.DirectParticipantIDs))
}
channel, err := scs.app.GetOrCreateDirectChannel(request.EmptyContext(scs.server.Log()), invite.DirectParticipantIDs[0], invite.DirectParticipantIDs[1], model.WithID(invite.ChannelId))
if err != nil {
return nil, fmt.Errorf("cannot create direct channel `%s`: %w", invite.ChannelId, err)
}
return channel, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
)
// syncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).
// It is sent to remote clusters as the payload of a `RemoteClusterMsg`.
type syncMsg struct {
Id string `json:"id"`
ChannelId string `json:"channel_id"`
Users map[string]*model.User `json:"users,omitempty"`
Posts []*model.Post `json:"posts,omitempty"`
Reactions []*model.Reaction `json:"reactions,omitempty"`
}
func newSyncMsg(channelID string) *syncMsg {
return &syncMsg{
Id: model.NewId(),
ChannelId: channelID,
}
}
func (sm *syncMsg) ToJSON() ([]byte, error) {
b, err := json.Marshal(sm)
if err != nil {
return nil, err
}
return b, nil
}
func (sm *syncMsg) String() string {
json, err := sm.ToJSON()
if err != nil {
return ""
}
return string(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"net/url"
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
// Team name regex taken from model.IsValidTeamName
permaLinkRegex = regexp.MustCompile(`https?://[0-9.\-A-Za-z]+/[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+/pl/([a-zA-Z0-9]+)`)
permaLinkSharedRegex = regexp.MustCompile(`https?://[0-9.\-A-Za-z]+/[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+/plshared/([a-zA-Z0-9]+)`)
)
const (
permalinkMarker = "plshared"
)
// processPermalinkToRemote processes all permalinks going towards a remote site.
func (scs *Service) processPermalinkToRemote(p *model.Post) string {
var sent bool
return permaLinkRegex.ReplaceAllStringFunc(p.Message, func(msg string) string {
// Extract the postID (This is simple enough not to warrant full-blown URL parsing.)
lastSlash := strings.LastIndexByte(msg, '/')
postID := msg[lastSlash+1:]
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
postList, err := scs.server.GetStore().Post().Get(context.Background(), postID, opts, "", map[string]bool{})
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Unable to get post during replacing permalinks", mlog.Err(err))
return msg
}
if len(postList.Order) == 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "No post found for permalink", mlog.String("postID", postID))
return msg
}
// If postID is for a different channel
if postList.Posts[postList.Order[0]].ChannelId != p.ChannelId {
// Send ephemeral message to OP (only once per message).
if !sent {
scs.sendEphemeralPost(p.ChannelId, p.UserId, i18n.T("sharedchannel.permalink.not_found"))
sent = true
}
// But don't modify msg
return msg
}
// Otherwise, modify pl to plshared as a marker to be replaced by remote sites
return strings.Replace(msg, "/pl/", "/"+permalinkMarker+"/", 1)
})
}
// processPermalinkFromRemote processes all permalinks coming from a remote site.
func (scs *Service) processPermalinkFromRemote(p *model.Post, team *model.Team) string {
return permaLinkSharedRegex.ReplaceAllStringFunc(p.Message, func(remoteLink string) string {
// Extract host name
parsed, err := url.Parse(remoteLink)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Unable to parse the remote link during replacing permalinks", mlog.Err(err))
return remoteLink
}
// Replace with local SiteURL
parsed.Scheme = scs.siteURL.Scheme
parsed.Host = scs.siteURL.Host
// Replace team name with local team
teamEnd := strings.Index(parsed.Path, "/"+permalinkMarker)
parsed.Path = "/" + team.Name + parsed.Path[teamEnd:]
// Replace plshared with pl
return strings.Replace(parsed.String(), "/"+permalinkMarker+"/", "/pl/", 1)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"errors"
"fmt"
"net/url"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TopicSync = "sharedchannel_sync"
TopicChannelInvite = "sharedchannel_invite"
TopicUploadCreate = "sharedchannel_upload"
MaxRetries = 3
MaxPostsPerSync = 12 // a bit more than one typical screenfull of posts
MaxUsersPerSync = 25
NotifyRemoteOfflineThreshold = time.Second * 10
NotifyMinimumDelay = time.Second * 2
MaxUpsertRetries = 25
ProfileImageSyncTimeout = time.Second * 5
KeyRemoteUsername = "RemoteUsername"
KeyRemoteEmail = "RemoteEmail"
)
// Mocks can be re-generated with `make sharedchannel-mocks`.
type ServerIface interface {
Config() *model.Config
IsLeader() bool
AddClusterLeaderChangedListener(listener func()) string
RemoveClusterLeaderChangedListener(id string)
GetStore() store.Store
Log() *mlog.Logger
GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace
}
type AppIface interface {
SendEphemeralPost(c request.CTX, userId string, post *model.Post) *model.Post
CreateChannelWithUser(c request.CTX, channel *model.Channel, userId string) (*model.Channel, *model.AppError)
GetOrCreateDirectChannel(c request.CTX, userId, otherUserId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError)
AddUserToTeamByTeamId(c *request.Context, teamId string, user *model.User) *model.AppError
PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError
CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks bool, setOnline bool) (savedPost *model.Post, err *model.AppError)
UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, *model.AppError)
SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError)
DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError
PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError)
CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError)
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
InvalidateCacheForUser(userID string)
NotifySharedChannelUserUpdate(user *model.User)
}
// errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.
type errNotFound interface {
IsErrNotFound() bool
}
// errInvalidInput allows checking against Store.ErrInvalidInput errors without making Store a dependency.
type errInvalidInput interface {
InvalidInputInfo() (entity string, field string, value any)
}
// Service provides shared channel synchronization.
type Service struct {
server ServerIface
app AppIface
changeSignal chan struct{}
// everything below guarded by `mux`
mux sync.RWMutex
active bool
leaderListenerId string
connectionStateListenerId string
done chan struct{}
tasks map[string]syncTask
syncTopicListenerId string
inviteTopicListenerId string
uploadTopicListenerId string
siteURL *url.URL
}
// NewSharedChannelService creates a RemoteClusterService instance.
func NewSharedChannelService(server ServerIface, app AppIface) (*Service, error) {
service := &Service{
server: server,
app: app,
changeSignal: make(chan struct{}, 1),
tasks: make(map[string]syncTask),
}
parsed, err := url.Parse(*server.Config().ServiceSettings.SiteURL)
if err != nil {
return nil, fmt.Errorf("unable to parse SiteURL: %w", err)
}
service.siteURL = parsed
return service, nil
}
// Start is called by the server on server start-up.
func (scs *Service) Start() error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return errors.New("Shared Channel Service cannot activate: requires Remote Cluster Service")
}
scs.mux.Lock()
scs.leaderListenerId = scs.server.AddClusterLeaderChangedListener(scs.onClusterLeaderChange)
scs.syncTopicListenerId = rcs.AddTopicListener(TopicSync, scs.onReceiveSyncMessage)
scs.inviteTopicListenerId = rcs.AddTopicListener(TopicChannelInvite, scs.onReceiveChannelInvite)
scs.uploadTopicListenerId = rcs.AddTopicListener(TopicUploadCreate, scs.onReceiveUploadCreate)
scs.connectionStateListenerId = rcs.AddConnectionStateListener(scs.onConnectionStateChange)
scs.mux.Unlock()
scs.onClusterLeaderChange()
return nil
}
// Shutdown is called by the server on server shutdown.
func (scs *Service) Shutdown() error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return errors.New("Shared Channel Service cannot shutdown: requires Remote Cluster Service")
}
scs.mux.Lock()
id := scs.leaderListenerId
rcs.RemoveTopicListener(scs.syncTopicListenerId)
scs.syncTopicListenerId = ""
rcs.RemoveTopicListener(scs.inviteTopicListenerId)
scs.inviteTopicListenerId = ""
rcs.RemoveConnectionStateListener(scs.connectionStateListenerId)
scs.connectionStateListenerId = ""
scs.mux.Unlock()
scs.server.RemoveClusterLeaderChangedListener(id)
scs.pause()
return nil
}
// Active determines whether the service is active on the node or not.
func (scs *Service) Active() bool {
scs.mux.Lock()
defer scs.mux.Unlock()
return scs.active
}
func (scs *Service) sendEphemeralPost(channelId string, userId string, text string) {
ephemeral := &model.Post{
ChannelId: channelId,
Message: text,
CreateAt: model.GetMillis(),
}
scs.app.SendEphemeralPost(request.EmptyContext(scs.server.Log()), userId, ephemeral)
}
// onClusterLeaderChange is called whenever the cluster leader may have changed.
func (scs *Service) onClusterLeaderChange() {
if scs.server.IsLeader() {
scs.resume()
} else {
scs.pause()
}
}
func (scs *Service) resume() {
scs.mux.Lock()
defer scs.mux.Unlock()
if scs.active {
return // already active
}
scs.active = true
scs.done = make(chan struct{})
go scs.syncLoop(scs.done)
scs.server.Log().Debug("Shared Channel Service active")
}
func (scs *Service) pause() {
scs.mux.Lock()
defer scs.mux.Unlock()
if !scs.active {
return // already inactive
}
scs.active = false
close(scs.done)
scs.done = nil
scs.server.Log().Debug("Shared Channel Service inactive")
}
// Makes the remote channel to be read-only(announcement mode, only admins can create posts and reactions).
func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError {
createPostPermission := model.ChannelModeratedPermissionsMap[model.PermissionCreatePost.Id]
createReactionPermission := model.ChannelModeratedPermissionsMap[model.PermissionAddReaction.Id]
updateMap := model.ChannelModeratedRolesPatch{
Guests: model.NewBool(false),
Members: model.NewBool(false),
}
readonlyChannelModerations := []*model.ChannelModerationPatch{
{
Name: &createPostPermission,
Roles: &updateMap,
},
{
Name: &createReactionPermission,
Roles: &updateMap,
},
}
_, err := scs.app.PatchChannelModerationsForChannel(request.EmptyContext(scs.server.Log()), channel, readonlyChannelModerations)
return err
}
// onConnectionStateChange is called whenever the connection state of a remote cluster changes,
// for example when one comes back online.
func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) {
if online {
// when a previously offline remote comes back online force a sync.
scs.ForceSyncForRemote(rc)
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Remote cluster connection status changed",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Bool("online", online),
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
if msg.Topic != TopicSync {
return fmt.Errorf("wrong topic, expected `%s`, got `%s`", TopicSync, msg.Topic)
}
if len(msg.Payload) == 0 {
return errors.New("empty sync message")
}
if scs.server.Log().IsLevelEnabled(mlog.LvlSharedChannelServiceMessagesInbound) {
scs.server.Log().Log(mlog.LvlSharedChannelServiceMessagesInbound, "inbound message",
mlog.String("remote", rc.DisplayName),
mlog.String("msg", string(msg.Payload)),
)
}
var sm syncMsg
if err := json.Unmarshal(msg.Payload, &sm); err != nil {
return fmt.Errorf("invalid sync message: %w", err)
}
return scs.processSyncMessage(request.EmptyContext(scs.server.Log()), &sm, rc, response)
}
func (scs *Service) processSyncMessage(c request.CTX, syncMsg *syncMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
var channel *model.Channel
var team *model.Team
var err error
syncResp := SyncResponse{
UserErrors: make([]string, 0),
UsersSyncd: make([]string, 0),
PostErrors: make([]string, 0),
ReactionErrors: make([]string, 0),
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Sync msg received",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.Int("user_count", len(syncMsg.Users)),
mlog.Int("post_count", len(syncMsg.Posts)),
mlog.Int("reaction_count", len(syncMsg.Reactions)),
)
if channel, err = scs.server.GetStore().Channel().Get(syncMsg.ChannelId, true); err != nil {
// if the channel doesn't exist then none of these sync items are going to work.
return fmt.Errorf("channel not found processing sync message: %w", err)
}
// add/update users before posts
for _, user := range syncMsg.Users {
if userSaved, err := scs.upsertSyncUser(c, user, channel, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync user",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.String("user_id", user.Id),
mlog.Err(err))
} else {
syncResp.UsersSyncd = append(syncResp.UsersSyncd, userSaved.Id)
if syncResp.UsersLastUpdateAt < user.UpdateAt {
syncResp.UsersLastUpdateAt = user.UpdateAt
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "User upserted via sync",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.String("user_id", user.Id),
)
}
}
for _, post := range syncMsg.Posts {
if syncMsg.ChannelId != post.ChannelId {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "ChannelId mismatch",
mlog.String("remote", rc.Name),
mlog.String("sm.ChannelId", syncMsg.ChannelId),
mlog.String("sm.Post.ChannelId", post.ChannelId),
mlog.String("PostId", post.Id),
)
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
continue
}
if channel.Type != model.ChannelTypeDirect && team == nil {
var err2 error
team, err2 = scs.server.GetStore().Channel().GetTeamForChannel(syncMsg.ChannelId)
if err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error getting Team for Channel",
mlog.String("ChannelId", post.ChannelId),
mlog.String("PostId", post.Id),
mlog.String("remote", rc.Name),
mlog.Err(err2),
)
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
continue
}
}
// process perma-links for remote
if team != nil {
post.Message = scs.processPermalinkFromRemote(post, team)
}
// add/update post
rpost, err := scs.upsertSyncPost(post, channel, rc)
if err != nil {
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.String("remote", rc.Name),
mlog.Err(err),
)
} else if syncResp.PostsLastUpdateAt < rpost.UpdateAt {
syncResp.PostsLastUpdateAt = rpost.UpdateAt
}
}
// add/remove reactions
for _, reaction := range syncMsg.Reactions {
if _, err := scs.upsertSyncReaction(reaction, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync reaction",
mlog.String("remote", rc.Name),
mlog.String("user_id", reaction.UserId),
mlog.String("post_id", reaction.PostId),
mlog.String("emoji", reaction.EmojiName),
mlog.Int64("delete_at", reaction.DeleteAt),
mlog.Err(err),
)
} else {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Reaction upserted via sync",
mlog.String("remote", rc.Name),
mlog.String("user_id", reaction.UserId),
mlog.String("post_id", reaction.PostId),
mlog.String("emoji", reaction.EmojiName),
mlog.Int64("delete_at", reaction.DeleteAt),
)
if syncResp.ReactionsLastUpdateAt < reaction.UpdateAt {
syncResp.ReactionsLastUpdateAt = reaction.UpdateAt
}
}
}
response.SetPayload(syncResp)
return nil
}
func (scs *Service) upsertSyncUser(c request.CTX, user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
if user.RemoteId == nil || *user.RemoteId == "" {
user.RemoteId = model.NewString(rc.RemoteId)
}
// Check if user already exists
euser, err := scs.server.GetStore().User().Get(context.Background(), user.Id)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return nil, fmt.Errorf("error checking sync user: %w", err)
}
}
var userSaved *model.User
if euser == nil {
if userSaved, err = scs.insertSyncUser(user, channel, rc); err != nil {
return nil, err
}
} else {
patch := &model.UserPatch{
Username: &user.Username,
Nickname: &user.Nickname,
FirstName: &user.FirstName,
LastName: &user.LastName,
Email: &user.Email,
Props: user.Props,
Position: &user.Position,
Locale: &user.Locale,
Timezone: user.Timezone,
RemoteId: user.RemoteId,
}
if userSaved, err = scs.updateSyncUser(patch, euser, channel, rc); err != nil {
return nil, err
}
}
// Add user to team. We do this here regardless of whether the user was
// just created or patched since there are three steps to adding a user
// (insert rec, add to team, add to channel) and any one could fail.
// Instead of undoing what succeeded on any failure we simply do all steps each
// time. AddUserToChannel & AddUserToTeamByTeamId do not error if user was already
// added and exit quickly.
if err := scs.app.AddUserToTeamByTeamId(request.EmptyContext(scs.server.Log()), channel.TeamId, userSaved); err != nil {
return nil, fmt.Errorf("error adding sync user to Team: %w", err)
}
// add user to channel
if _, err := scs.app.AddUserToChannel(c, userSaved, channel, false); err != nil {
return nil, fmt.Errorf("error adding sync user to ChannelMembers: %w", err)
}
return userSaved, nil
}
func (scs *Service) insertSyncUser(user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
var userSaved *model.User
var suffix string
// ensure the new user is created with system_user role and random password.
user = sanitizeUserForSync(user)
// save the original username and email in props (if not already done by another remote)
if _, ok := user.GetProp(KeyRemoteUsername); !ok {
user.SetProp(KeyRemoteUsername, user.Username)
}
if _, ok := user.GetProp(KeyRemoteEmail); !ok {
user.SetProp(KeyRemoteEmail, user.Email)
}
// Apply a suffix to the username until it is unique. Collisions will be quite
// rare since we are joining a username that is unique at a remote site with a unique
// name for that site. However we need to truncate the combined name to 64 chars and
// that might introduce a collision.
for i := 1; i <= MaxUpsertRetries; i++ {
if i > 1 {
suffix = strconv.FormatInt(int64(i), 10)
}
user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength)
user.Email = mungEmail(rc.Name, model.UserEmailMaxLength)
if userSaved, err = scs.server.GetStore().User().Save(user); err != nil {
e, ok := err.(errInvalidInput)
if !ok {
break
}
_, field, value := e.InvalidInputInfo()
if field == "email" || field == "username" {
// username or email collision; try again with different suffix
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Collision inserting sync user",
mlog.String("field", field),
mlog.Any("value", value),
mlog.Int("attempt", i),
mlog.Err(err),
)
}
} else {
scs.app.NotifySharedChannelUserUpdate(userSaved)
return userSaved, nil
}
}
return nil, fmt.Errorf("error inserting sync user %s: %w", user.Id, err)
}
func (scs *Service) updateSyncUser(patch *model.UserPatch, user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
var update *model.UserUpdate
var suffix string
// preserve existing real username/email since Patch will over-write them;
// the real username/email in props can be updated if they don't contain colons,
// meaning the update is coming from the user's origin server (not munged).
realUsername, _ := user.GetProp(KeyRemoteUsername)
realEmail, _ := user.GetProp(KeyRemoteEmail)
if patch.Username != nil && !strings.Contains(*patch.Username, ":") {
realUsername = *patch.Username
}
if patch.Email != nil && !strings.Contains(*patch.Email, ":") {
realEmail = *patch.Email
}
user.Patch(patch)
user = sanitizeUserForSync(user)
user.SetProp(KeyRemoteUsername, realUsername)
user.SetProp(KeyRemoteEmail, realEmail)
// Apply a suffix to the username until it is unique.
for i := 1; i <= MaxUpsertRetries; i++ {
if i > 1 {
suffix = strconv.FormatInt(int64(i), 10)
}
user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength)
user.Email = mungEmail(rc.Name, model.UserEmailMaxLength)
if update, err = scs.server.GetStore().User().Update(user, false); err != nil {
e, ok := err.(errInvalidInput)
if !ok {
break
}
_, field, value := e.InvalidInputInfo()
if field == "email" || field == "username" {
// username or email collision; try again with different suffix
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Collision updating sync user",
mlog.String("field", field),
mlog.Any("value", value),
mlog.Int("attempt", i),
mlog.Err(err),
)
}
} else {
scs.app.InvalidateCacheForUser(update.New.Id)
scs.app.NotifySharedChannelUserUpdate(update.New)
return update.New, nil
}
}
return nil, fmt.Errorf("error updating sync user %s: %w", user.Id, err)
}
func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc *model.RemoteCluster) (*model.Post, error) {
var appErr *model.AppError
post.RemoteId = model.NewString(rc.RemoteId)
rpost, err := scs.server.GetStore().Post().GetSingle(post.Id, true)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return nil, fmt.Errorf("error checking sync post: %w", err)
}
}
if rpost == nil {
// post doesn't exist; create new one
rpost, appErr = scs.app.CreatePost(request.EmptyContext(scs.server.Log()), post, channel, true, true)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Created sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else if post.DeleteAt > 0 {
// delete post
rpost, appErr = scs.app.DeletePost(request.EmptyContext(scs.server.Log()), post.Id, post.UserId)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Deleted sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else if post.EditAt > rpost.EditAt || post.Message != rpost.Message {
// update post
rpost, appErr = scs.app.UpdatePost(request.EmptyContext(scs.server.Log()), post, false)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Updated sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else {
// nothing to update
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Update to sync post ignored",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
var rerr error
if appErr != nil {
rerr = errors.New(appErr.Error())
}
return rpost, rerr
}
func (scs *Service) upsertSyncReaction(reaction *model.Reaction, rc *model.RemoteCluster) (*model.Reaction, error) {
savedReaction := reaction
var appErr *model.AppError
reaction.RemoteId = model.NewString(rc.RemoteId)
if reaction.DeleteAt == 0 {
savedReaction, appErr = scs.app.SaveReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
} else {
appErr = scs.app.DeleteReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
}
var err error
if appErr != nil {
err = errors.New(appErr.Error())
}
return savedReaction, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type syncTask struct {
id string
channelID string
remoteID string
AddedAt time.Time
retryCount int
retryMsg *syncMsg
schedule time.Time
}
func newSyncTask(channelID string, remoteID string, retryMsg *syncMsg) syncTask {
var retryID string
if retryMsg != nil {
retryID = retryMsg.Id
}
return syncTask{
id: channelID + remoteID + retryID, // combination of ids to avoid duplicates
channelID: channelID,
remoteID: remoteID, // empty means update all remote clusters
retryMsg: retryMsg,
schedule: time.Now(),
}
}
// incRetry increments the retry counter and returns true if MaxRetries not exceeded.
func (st *syncTask) incRetry() bool {
st.retryCount++
return st.retryCount <= MaxRetries
}
// NotifyChannelChanged is called to indicate that a shared channel has been modified,
// thus triggering an update to all remote clusters.
func (scs *Service) NotifyChannelChanged(channelID string) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
task := newSyncTask(channelID, "", nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
// NotifyUserProfileChanged is called to indicate that a user belonging to at least one
// shared channel has modified their user profile (name, username, email, custom status, profile image)
func (scs *Service) NotifyUserProfileChanged(userID string) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
scusers, err := scs.server.GetStore().SharedChannel().GetUsersForUser(userID)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch shared channel users",
mlog.String("userID", userID),
mlog.Err(err),
)
return
}
if len(scusers) == 0 {
return
}
notified := make(map[string]struct{})
for _, user := range scusers {
// update every channel + remote combination they belong to.
// Redundant updates (ie. to same remote for multiple channels) will be
// filtered out.
combo := user.ChannelId + user.RemoteId
if _, ok := notified[combo]; ok {
continue
}
notified[combo] = struct{}{}
task := newSyncTask(user.ChannelId, user.RemoteId, nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
}
// ForceSyncForRemote causes all channels shared with the remote to be synchronized.
func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
// fetch all channels shared with this remote.
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: rc.RemoteId,
}
scrs, err := scs.server.GetStore().SharedChannel().GetRemotes(opts)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch shared channel remotes",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
return
}
for _, scr := range scrs {
task := newSyncTask(scr.ChannelId, rc.RemoteId, nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
}
// addTask adds or re-adds a task to the queue.
func (scs *Service) addTask(task syncTask) {
task.AddedAt = time.Now()
scs.mux.Lock()
if _, ok := scs.tasks[task.id]; !ok {
scs.tasks[task.id] = task
}
scs.mux.Unlock()
// wake up the sync goroutine
select {
case scs.changeSignal <- struct{}{}:
default:
// that's ok, the sync routine is already busy
}
}
// syncLoop is called via a dedicated goroutine to wait for notifications of channel changes and
// updates each remote based on those changes.
func (scs *Service) syncLoop(done chan struct{}) {
// create a timer to periodically check the task queue, but only if there is
// a delayed task in the queue.
delay := time.NewTimer(NotifyMinimumDelay)
defer stopTimer(delay)
// wait for channel changed signal and update for oldest task.
for {
select {
case <-scs.changeSignal:
if wait := scs.doSync(); wait > 0 {
stopTimer(delay)
delay.Reset(wait)
}
case <-delay.C:
if wait := scs.doSync(); wait > 0 {
delay.Reset(wait)
}
case <-done:
return
}
}
}
func stopTimer(timer *time.Timer) {
timer.Stop()
select {
case <-timer.C:
default:
}
}
// doSync checks the task queue for any tasks to be processed and processes all that are ready.
// If any delayed tasks remain in queue then the duration until the next scheduled task is returned.
func (scs *Service) doSync() time.Duration {
var task syncTask
var ok bool
var shortestWait time.Duration
for {
task, ok, shortestWait = scs.removeOldestTask()
if !ok {
break
}
if err := scs.processTask(task); err != nil {
// put task back into map so it will update again
if task.incRetry() {
scs.addTask(task)
} else {
scs.server.Log().Error("Failed to synchronize shared channel",
mlog.String("channelId", task.channelID),
mlog.String("remoteId", task.remoteID),
mlog.Err(err),
)
}
}
}
return shortestWait
}
// removeOldestTask removes and returns the oldest task in the task map.
// A task coming in via NotifyChannelChanged must stay in queue for at least
// `NotifyMinimumDelay` to ensure we don't go nuts trying to sync during a bulk update.
// If no tasks are available then false is returned.
func (scs *Service) removeOldestTask() (syncTask, bool, time.Duration) {
scs.mux.Lock()
defer scs.mux.Unlock()
var oldestTask syncTask
var oldestKey string
var shortestWait time.Duration
for key, task := range scs.tasks {
// check if task is ready
if wait := time.Until(task.schedule); wait > 0 {
if wait < shortestWait || shortestWait == 0 {
shortestWait = wait
}
continue
}
// task is ready; check if it's the oldest ready task
if task.AddedAt.Before(oldestTask.AddedAt) || oldestTask.AddedAt.IsZero() {
oldestKey = key
oldestTask = task
}
}
if oldestKey != "" {
delete(scs.tasks, oldestKey)
return oldestTask, true, shortestWait
}
return oldestTask, false, shortestWait
}
// processTask updates one or more remote clusters with any new channel content.
func (scs *Service) processTask(task syncTask) error {
var err error
var remotes []*model.RemoteCluster
if task.remoteID == "" {
filter := model.RemoteClusterQueryFilter{
InChannel: task.channelID,
OnlyConfirmed: true,
}
remotes, err = scs.server.GetStore().RemoteCluster().GetAll(filter)
if err != nil {
return err
}
} else {
rc, err := scs.server.GetStore().RemoteCluster().Get(task.remoteID)
if err != nil {
return err
}
if !rc.IsOnline() {
return fmt.Errorf("Failed updating shared channel '%s' for offline remote cluster '%s'", task.channelID, rc.DisplayName)
}
remotes = []*model.RemoteCluster{rc}
}
for _, rc := range remotes {
rtask := task
rtask.remoteID = rc.RemoteId
if err := scs.syncForRemote(rtask, rc); err != nil {
// retry...
if rtask.incRetry() {
scs.addTask(rtask)
} else {
scs.server.Log().Error("Failed to synchronize shared channel for remote cluster",
mlog.String("channelId", rtask.channelID),
mlog.String("remote", rc.DisplayName),
mlog.Err(err),
)
}
}
}
return nil
}
func (scs *Service) handlePostError(postId string, task syncTask, rc *model.RemoteCluster) {
if task.retryMsg != nil && len(task.retryMsg.Posts) == 1 && task.retryMsg.Posts[0].Id == postId {
// this was a retry for specific post that failed previously. Try again if within MaxRetries.
if task.incRetry() {
scs.addTask(task)
} else {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error syncing post",
mlog.String("remote", rc.DisplayName),
mlog.String("post_id", postId),
)
}
return
}
// this post failed as part of a group of posts. Retry as an individual post.
post, err := scs.server.GetStore().Post().GetSingle(postId, true)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error fetching post for sync retry",
mlog.String("remote", rc.DisplayName),
mlog.String("post_id", postId),
)
return
}
syncMsg := newSyncMsg(task.channelID)
syncMsg.Posts = []*model.Post{post}
scs.addTask(newSyncTask(task.channelID, task.remoteID, syncMsg))
}
// notifyRemoteOffline creates an ephemeral post to the author for any posts created recently to remotes
// that are offline.
func (scs *Service) notifyRemoteOffline(posts []*model.Post, rc *model.RemoteCluster) {
// only send one ephemeral post per author.
notified := make(map[string]bool)
// range the slice in reverse so the newest posts are visited first; this ensures an ephemeral
// get added where it is mostly likely to be seen.
for i := len(posts) - 1; i >= 0; i-- {
post := posts[i]
if didNotify := notified[post.UserId]; didNotify {
continue
}
postCreateAt := model.GetTimeForMillis(post.CreateAt)
if post.DeleteAt == 0 && post.UserId != "" && time.Since(postCreateAt) < NotifyRemoteOfflineThreshold {
T := scs.getUserTranslations(post.UserId)
ephemeral := &model.Post{
ChannelId: post.ChannelId,
Message: T("sharedchannel.cannot_deliver_post", map[string]any{"Remote": rc.DisplayName}),
CreateAt: post.CreateAt + 1,
}
scs.app.SendEphemeralPost(request.EmptyContext(scs.server.Log()), post.UserId, ephemeral)
notified[post.UserId] = true
}
}
}
func (scs *Service) updateCursorForRemote(scrId string, rc *model.RemoteCluster, cursor model.GetPostsSinceForSyncCursor) {
if err := scs.server.GetStore().SharedChannel().UpdateRemoteCursor(scrId, cursor); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error updating cursor for shared channel remote",
mlog.String("remote", rc.DisplayName),
mlog.Err(err),
)
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "updated cursor for remote",
mlog.String("remote_id", rc.RemoteId),
mlog.String("remote", rc.DisplayName),
mlog.Int64("last_post_update_at", cursor.LastPostUpdateAt),
mlog.String("last_post_id", cursor.LastPostId),
)
}
func (scs *Service) getUserTranslations(userId string) i18n.TranslateFunc {
var locale string
user, err := scs.server.GetStore().User().Get(context.Background(), userId)
if err == nil {
locale = user.Locale
}
if locale == "" {
locale = model.DefaultLocale
}
return i18n.GetUserTranslations(locale)
}
// shouldUserSync determines if a user needs to be synchronized.
// User should be synchronized if it has no entry in the SharedChannelUsers table for the specified channel,
// or there is an entry but the LastSyncAt is less than user.UpdateAt
func (scs *Service) shouldUserSync(user *model.User, channelID string, rc *model.RemoteCluster) (sync bool, syncImage bool, err error) {
// don't sync users with the remote they originated from.
if user.RemoteId != nil && *user.RemoteId == rc.RemoteId {
return false, false, nil
}
scu, err := scs.server.GetStore().SharedChannel().GetSingleUser(user.Id, channelID, rc.RemoteId)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return false, false, err
}
// user not in the SharedChannelUsers table, so we must add them.
scu = &model.SharedChannelUser{
UserId: user.Id,
RemoteId: rc.RemoteId,
ChannelId: channelID,
}
if _, err = scs.server.GetStore().SharedChannel().SaveUser(scu); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error adding user to shared channel users",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.String("channel_id", user.Id),
mlog.Err(err),
)
}
return true, true, nil
}
return user.UpdateAt > scu.LastSyncAt, user.LastPictureUpdate > scu.LastSyncAt, nil
}
func (scs *Service) syncProfileImage(user *model.User, channelID string, rc *model.RemoteCluster) {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), ProfileImageSyncTimeout)
defer cancel()
rcs.SendProfileImage(ctx, user.Id, rc, scs.app, func(userId string, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if resp.IsSuccess() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Users profile image synchronized",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
)
if err2 := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(user.Id, channelID, rc.RemoteId); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error updating users LastSyncTime after profile image update",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.Err(err2),
)
}
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error synchronizing users profile image",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.Err(err),
)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"fmt"
"sync"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type sendSyncMsgResultFunc func(syncResp SyncResponse, err error)
type attachment struct {
fi *model.FileInfo
post *model.Post
}
type syncData struct {
task syncTask
rc *model.RemoteCluster
scr *model.SharedChannelRemote
users map[string]*model.User
profileImages map[string]*model.User
posts []*model.Post
reactions []*model.Reaction
attachments []attachment
resultRepeat bool
resultNextCursor model.GetPostsSinceForSyncCursor
}
func newSyncData(task syncTask, rc *model.RemoteCluster, scr *model.SharedChannelRemote) *syncData {
return &syncData{
task: task,
rc: rc,
scr: scr,
users: make(map[string]*model.User),
profileImages: make(map[string]*model.User),
resultNextCursor: model.GetPostsSinceForSyncCursor{LastPostUpdateAt: scr.LastPostUpdateAt, LastPostId: scr.LastPostId},
}
}
func (sd *syncData) isEmpty() bool {
return len(sd.users) == 0 && len(sd.profileImages) == 0 && len(sd.posts) == 0 && len(sd.reactions) == 0 && len(sd.attachments) == 0
}
func (sd *syncData) isCursorChanged() bool {
return sd.scr.LastPostUpdateAt != sd.resultNextCursor.LastPostUpdateAt || sd.scr.LastPostId != sd.resultNextCursor.LastPostId
}
// syncForRemote updates a remote cluster with any new posts/reactions for a specific
// channel. If many changes are found, only the oldest X changes are sent and the channel
// is re-added to the task map. This ensures no channels are starved for updates even if some
// channels are very active.
// Returning an error forces a retry on the task.
func (scs *Service) syncForRemote(task syncTask, rc *model.RemoteCluster) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster %s for channel id %s; Remote Cluster Service not enabled", rc.Name, task.channelID)
}
scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(task.channelID, rc.RemoteId)
if err != nil {
return err
}
// if this is retrying a failed msg, just send it again.
if task.retryMsg != nil {
sd := newSyncData(task, rc, scr)
sd.users = task.retryMsg.Users
sd.posts = task.retryMsg.Posts
sd.reactions = task.retryMsg.Reactions
return scs.sendSyncData(sd)
}
sd := newSyncData(task, rc, scr)
// schedule another sync if the repeat flag is set at some point.
defer func(rpt *bool) {
if *rpt {
scs.addTask(newSyncTask(task.channelID, task.remoteID, nil))
}
}(&sd.resultRepeat)
// fetch new posts or retry post.
if err := scs.fetchPostsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch posts for sync %v: %w", sd, err)
}
if !rc.IsOnline() {
if len(sd.posts) != 0 {
scs.notifyRemoteOffline(sd.posts, rc)
}
sd.resultRepeat = false
return nil
}
// fetch users that have updated their user profile or image.
if err := scs.fetchUsersForSync(sd); err != nil {
return fmt.Errorf("cannot fetch users for sync %v: %w", sd, err)
}
// fetch reactions for posts
if err := scs.fetchReactionsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch reactions for sync %v: %w", sd, err)
}
// fetch users associated with posts & reactions
if err := scs.fetchPostUsersForSync(sd); err != nil {
return fmt.Errorf("cannot fetch post users for sync %v: %w", sd, err)
}
// filter out any posts that don't need to be sent.
scs.filterPostsForSync(sd)
// fetch attachments for posts
if err := scs.fetchPostAttachmentsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch post attachments for sync %v: %w", sd, err)
}
if sd.isEmpty() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Not sending sync data; everything filtered out",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", task.channelID),
mlog.Bool("repeat", sd.resultRepeat),
)
if sd.isCursorChanged() {
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
}
return nil
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Sending sync data",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", task.channelID),
mlog.Bool("repeat", sd.resultRepeat),
mlog.Int("users", len(sd.users)),
mlog.Int("images", len(sd.profileImages)),
mlog.Int("posts", len(sd.posts)),
mlog.Int("reactions", len(sd.reactions)),
mlog.Int("attachments", len(sd.attachments)),
)
return scs.sendSyncData(sd)
}
// fetchUsersForSync populates the sync data with any channel users who updated their user profile
// since the last sync.
func (scs *Service) fetchUsersForSync(sd *syncData) error {
filter := model.GetUsersForSyncFilter{
ChannelID: sd.task.channelID,
Limit: MaxUsersPerSync,
}
users, err := scs.server.GetStore().SharedChannel().GetUsersForSync(filter)
if err != nil {
return err
}
for _, u := range users {
if u.GetRemoteID() != sd.rc.RemoteId {
sd.users[u.Id] = u
}
}
filter.CheckProfileImage = true
usersImage, err := scs.server.GetStore().SharedChannel().GetUsersForSync(filter)
if err != nil {
return err
}
for _, u := range usersImage {
if u.GetRemoteID() != sd.rc.RemoteId {
sd.profileImages[u.Id] = u
}
}
return nil
}
// fetchPostsForSync populates the sync data with any new posts since the last sync.
func (scs *Service) fetchPostsForSync(sd *syncData) error {
options := model.GetPostsSinceForSyncOptions{
ChannelId: sd.task.channelID,
IncludeDeleted: true,
}
cursor := model.GetPostsSinceForSyncCursor{
LastPostUpdateAt: sd.scr.LastPostUpdateAt,
LastPostId: sd.scr.LastPostId,
}
posts, nextCursor, err := scs.server.GetStore().Post().GetPostsSinceForSync(options, cursor, MaxPostsPerSync)
if err != nil {
return fmt.Errorf("could not fetch new posts for sync: %w", err)
}
// Append the posts individually, checking for root posts that might appear later in the list.
// This is due to the UpdateAt collision handling algorithm where the order of posts is not based
// on UpdateAt or CreateAt when the posts have the same UpdateAt value. Here we are guarding
// against a root post with the same UpdateAt (and probably the same CreateAt) appearing later
// in the list and must be sync'd before the child post. This is and edge case that likely only
// happens during load testing or bulk imports.
for _, p := range posts {
if p.RootId != "" {
root, err := scs.server.GetStore().Post().GetSingle(p.RootId, true)
if err == nil {
if (root.CreateAt >= cursor.LastPostUpdateAt || root.UpdateAt >= cursor.LastPostUpdateAt) && !containsPost(sd.posts, root) {
sd.posts = append(sd.posts, root)
}
}
}
sd.posts = append(sd.posts, p)
}
sd.resultNextCursor = nextCursor
sd.resultRepeat = len(posts) == MaxPostsPerSync
return nil
}
func containsPost(posts []*model.Post, post *model.Post) bool {
for _, p := range posts {
if p.Id == post.Id {
return true
}
}
return false
}
// fetchReactionsForSync populates the sync data with any new reactions since the last sync.
func (scs *Service) fetchReactionsForSync(sd *syncData) error {
merr := merror.New()
for _, post := range sd.posts {
// any reactions originating from the remote cluster are filtered out
reactions, err := scs.server.GetStore().Reaction().GetForPostSince(post.Id, sd.scr.LastPostUpdateAt, sd.rc.RemoteId, true)
if err != nil {
merr.Append(fmt.Errorf("could not get reactions for post %s: %w", post.Id, err))
continue
}
sd.reactions = append(sd.reactions, reactions...)
}
return merr.ErrorOrNil()
}
// fetchPostUsersForSync populates the sync data with all users associated with posts.
func (scs *Service) fetchPostUsersForSync(sd *syncData) error {
sc, err := scs.server.GetStore().SharedChannel().Get(sd.task.channelID)
if err != nil {
return fmt.Errorf("cannot determine teamID: %w", err)
}
type p2mm struct {
post *model.Post
mentionMap model.UserMentionMap
}
userIDs := make(map[string]p2mm)
for _, reaction := range sd.reactions {
userIDs[reaction.UserId] = p2mm{}
}
for _, post := range sd.posts {
// add author
userIDs[post.UserId] = p2mm{}
// get mentions and users for each mention
mentionMap := scs.app.MentionsToTeamMembers(request.EmptyContext(scs.server.Log()), post.Message, sc.TeamId)
for _, userID := range mentionMap {
userIDs[userID] = p2mm{
post: post,
mentionMap: mentionMap,
}
}
}
merr := merror.New()
for userID, v := range userIDs {
user, err := scs.server.GetStore().User().Get(context.Background(), userID)
if err != nil {
merr.Append(fmt.Errorf("could not get user %s: %w", userID, err))
continue
}
sync, syncImage, err2 := scs.shouldUserSync(user, sd.task.channelID, sd.rc)
if err2 != nil {
merr.Append(fmt.Errorf("could not check should sync user %s: %w", userID, err))
continue
}
if sync {
sd.users[user.Id] = user
}
if syncImage {
sd.profileImages[user.Id] = user
}
// if this was a mention then put the real username in place of the username+remotename, but only
// when sending to the remote that the user belongs to.
if v.post != nil && user.RemoteId != nil && *user.RemoteId == sd.rc.RemoteId {
fixMention(v.post, v.mentionMap, user)
}
}
return merr.ErrorOrNil()
}
// fetchPostAttachmentsForSync populates the sync data with any file attachments for new posts.
func (scs *Service) fetchPostAttachmentsForSync(sd *syncData) error {
merr := merror.New()
for _, post := range sd.posts {
fis, err := scs.server.GetStore().FileInfo().GetForPost(post.Id, false, true, true)
if err != nil {
merr.Append(fmt.Errorf("could not get file attachment info for post %s: %w", post.Id, err))
continue
}
for _, fi := range fis {
if scs.shouldSyncAttachment(fi, sd.rc) {
sd.attachments = append(sd.attachments, attachment{fi: fi, post: post})
}
}
}
return merr.ErrorOrNil()
}
// filterPostsforSync removes any posts that do not need to sync.
func (scs *Service) filterPostsForSync(sd *syncData) {
filtered := make([]*model.Post, 0, len(sd.posts))
for _, p := range sd.posts {
// Don't resend an existing post where only the reactions changed.
// Posts we must send:
// - new posts (EditAt == 0)
// - edited posts (EditAt >= LastPostUpdateAt)
// - deleted posts (DeleteAt > 0)
if p.EditAt > 0 && p.EditAt < sd.scr.LastPostUpdateAt && p.DeleteAt == 0 {
continue
}
// Don't send a deleted post if it is just the original copy from an edit.
if p.DeleteAt > 0 && p.OriginalId != "" {
continue
}
// don't sync a post back to the remote it came from.
if p.GetRemoteID() == sd.rc.RemoteId {
continue
}
// parse out all permalinks in the message.
p.Message = scs.processPermalinkToRemote(p)
filtered = append(filtered, p)
}
sd.posts = filtered
}
// sendSyncData sends all the collected users, posts, reactions, images, and attachments to the
// remote cluster.
// The order of items sent is important: users -> attachments -> posts -> reactions -> profile images
func (scs *Service) sendSyncData(sd *syncData) error {
merr := merror.New()
sanitizeSyncData(sd)
// send users
if len(sd.users) != 0 {
if err := scs.sendUserSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send user sync data: %w", err))
}
}
// send attachments
if len(sd.attachments) != 0 {
scs.sendAttachmentSyncData(sd)
}
// send posts
if len(sd.posts) != 0 {
if err := scs.sendPostSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send post sync data: %w", err))
}
} else if sd.isCursorChanged() {
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
}
// send reactions
if len(sd.reactions) != 0 {
if err := scs.sendReactionSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send reaction sync data: %w", err))
}
}
// send user profile images
if len(sd.profileImages) != 0 {
scs.sendProfileImageSyncData(sd)
}
return merr.ErrorOrNil()
}
// sendUserSyncData sends the collected user updates to the remote cluster.
func (scs *Service) sendUserSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Users = sd.users
err := scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
for _, userID := range syncResp.UsersSyncd {
if err := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(userID, sd.task.channelID, sd.rc.RemoteId); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Cannot update shared channel user LastSyncAt",
mlog.String("user_id", userID),
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Err(err),
)
}
}
if len(syncResp.UserErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for user(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("users", syncResp.UserErrors),
)
}
})
return err
}
// sendAttachmentSyncData sends the collected post updates to the remote cluster.
func (scs *Service) sendAttachmentSyncData(sd *syncData) {
for _, a := range sd.attachments {
if err := scs.sendAttachmentForRemote(a.fi, a.post, sd.rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Cannot sync post attachment",
mlog.String("post_id", a.post.Id),
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Err(err),
)
}
// updating SharedChannelAttachments with LastSyncAt is already done.
}
}
// sendPostSyncData sends the collected post updates to the remote cluster.
func (scs *Service) sendPostSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Posts = sd.posts
return scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
if len(syncResp.PostErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for post(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("posts", syncResp.PostErrors),
)
for _, postID := range syncResp.PostErrors {
scs.handlePostError(postID, sd.task, sd.rc)
}
}
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
})
}
// sendReactionSyncData sends the collected reaction updates to the remote cluster.
func (scs *Service) sendReactionSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Reactions = sd.reactions
return scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
if len(syncResp.ReactionErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for reactions(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("reaction_posts", syncResp.ReactionErrors),
)
}
})
}
// sendProfileImageSyncData sends the collected user profile image updates to the remote cluster.
func (scs *Service) sendProfileImageSyncData(sd *syncData) {
for _, user := range sd.profileImages {
scs.syncProfileImage(user, sd.task.channelID, sd.rc)
}
}
// sendSyncMsgToRemote synchronously sends the sync message to the remote cluster.
func (scs *Service) sendSyncMsgToRemote(msg *syncMsg, rc *model.RemoteCluster, f sendSyncMsgResultFunc) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster %s for channel id %s; Remote Cluster Service not enabled", rc.Name, msg.ChannelId)
}
b, err := json.Marshal(msg)
if err != nil {
return err
}
rcMsg := model.NewRemoteClusterMsg(TopicSync, b)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
err = rcs.SendMsg(ctx, rcMsg, rc, func(rcMsg model.RemoteClusterMsg, rc *model.RemoteCluster, rcResp *remotecluster.Response, errResp error) {
defer wg.Done()
var syncResp SyncResponse
if err2 := json.Unmarshal(rcResp.Payload, &syncResp); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Invalid sync msg response from remote cluster",
mlog.String("remote", rc.Name),
mlog.String("channel_id", msg.ChannelId),
mlog.Err(err2),
)
return
}
if f != nil {
f(syncResp, errResp)
}
})
wg.Wait()
return err
}
func sanitizeSyncData(sd *syncData) {
for id, user := range sd.users {
sd.users[id] = sanitizeUserForSync(user)
}
for id, user := range sd.profileImages {
sd.profileImages[id] = sanitizeUserForSync(user)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// fixMention replaces any mentions in a post for the user with the user's real username.
func fixMention(post *model.Post, mentionMap model.UserMentionMap, user *model.User) {
if post == nil || len(mentionMap) == 0 {
return
}
realUsername, ok := user.GetProp(KeyRemoteUsername)
if !ok {
return
}
// there may be more than one mention for each user so we have to walk the whole map.
for mention, id := range mentionMap {
if id == user.Id && strings.Contains(mention, ":") {
post.Message = strings.ReplaceAll(post.Message, "@"+mention, "@"+realUsername)
}
}
}
func sanitizeUserForSync(user *model.User) *model.User {
user.Password = model.NewId()
user.AuthData = nil
user.AuthService = ""
user.Roles = "system_user"
user.AllowMarketing = false
user.NotifyProps = model.StringMap{}
user.LastPasswordUpdate = 0
user.LastPictureUpdate = 0
user.FailedAttempts = 0
user.MfaActive = false
user.MfaSecret = ""
return user
}
// mungUsername creates a new username by combining username and remote cluster name, plus
// a suffix to create uniqueness. If the resulting username exceeds the max length then
// it is truncated and ellipses added.
func mungUsername(username string, remotename string, suffix string, maxLen int) string {
if suffix != "" {
suffix = "~" + suffix
}
// If the username already contains a colon then another server already munged it.
// In that case we can split on the colon and use the existing remote name.
// We still need to re-mung with suffix in case of collision.
comps := strings.Split(username, ":")
if len(comps) >= 2 {
username = comps[0]
remotename = strings.Join(comps[1:], "")
}
var userEllipses string
var remoteEllipses string
// The remotename is allowed to use up to half the maxLen, and the username gets the remaining space.
// Username might have a suffix to account for, and remotename always has a preceding colon.
half := maxLen / 2
// If the remotename is less than half the maxLen, then the left over space can be given to
// the username.
extra := half - (len(remotename) + 1)
if extra < 0 {
extra = 0
}
truncUser := (len(username) + len(suffix)) - (half + extra)
if truncUser > 0 {
username = username[:len(username)-truncUser-3]
userEllipses = "..."
}
truncRemote := (len(remotename) + 1) - (maxLen - (len(username) + len(userEllipses) + len(suffix)))
if truncRemote > 0 {
remotename = remotename[:len(remotename)-truncRemote-3]
remoteEllipses = "..."
}
return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses)
}
// mungEmail creates a unique email address using a UID and remote name.
func mungEmail(remotename string, maxLen int) string {
s := fmt.Sprintf("%s@%s", model.NewId(), remotename)
if len(s) > maxLen {
s = s[:maxLen]
}
return s
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"regexp"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func slackConvertTimeStamp(ts string) int64 {
timeString := strings.SplitN(ts, ".", 2)[0]
timeStamp, err := strconv.ParseInt(timeString, 10, 64)
if err != nil {
mlog.Warn("Slack Import: Bad timestamp detected.")
return 1
}
return timeStamp * 1000 // Convert to milliseconds
}
func slackConvertChannelName(channelName string, channelId string) string {
newName := strings.Trim(channelName, "_-")
if len(newName) == 1 {
return "slack-channel-" + newName
}
if isValidChannelNameCharacters(newName) {
return newName
}
return strings.ToLower(channelId)
}
func slackConvertUserMentions(users []slackUser, posts map[string][]slackPost) map[string][]slackPost {
var regexes = make(map[string]*regexp.Regexp, len(users))
for _, user := range users {
r, err := regexp.Compile("<@" + user.Id + `(\|` + user.Username + ")?>")
if err != nil {
mlog.Warn("Slack Import: Unable to compile the @mention, matching regular expression for the Slack user.", mlog.String("user_name", user.Username), mlog.String("user_id", user.Id))
continue
}
regexes["@"+user.Username] = r
}
// Special cases.
regexes["@here"], _ = regexp.Compile(`<!here\|@here>`)
regexes["@channel"], _ = regexp.Compile("<!channel>")
regexes["@all"], _ = regexp.Compile("<!everyone>")
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
for mention, r := range regexes {
post.Text = r.ReplaceAllString(post.Text, mention)
posts[channelName][postIdx] = post
}
}
}
return posts
}
func slackConvertChannelMentions(channels []slackChannel, posts map[string][]slackPost) map[string][]slackPost {
var regexes = make(map[string]*regexp.Regexp, len(channels))
for _, channel := range channels {
r, err := regexp.Compile("<#" + channel.Id + `(\|` + channel.Name + ")?>")
if err != nil {
mlog.Warn("Slack Import: Unable to compile the !channel, matching regular expression for the Slack channel.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
continue
}
regexes["~"+channel.Name] = r
}
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
for channelReplace, r := range regexes {
post.Text = r.ReplaceAllString(post.Text, channelReplace)
posts[channelName][postIdx] = post
}
}
}
return posts
}
func slackConvertPostsMarkup(posts map[string][]slackPost) map[string][]slackPost {
regexReplaceAllString := []struct {
regex *regexp.Regexp
rpl string
}{
// URL
{
regexp.MustCompile(`<([^|<>]+)\|([^|<>]+)>`),
"[$2]($1)",
},
// bold
{
regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
"$1**$2**",
},
// strikethrough
{
regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
"$1~~$2~~",
},
// single paragraph blockquote
// Slack converts > character to >
{
regexp.MustCompile(`(?sm)^>`),
">",
},
}
regexReplaceAllStringFunc := []struct {
regex *regexp.Regexp
fn func(string) string
}{
// multiple paragraphs blockquotes
{
regexp.MustCompile(`(?sm)^>>>(.+)$`),
func(src string) string {
// remove >>> prefix, might have leading \n
prefixRegexp := regexp.MustCompile(`^([\n])?>>>(.*)`)
src = prefixRegexp.ReplaceAllString(src, "$1$2")
// append > to start of line
appendRegexp := regexp.MustCompile(`(?m)^`)
return appendRegexp.ReplaceAllString(src, ">$0")
},
},
}
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
result := post.Text
for _, rule := range regexReplaceAllString {
result = rule.regex.ReplaceAllString(result, rule.rpl)
}
for _, rule := range regexReplaceAllStringFunc {
result = rule.regex.ReplaceAllStringFunc(result, rule.fn)
}
posts[channelName][postIdx].Text = result
}
}
return posts
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"encoding/json"
"io"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func slackParseChannels(data io.Reader, channelType model.ChannelType) ([]slackChannel, error) {
decoder := json.NewDecoder(data)
var channels []slackChannel
if err := decoder.Decode(&channels); err != nil {
mlog.Warn("Slack Import: Error occurred when parsing some Slack channels. Import may work anyway.", mlog.Err(err))
return channels, err
}
for i := range channels {
channels[i].Type = channelType
}
return channels, nil
}
func slackParseUsers(data io.Reader) ([]slackUser, error) {
decoder := json.NewDecoder(data)
var users []slackUser
err := decoder.Decode(&users)
// This actually returns errors that are ignored.
// In this case it is erroring because of a null that Slack
// introduced. So we just return the users here.
return users, err
}
func slackParsePosts(data io.Reader) ([]slackPost, error) {
decoder := json.NewDecoder(data)
var posts []slackPost
if err := decoder.Decode(&posts); err != nil {
mlog.Warn("Slack Import: Error occurred when parsing some Slack posts. Import may work anyway.", mlog.Err(err))
return posts, err
}
return posts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"archive/zip"
"bytes"
"errors"
"image"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type slackChannel struct {
Id string `json:"id"`
Name string `json:"name"`
Creator string `json:"creator"`
Members []string `json:"members"`
Purpose slackChannelSub `json:"purpose"`
Topic slackChannelSub `json:"topic"`
Type model.ChannelType
}
type slackChannelSub struct {
Value string `json:"value"`
}
type slackProfile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
type slackUser struct {
Id string `json:"id"`
Username string `json:"name"`
Profile slackProfile `json:"profile"`
}
type slackFile struct {
Id string `json:"id"`
Title string `json:"title"`
}
type slackPost struct {
User string `json:"user"`
BotId string `json:"bot_id"`
BotUsername string `json:"username"`
Text string `json:"text"`
TimeStamp string `json:"ts"`
ThreadTS string `json:"thread_ts"`
Type string `json:"type"`
SubType string `json:"subtype"`
Comment *slackComment `json:"comment"`
Upload bool `json:"upload"`
File *slackFile `json:"file"`
Files []*slackFile `json:"files"`
Attachments []*model.SlackAttachment `json:"attachments"`
}
var isValidChannelNameCharacters = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString
const slackImportMaxFileSize = 1024 * 1024 * 70
type slackComment struct {
User string `json:"user"`
Comment string `json:"comment"`
}
// Actions provides the actions that needs to be used for import slack data
type Actions struct {
UpdateActive func(*model.User, bool) (*model.User, *model.AppError)
AddUserToChannel func(request.CTX, *model.User, *model.Channel, bool) (*model.ChannelMember, *model.AppError)
JoinUserToTeam func(*model.Team, *model.User, string) (*model.TeamMember, *model.AppError)
CreateDirectChannel func(request.CTX, string, string, ...model.ChannelOption) (*model.Channel, *model.AppError)
CreateGroupChannel func(request.CTX, []string) (*model.Channel, *model.AppError)
CreateChannel func(*model.Channel, bool) (*model.Channel, *model.AppError)
DoUploadFile func(time.Time, string, string, string, string, []byte) (*model.FileInfo, *model.AppError)
GenerateThumbnailImage func(image.Image, string, string)
GeneratePreviewImage func(image.Image, string, string)
InvalidateAllCaches func()
MaxPostSize func() int
PrepareImage func(fileData []byte) (image.Image, string, func(), error)
}
// SlackImporter is a service that allows to import slack dumps into mattermost
type SlackImporter struct {
store store.Store
actions Actions
config *model.Config
}
// New creates a new SlackImporter service instance. It receive a store, a set of actions and the current config.
// It is expected to be used right away and discarded after that
func New(store store.Store, actions Actions, config *model.Config) *SlackImporter {
return &SlackImporter{
store: store,
actions: actions,
config: config,
}
}
func (si *SlackImporter) SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
// Create log file
log := bytes.NewBufferString(i18n.T("api.slackimport.slack_import.log"))
zipreader, err := zip.NewReader(fileData, fileSize)
if err != nil || zipreader.File == nil {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.app_error"))
return model.NewAppError("SlackImport", "api.slackimport.slack_import.zip.app_error", nil, "", http.StatusBadRequest).Wrap(err), log
}
var channels []slackChannel
var publicChannels []slackChannel
var privateChannels []slackChannel
var groupChannels []slackChannel
var directChannels []slackChannel
var users []slackUser
posts := make(map[string][]slackPost)
uploads := make(map[string]*zip.File)
for _, file := range zipreader.File {
fileReader, err := file.Open()
if err != nil {
log.WriteString(i18n.T("api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name}))
return model.NewAppError("SlackImport", "api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name}, "", http.StatusInternalServerError).Wrap(err), log
}
reader := utils.NewLimitedReaderWithError(fileReader, slackImportMaxFileSize)
if file.Name == "channels.json" {
publicChannels, err = slackParseChannels(reader, model.ChannelTypeOpen)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, publicChannels...)
} else if file.Name == "dms.json" {
directChannels, err = slackParseChannels(reader, model.ChannelTypeDirect)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, directChannels...)
} else if file.Name == "groups.json" {
privateChannels, err = slackParseChannels(reader, model.ChannelTypePrivate)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, privateChannels...)
} else if file.Name == "mpims.json" {
groupChannels, err = slackParseChannels(reader, model.ChannelTypeGroup)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, groupChannels...)
} else if file.Name == "users.json" {
users, err = slackParseUsers(reader)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
} else {
spl := strings.Split(file.Name, "/")
if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") {
newposts, err := slackParsePosts(reader)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channel := spl[0]
if _, ok := posts[channel]; !ok {
posts[channel] = newposts
} else {
posts[channel] = append(posts[channel], newposts...)
}
} else if len(spl) == 3 && spl[0] == "__uploads" {
uploads[spl[1]] = file
}
}
}
posts = slackConvertUserMentions(users, posts)
posts = slackConvertChannelMentions(channels, posts)
posts = slackConvertPostsMarkup(posts)
addedUsers := si.slackAddUsers(teamID, users, log)
botUser := si.slackAddBotUser(teamID, log)
si.slackAddChannels(c, teamID, channels, posts, addedUsers, uploads, botUser, log)
if botUser != nil {
si.deactivateSlackBotUser(botUser)
}
si.actions.InvalidateAllCaches()
log.WriteString(i18n.T("api.slackimport.slack_import.notes"))
log.WriteString("=======\r\n\r\n")
log.WriteString(i18n.T("api.slackimport.slack_import.note1"))
log.WriteString(i18n.T("api.slackimport.slack_import.note2"))
log.WriteString(i18n.T("api.slackimport.slack_import.note3"))
return nil, log
}
func truncateRunes(s string, i int) string {
runes := []rune(s)
if len(runes) > i {
return string(runes[:i])
}
return s
}
func (si *SlackImporter) slackAddUsers(teamId string, slackusers []slackUser, importerLog *bytes.Buffer) map[string]*model.User {
// Log header
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.created"))
importerLog.WriteString("===============\r\n\r\n")
addedUsers := make(map[string]*model.User)
// Need the team
team, err := si.store.Team().Get(teamId)
if err != nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_import.team_fail"))
return addedUsers
}
for _, sUser := range slackusers {
firstName := sUser.Profile.FirstName
lastName := sUser.Profile.LastName
email := sUser.Profile.Email
if email == "" {
email = sUser.Username + "@example.com"
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.missing_email_address", map[string]any{"Email": email, "Username": sUser.Username}))
mlog.Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username))
}
password := model.NewId()
// Check for email conflict and use existing user if found
if existingUser, err := si.store.User().GetByEmail(email); err == nil {
addedUsers[sUser.Id] = existingUser
if _, err := si.actions.JoinUserToTeam(team, addedUsers[sUser.Id], ""); err != nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing_failed", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username}))
} else {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username}))
}
continue
}
email = strings.ToLower(email)
newUser := model.User{
Username: sUser.Username,
FirstName: firstName,
LastName: lastName,
Email: email,
Password: password,
}
mUser := si.oldImportUser(team, &newUser)
if mUser == nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.unable_import", map[string]any{"Username": sUser.Username}))
continue
}
addedUsers[sUser.Id] = mUser
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email_pwd", map[string]any{"Email": newUser.Email, "Password": password}))
}
return addedUsers
}
func (si *SlackImporter) slackAddBotUser(teamId string, log *bytes.Buffer) *model.User {
team, err := si.store.Team().Get(teamId)
if err != nil {
log.WriteString(i18n.T("api.slackimport.slack_import.team_fail"))
return nil
}
password := model.NewId()
username := "slackimportuser_" + model.NewId()
email := username + "@localhost"
botUser := model.User{
Username: username,
FirstName: "",
LastName: "",
Email: email,
Password: password,
}
mUser := si.oldImportUser(team, &botUser)
if mUser == nil {
log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.unable_import", map[string]any{"Username": username}))
return nil
}
log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]any{"Email": botUser.Email, "Password": password}))
return mUser
}
func (si *SlackImporter) slackAddPosts(teamId string, channel *model.Channel, posts []slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User) {
sort.Slice(posts, func(i, j int) bool {
return slackConvertTimeStamp(posts[i].TimeStamp) < slackConvertTimeStamp(posts[j].TimeStamp)
})
threads := make(map[string]string)
for _, sPost := range posts {
switch {
case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"):
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
if sPost.Upload {
if sPost.File != nil {
if fileInfo, ok := si.slackUploadFile(sPost.File, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok {
newPost.FileIds = append(newPost.FileIds, fileInfo.Id)
}
} else if sPost.Files != nil {
for _, file := range sPost.Files {
if fileInfo, ok := si.slackUploadFile(file, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok {
newPost.FileIds = append(newPost.FileIds, fileInfo.Id)
}
}
}
}
// If post in thread
if sPost.ThreadTS != "" && sPost.ThreadTS != sPost.TimeStamp {
newPost.RootId = threads[sPost.ThreadTS]
}
postId := si.oldImportPost(&newPost)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && sPost.SubType == "file_comment":
if sPost.Comment == nil {
mlog.Debug("Slack Import: Unable to import the message as it has no comments.")
continue
}
if sPost.Comment.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.Comment.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.Comment.User].Id,
ChannelId: channel.Id,
Message: sPost.Comment.Comment,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "bot_message":
if botUser == nil {
mlog.Warn("Slack Import: Unable to import the bot message as the bot user does not exist.")
continue
}
if sPost.BotId == "" {
mlog.Warn("Slack Import: Unable to import bot message as the BotId field is missing.")
continue
}
props := make(model.StringInterface)
props["override_username"] = sPost.BotUsername
if len(sPost.Attachments) > 0 {
props["attachments"] = sPost.Attachments
}
post := &model.Post{
UserId: botUser.Id,
ChannelId: channel.Id,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Message: sPost.Text,
Type: model.PostTypeSlackAttachment,
}
postId := si.oldImportIncomingWebhookPost(post, props)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && (sPost.SubType == "channel_join" || sPost.SubType == "channel_leave"):
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
var postType string
if sPost.SubType == "channel_join" {
postType = model.PostTypeJoinChannel
} else {
postType = model.PostTypeLeaveChannel
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: postType,
Props: model.StringInterface{
"username": users[sPost.User].Username,
},
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "me_message":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: "*" + sPost.Text + "*",
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
postId := si.oldImportPost(&newPost)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && sPost.SubType == "channel_topic":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypeHeaderChange,
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "channel_purpose":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypePurposeChange,
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "channel_name":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypeDisplaynameChange,
}
si.oldImportPost(&newPost)
default:
mlog.Warn(
"Slack Import: Unable to import the message as its type is not supported",
mlog.String("post_type", sPost.Type),
mlog.String("post_subtype", sPost.SubType),
)
}
}
}
func (si *SlackImporter) slackUploadFile(slackPostFile *slackFile, uploads map[string]*zip.File, teamId string, channelId string, userId string, slackTimestamp string) (*model.FileInfo, bool) {
if slackPostFile == nil {
mlog.Warn("Slack Import: Unable to attach the file to the post as the latter has no file section present in Slack export.")
return nil, false
}
file, ok := uploads[slackPostFile.Id]
if !ok {
mlog.Warn("Slack Import: Unable to import file as the file is missing from the Slack export zip file.", mlog.String("file_id", slackPostFile.Id))
return nil, false
}
openFile, err := file.Open()
if err != nil {
mlog.Warn("Slack Import: Unable to open the file from the Slack export.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err))
return nil, false
}
defer openFile.Close()
timestamp := utils.TimeFromMillis(slackConvertTimeStamp(slackTimestamp))
uploadedFile, err := si.oldImportFile(timestamp, openFile, teamId, channelId, userId, filepath.Base(file.Name))
if err != nil {
mlog.Warn("Slack Import: An error occurred when uploading file.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err))
return nil, false
}
return uploadedFile, true
}
func (si *SlackImporter) deactivateSlackBotUser(user *model.User) {
if _, err := si.actions.UpdateActive(user, false); err != nil {
mlog.Warn("Slack Import: Unable to deactivate the user account used for the bot.")
}
}
func (si *SlackImporter) addSlackUsersToChannel(c request.CTX, members []string, users map[string]*model.User, channel *model.Channel, log *bytes.Buffer) {
for _, member := range members {
user, ok := users[member]
if !ok {
log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": "?"}))
continue
}
if _, err := si.actions.AddUserToChannel(c, user, channel, false); err != nil {
log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": user.Username}))
}
}
}
func slackSanitiseChannelProperties(channel model.Channel) model.Channel {
if utf8.RuneCountInString(channel.DisplayName) > model.ChannelDisplayNameMaxRunes {
mlog.Warn("Slack Import: Channel display name exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.DisplayName = truncateRunes(channel.DisplayName, model.ChannelDisplayNameMaxRunes)
}
if len(channel.Name) > model.ChannelNameMaxLength {
mlog.Warn("Slack Import: Channel handle exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Name = channel.Name[0:model.ChannelNameMaxLength]
}
if utf8.RuneCountInString(channel.Purpose) > model.ChannelPurposeMaxRunes {
mlog.Warn("Slack Import: Channel purpose exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Purpose = truncateRunes(channel.Purpose, model.ChannelPurposeMaxRunes)
}
if utf8.RuneCountInString(channel.Header) > model.ChannelHeaderMaxRunes {
mlog.Warn("Slack Import: Channel header exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Header = truncateRunes(channel.Header, model.ChannelHeaderMaxRunes)
}
return channel
}
func (si *SlackImporter) slackAddChannels(c request.CTX, teamId string, slackchannels []slackChannel, posts map[string][]slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User, importerLog *bytes.Buffer) map[string]*model.Channel {
// Write Header
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.added"))
importerLog.WriteString("=================\r\n\r\n")
addedChannels := make(map[string]*model.Channel)
for _, sChannel := range slackchannels {
newChannel := model.Channel{
TeamId: teamId,
Type: sChannel.Type,
DisplayName: sChannel.Name,
Name: slackConvertChannelName(sChannel.Name, sChannel.Id),
Purpose: sChannel.Purpose.Value,
Header: sChannel.Topic.Value,
}
// Direct message channels in Slack don't have a name so we set the id as name or else the messages won't get imported.
if newChannel.Type == model.ChannelTypeDirect {
sChannel.Name = sChannel.Id
}
newChannel = slackSanitiseChannelProperties(newChannel)
var mChannel *model.Channel
var err error
if mChannel, err = si.store.Channel().GetByName(teamId, sChannel.Name, true); err == nil {
// The channel already exists as an active channel. Merge with the existing one.
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.merge", map[string]any{"DisplayName": newChannel.DisplayName}))
} else if _, nErr := si.store.Channel().GetDeletedByName(teamId, sChannel.Name); nErr == nil {
// The channel already exists but has been deleted. Generate a random string for the handle instead.
newChannel.Name = model.NewId()
newChannel = slackSanitiseChannelProperties(newChannel)
}
if mChannel == nil {
// Haven't found an existing channel to merge with. Try importing it as a new one.
mChannel = si.oldImportChannel(c, &newChannel, sChannel, users)
if mChannel == nil {
mlog.Warn("Slack Import: Unable to import Slack channel.", mlog.String("channel_display_name", newChannel.DisplayName))
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.import_failed", map[string]any{"DisplayName": newChannel.DisplayName}))
continue
}
}
// Members for direct and group channels are added during the creation of the channel in the oldImportChannel function
if sChannel.Type == model.ChannelTypeOpen || sChannel.Type == model.ChannelTypePrivate {
si.addSlackUsersToChannel(c, sChannel.Members, users, mChannel, importerLog)
}
importerLog.WriteString(newChannel.DisplayName + "\r\n")
addedChannels[sChannel.Id] = mChannel
si.slackAddPosts(teamId, mChannel, posts[sChannel.Name], users, uploads, botUser)
}
return addedChannels
}
//
// -- Old SlackImport Functions --
// Import functions are suitable for entering posts and users into the database without
// some of the usual checks. (IsValid is still run)
//
func (si *SlackImporter) oldImportPost(post *model.Post) string {
// Workaround for empty messages, which may be the case if they are webhook posts.
firstIteration := true
firstPostId := ""
if post.RootId != "" {
firstPostId = post.RootId
}
maxPostSize := si.actions.MaxPostSize()
for messageRuneCount := utf8.RuneCountInString(post.Message); messageRuneCount > 0 || firstIteration; messageRuneCount = utf8.RuneCountInString(post.Message) {
var remainder string
if messageRuneCount > maxPostSize {
remainder = string(([]rune(post.Message))[maxPostSize:])
post.Message = truncateRunes(post.Message, maxPostSize)
} else {
remainder = ""
}
post.Hashtags, _ = model.ParseHashtags(post.Message)
post.RootId = firstPostId
_, err := si.store.Post().Save(post)
if err != nil {
mlog.Debug("Error saving post.", mlog.String("user_id", post.UserId), mlog.String("message", post.Message))
}
if firstIteration {
if firstPostId == "" {
firstPostId = post.Id
}
for _, fileId := range post.FileIds {
if err := si.store.FileInfo().AttachToPost(fileId, post.Id, post.ChannelId, post.UserId); err != nil {
mlog.Error(
"Error attaching files to post.",
mlog.String("post_id", post.Id),
mlog.String("file_ids", strings.Join(post.FileIds, ",")),
mlog.String("user_id", post.UserId),
mlog.Err(err),
)
}
}
post.FileIds = nil
}
post.Id = ""
post.CreateAt++
post.Message = remainder
firstIteration = false
}
return firstPostId
}
func (si *SlackImporter) oldImportUser(team *model.Team, user *model.User) *model.User {
user.MakeNonNil()
user.Roles = model.SystemUserRoleId
ruser, nErr := si.store.User().Save(user)
if nErr != nil {
mlog.Debug("Error saving user.", mlog.Err(nErr))
return nil
}
if _, err := si.store.User().VerifyEmail(ruser.Id, ruser.Email); err != nil {
mlog.Warn("Failed to set email verified.", mlog.Err(err))
}
if _, err := si.actions.JoinUserToTeam(team, user, ""); err != nil {
mlog.Warn("Failed to join team when importing.", mlog.Err(err))
}
return ruser
}
func (si *SlackImporter) oldImportChannel(c request.CTX, channel *model.Channel, sChannel slackChannel, users map[string]*model.User) *model.Channel {
switch {
case channel.Type == model.ChannelTypeDirect:
if len(sChannel.Members) < 2 {
return nil
}
u1 := users[sChannel.Members[0]]
u2 := users[sChannel.Members[1]]
if u1 == nil || u2 == nil {
mlog.Warn("Either or both of user ids not found in users.json. Ignoring.", mlog.String("id1", sChannel.Members[0]), mlog.String("id2", sChannel.Members[1]))
return nil
}
sc, err := si.actions.CreateDirectChannel(c, u1.Id, u2.Id)
if err != nil {
return nil
}
return sc
// check if direct channel has less than 8 members and if not import as private channel instead
case channel.Type == model.ChannelTypeGroup && len(sChannel.Members) < 8:
members := make([]string, len(sChannel.Members))
for i := range sChannel.Members {
u := users[sChannel.Members[i]]
if u == nil {
mlog.Warn("User not found in users.json. Ignoring.", mlog.String("id", sChannel.Members[i]))
continue
}
members[i] = u.Id
}
creator := users[sChannel.Creator]
if creator == nil {
return nil
}
sc, err := si.actions.CreateGroupChannel(c, members)
if err != nil {
return nil
}
return sc
case channel.Type == model.ChannelTypeGroup:
channel.Type = model.ChannelTypePrivate
sc, err := si.actions.CreateChannel(channel, false)
if err != nil {
return nil
}
return sc
}
sc, err := si.store.Channel().Save(channel, *si.config.TeamSettings.MaxChannelsPerTeam)
if err != nil {
return nil
}
return sc
}
func (si *SlackImporter) oldImportFile(timestamp time.Time, file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) {
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
fileInfo, err := si.actions.DoUploadFile(timestamp, teamId, channelId, userId, fileName, data)
if err != nil {
return nil, err
}
if fileInfo.IsImage() && !fileInfo.IsSvg() {
img, imgType, release, err := si.actions.PrepareImage(data)
if err != nil {
return nil, err
}
defer release()
si.actions.GenerateThumbnailImage(img, imgType, fileInfo.ThumbnailPath)
si.actions.GeneratePreviewImage(img, imgType, fileInfo.PreviewPath)
}
return fileInfo, nil
}
func (si *SlackImporter) oldImportIncomingWebhookPost(post *model.Post, props model.StringInterface) string {
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})")
post.AddProp("from_webhook", "true")
if _, ok := props["override_username"]; !ok {
post.AddProp("override_username", model.DefaultWebhookUsername)
}
if len(props) > 0 {
for key, val := range props {
if key == "attachments" {
if attachments, success := val.([]*model.SlackAttachment); success {
model.ParseSlackAttachment(post, attachments)
}
} else if key != "from_webhook" {
post.AddProp(key, val)
}
}
}
return si.oldImportPost(post)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
rudder "github.com/rudderlabs/analytics-go"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/marketplace"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
DBAccessAttempts = 3
DBAccessTimeoutSecs = 10
RudderKey = "placeholder_rudder_key"
RudderDataplaneURL = "placeholder_rudder_dataplane_url"
EnvVarInstallType = "MM_INSTALL_TYPE"
TrackConfigService = "config_service"
TrackConfigTeam = "config_team"
TrackConfigClientReq = "config_client_requirements"
TrackConfigSQL = "config_sql"
TrackConfigLog = "config_log"
TrackConfigAudit = "config_audit"
TrackConfigNotificationLog = "config_notifications_log"
TrackConfigFile = "config_file"
TrackConfigRate = "config_rate"
TrackConfigEmail = "config_email"
TrackConfigPrivacy = "config_privacy"
TrackConfigTheme = "config_theme"
TrackConfigOAuth = "config_oauth"
TrackConfigLDAP = "config_ldap"
TrackConfigCompliance = "config_compliance"
TrackConfigLocalization = "config_localization"
TrackConfigSAML = "config_saml"
TrackConfigPassword = "config_password"
TrackConfigCluster = "config_cluster"
TrackConfigMetrics = "config_metrics"
TrackConfigSupport = "config_support"
TrackConfigNativeApp = "config_nativeapp"
TrackConfigExperimental = "config_experimental"
TrackConfigAnalytics = "config_analytics"
TrackConfigAnnouncement = "config_announcement"
TrackConfigElasticsearch = "config_elasticsearch"
TrackConfigPlugin = "config_plugin"
TrackConfigDataRetention = "config_data_retention"
TrackConfigMessageExport = "config_message_export"
TrackConfigDisplay = "config_display"
TrackConfigGuestAccounts = "config_guest_accounts"
TrackConfigImageProxy = "config_image_proxy"
TrackConfigBleve = "config_bleve"
TrackConfigExport = "config_export"
TrackFeatureFlags = "config_feature_flags"
TrackConfigProducts = "products"
TrackPermissionsGeneral = "permissions_general"
TrackPermissionsSystemScheme = "permissions_system_scheme"
TrackPermissionsTeamSchemes = "permissions_team_schemes"
TrackPermissionsSystemRoles = "permissions_system_roles"
TrackElasticsearch = "elasticsearch"
TrackGroups = "groups"
TrackChannelModeration = "channel_moderation"
TrackWarnMetrics = "warn_metrics"
TrackActivity = "activity"
TrackLicense = "license"
TrackServer = "server"
TrackPlugins = "plugins"
)
type ServerIface interface {
Config() *model.Config
IsLeader() bool
HTTPService() httpservice.HTTPService
GetPluginsEnvironment() *plugin.Environment
License() *model.License
GetRoleByName(context.Context, string) (*model.Role, *model.AppError)
GetSchemes(string, int, int) ([]*model.Scheme, *model.AppError)
HooksManager() *product.HooksManager
}
type TelemetryService struct {
srv ServerIface
dbStore store.Store
searchEngine *searchengine.Broker
log *mlog.Logger
rudderClient rudder.Client
TelemetryID string
timestampLastTelemetrySent time.Time
verbose bool
}
type RudderConfig struct {
RudderKey string
DataplaneURL string
}
func New(srv ServerIface, dbStore store.Store, searchEngine *searchengine.Broker, log *mlog.Logger, verbose bool) (*TelemetryService, error) {
service := &TelemetryService{
srv: srv,
dbStore: dbStore,
searchEngine: searchEngine,
log: log,
verbose: verbose,
}
if err := service.ensureTelemetryID(); err != nil {
return nil, fmt.Errorf("unable to ensure telemetry ID: %w", err)
}
return service, nil
}
func (ts *TelemetryService) ensureTelemetryID() error {
if ts.TelemetryID != "" {
return nil
}
id := model.NewId()
var err error
for i := 0; i < DBAccessAttempts; i++ {
ts.log.Info("Ensuring the telemetry ID", mlog.String("id", id))
systemID := &model.System{Name: model.SystemTelemetryId, Value: id}
systemID, err = ts.dbStore.System().InsertIfExists(systemID)
if err != nil {
ts.log.Info("Unable to get/set the telemetry ID", mlog.Err(err))
time.Sleep(DBAccessTimeoutSecs * time.Second)
continue
}
ts.TelemetryID = systemID.Value
return nil
}
return fmt.Errorf("unable to get the telemetry ID: %w", err)
}
func (ts *TelemetryService) getRudderConfig() RudderConfig {
if !strings.Contains(RudderKey, "placeholder") && !strings.Contains(RudderDataplaneURL, "placeholder") {
return RudderConfig{RudderKey, RudderDataplaneURL}
} else if os.Getenv("RudderKey") != "" && os.Getenv("RudderDataplaneURL") != "" {
return RudderConfig{os.Getenv("RudderKey"), os.Getenv("RudderDataplaneURL")}
} else {
return RudderConfig{}
}
}
func (ts *TelemetryService) telemetryEnabled() bool {
return *ts.srv.Config().LogSettings.EnableDiagnostics && ts.srv.IsLeader()
}
func (ts *TelemetryService) sendDailyTelemetry(override bool) {
config := ts.getRudderConfig()
if ts.telemetryEnabled() && ((config.DataplaneURL != "" && config.RudderKey != "") || override) {
ts.initRudder(config.DataplaneURL, config.RudderKey)
ts.trackActivity()
ts.trackConfig()
ts.trackLicense()
ts.trackPlugins()
ts.trackServer()
ts.trackPermissions()
ts.trackElasticsearch()
ts.trackGroups()
ts.trackChannelModeration()
ts.trackWarnMetrics()
ts.trackProducts()
}
}
func (ts *TelemetryService) SendTelemetry(event string, properties map[string]any) {
if ts.rudderClient != nil {
var context *rudder.Context
// if we are part of a cloud installation, add it's ID to the tracked event's context
if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" {
context = &rudder.Context{Traits: map[string]any{"installationId": installationId}}
}
err := ts.rudderClient.Enqueue(rudder.Track{
Event: event,
UserId: ts.TelemetryID,
Properties: properties,
Context: context,
})
if err != nil {
ts.log.Warn("Error sending telemetry", mlog.Err(err))
}
}
}
func isDefaultArray(setting, defaultValue []string) bool {
if len(setting) != len(defaultValue) {
return false
}
for i := 0; i < len(setting); i++ {
if setting[i] != defaultValue[i] {
return false
}
}
return true
}
func isDefault(setting any, defaultValue any) bool {
return setting == defaultValue
}
func pluginSetting(pluginSettings *model.PluginSettings, plugin, key string, defaultValue any) any {
settings, ok := pluginSettings.Plugins[plugin]
if !ok {
return defaultValue
}
if value, ok := settings[key]; ok {
return value
}
return defaultValue
}
func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool {
state, ok := pluginStates[pluginId]
if !ok {
return false
}
return state.Enable
}
func pluginVersion(pluginsAvailable []*model.BundleInfo, pluginId string) string {
for _, plugin := range pluginsAvailable {
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
return plugin.Manifest.Version
}
}
return ""
}
func (ts *TelemetryService) trackActivity() {
var userCount int64
var guestAccountsCount int64
var botAccountsCount int64
var inactiveUserCount int64
var publicChannelCount int64
var privateChannelCount int64
var directChannelCount int64
var deletedPublicChannelCount int64
var deletedPrivateChannelCount int64
var postsCount int64
var postsCountPreviousDay int64
var botPostsCountPreviousDay int64
var slashCommandsCount int64
var incomingWebhooksCount int64
var outgoingWebhooksCount int64
activeUsersDailyCountChan := make(chan store.StoreResult, 1)
go func() {
count, err := ts.dbStore.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
activeUsersDailyCountChan <- store.StoreResult{Data: count, NErr: err}
close(activeUsersDailyCountChan)
}()
activeUsersMonthlyCountChan := make(chan store.StoreResult, 1)
go func() {
count, err := ts.dbStore.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
activeUsersMonthlyCountChan <- store.StoreResult{Data: count, NErr: err}
close(activeUsersMonthlyCountChan)
}()
if count, err := ts.dbStore.User().Count(model.UserCountOptions{IncludeDeleted: true}); err == nil {
userCount = count
}
if count, err := ts.dbStore.User().AnalyticsGetGuestCount(); err == nil {
guestAccountsCount = count
}
if count, err := ts.dbStore.User().Count(model.UserCountOptions{IncludeBotAccounts: true, ExcludeRegularUsers: true}); err == nil {
botAccountsCount = count
}
if iucr, err := ts.dbStore.User().AnalyticsGetInactiveUsersCount(); err == nil {
inactiveUserCount = iucr
}
teamCount, err := ts.dbStore.Team().AnalyticsTeamCount(nil)
if err != nil {
mlog.Info("Could not get team count", mlog.Err(err))
}
if ucc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypeOpen); err == nil {
publicChannelCount = ucc
}
if pcc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypePrivate); err == nil {
privateChannelCount = pcc
}
if dcc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypeDirect); err == nil {
directChannelCount = dcc
}
if duccr, err := ts.dbStore.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen); err == nil {
deletedPublicChannelCount = duccr
}
if dpccr, err := ts.dbStore.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate); err == nil {
deletedPrivateChannelCount = dpccr
}
postsCount, _ = ts.dbStore.Post().AnalyticsPostCount(&model.PostCountOptions{})
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: false, YesterdayOnly: true}
postCountsYesterday, _ := ts.dbStore.Post().AnalyticsPostCountsByDay(postCountsOptions)
postsCountPreviousDay = 0
if len(postCountsYesterday) > 0 {
postsCountPreviousDay = int64(postCountsYesterday[0].Value)
}
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: true, YesterdayOnly: true}
botPostCountsYesterday, _ := ts.dbStore.Post().AnalyticsPostCountsByDay(postCountsOptions)
botPostsCountPreviousDay = 0
if len(botPostCountsYesterday) > 0 {
botPostsCountPreviousDay = int64(botPostCountsYesterday[0].Value)
}
slashCommandsCount, _ = ts.dbStore.Command().AnalyticsCommandCount("")
if c, err := ts.dbStore.Webhook().AnalyticsIncomingCount(""); err == nil {
incomingWebhooksCount = c
}
outgoingWebhooksCount, _ = ts.dbStore.Webhook().AnalyticsOutgoingCount("")
var activeUsersDailyCount int64
if r := <-activeUsersDailyCountChan; r.NErr == nil {
activeUsersDailyCount = r.Data.(int64)
}
var activeUsersMonthlyCount int64
if r := <-activeUsersMonthlyCountChan; r.NErr == nil {
activeUsersMonthlyCount = r.Data.(int64)
}
activity := map[string]any{
"registered_users": userCount,
"bot_accounts": botAccountsCount,
"guest_accounts": guestAccountsCount,
"active_users_daily": activeUsersDailyCount,
"active_users_monthly": activeUsersMonthlyCount,
"registered_deactivated_users": inactiveUserCount,
"teams": teamCount,
"public_channels": publicChannelCount,
"private_channels": privateChannelCount,
"direct_message_channels": directChannelCount,
"public_channels_deleted": deletedPublicChannelCount,
"private_channels_deleted": deletedPrivateChannelCount,
"posts_previous_day": postsCountPreviousDay,
"bot_posts_previous_day": botPostsCountPreviousDay,
"posts": postsCount,
"slash_commands": slashCommandsCount,
"incoming_webhooks": incomingWebhooksCount,
"outgoing_webhooks": outgoingWebhooksCount,
}
if license := ts.srv.License(); license.IsCloud() {
var tmpStorage int64
if usage, err := ts.dbStore.FileInfo().GetStorageUsage(true, false); err == nil {
tmpStorage = usage
}
activity["storage_bytes"] = utils.RoundOffToZeroesResolution(float64(tmpStorage), 8)
}
ts.SendTelemetry(TrackActivity, activity)
}
func (ts *TelemetryService) trackConfig() {
cfg := ts.srv.Config()
ts.SendTelemetry(TrackConfigService, map[string]any{
"web_server_mode": *cfg.ServiceSettings.WebserverMode,
"enable_security_fix_alert": *cfg.ServiceSettings.EnableSecurityFixAlert,
"enable_insecure_outgoing_connections": *cfg.ServiceSettings.EnableInsecureOutgoingConnections,
"enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks,
"enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks,
"enable_commands": *cfg.ServiceSettings.EnableCommands,
"enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride,
"enable_post_icon_override": cfg.ServiceSettings.EnablePostIconOverride,
"enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens,
"enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji,
"enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker,
"enable_gif_picker": *cfg.ServiceSettings.EnableGifPicker,
"gfycat_api_key": isDefault(*cfg.ServiceSettings.GfycatAPIKey, model.ServiceSettingsDefaultGfycatAPIKey),
"gfycat_api_secret": isDefault(*cfg.ServiceSettings.GfycatAPISecret, model.ServiceSettingsDefaultGfycatAPISecret),
"experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer,
"enable_testing": cfg.ServiceSettings.EnableTesting,
"enable_developer": *cfg.ServiceSettings.EnableDeveloper,
"developer_flags": isDefault(*cfg.ServiceSettings.DeveloperFlags, model.ServiceSettingsDefaultDeveloperFlags),
"enable_client_performance_debugging": *cfg.ServiceSettings.EnableClientPerformanceDebugging,
"enable_multifactor_authentication": *cfg.ServiceSettings.EnableMultifactorAuthentication,
"enforce_multifactor_authentication": *cfg.ServiceSettings.EnforceMultifactorAuthentication,
"enable_oauth_service_provider": cfg.ServiceSettings.EnableOAuthServiceProvider,
"connection_security": *cfg.ServiceSettings.ConnectionSecurity,
"tls_strict_transport": *cfg.ServiceSettings.TLSStrictTransport,
"uses_letsencrypt": *cfg.ServiceSettings.UseLetsEncrypt,
"forward_80_to_443": *cfg.ServiceSettings.Forward80To443,
"maximum_login_attempts": *cfg.ServiceSettings.MaximumLoginAttempts,
"extend_session_length_with_activity": *cfg.ServiceSettings.ExtendSessionLengthWithActivity,
"session_length_web_in_hours": *cfg.ServiceSettings.SessionLengthWebInHours,
"session_length_mobile_in_hours": *cfg.ServiceSettings.SessionLengthMobileInHours,
"session_length_sso_in_hours": *cfg.ServiceSettings.SessionLengthSSOInHours,
"session_cache_in_minutes": *cfg.ServiceSettings.SessionCacheInMinutes,
"session_idle_timeout_in_minutes": *cfg.ServiceSettings.SessionIdleTimeoutInMinutes,
"isdefault_site_url": isDefault(*cfg.ServiceSettings.SiteURL, model.ServiceSettingsDefaultSiteURL),
"isdefault_tls_cert_file": isDefault(*cfg.ServiceSettings.TLSCertFile, model.ServiceSettingsDefaultTLSCertFile),
"isdefault_tls_key_file": isDefault(*cfg.ServiceSettings.TLSKeyFile, model.ServiceSettingsDefaultTLSKeyFile),
"isdefault_read_timeout": isDefault(*cfg.ServiceSettings.ReadTimeout, model.ServiceSettingsDefaultReadTimeout),
"isdefault_write_timeout": isDefault(*cfg.ServiceSettings.WriteTimeout, model.ServiceSettingsDefaultWriteTimeout),
"isdefault_idle_timeout": isDefault(*cfg.ServiceSettings.IdleTimeout, model.ServiceSettingsDefaultIdleTimeout),
"isdefault_google_developer_key": isDefault(cfg.ServiceSettings.GoogleDeveloperKey, ""),
"isdefault_allow_cors_from": isDefault(*cfg.ServiceSettings.AllowCorsFrom, model.ServiceSettingsDefaultAllowCorsFrom),
"isdefault_cors_exposed_headers": isDefault(cfg.ServiceSettings.CorsExposedHeaders, ""),
"cors_allow_credentials": *cfg.ServiceSettings.CorsAllowCredentials,
"cors_debug": *cfg.ServiceSettings.CorsDebug,
"isdefault_allowed_untrusted_internal_connections": isDefault(*cfg.ServiceSettings.AllowedUntrustedInternalConnections, ""),
"post_edit_time_limit": *cfg.ServiceSettings.PostEditTimeLimit,
"enable_user_typing_messages": *cfg.ServiceSettings.EnableUserTypingMessages,
"enable_channel_viewed_messages": *cfg.ServiceSettings.EnableChannelViewedMessages,
"time_between_user_typing_updates_milliseconds": *cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds,
"cluster_log_timeout_milliseconds": *cfg.ServiceSettings.ClusterLogTimeoutMilliseconds,
"enable_post_search": *cfg.ServiceSettings.EnablePostSearch,
"minimum_hashtag_length": *cfg.ServiceSettings.MinimumHashtagLength,
"enable_user_statuses": *cfg.ServiceSettings.EnableUserStatuses,
"enable_preview_features": *cfg.ServiceSettings.EnablePreviewFeatures,
"enable_tutorial": *cfg.ServiceSettings.EnableTutorial,
"enable_onboarding_flow": *cfg.ServiceSettings.EnableOnboardingFlow,
"experimental_enable_default_channel_leave_join_messages": *cfg.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages,
"experimental_group_unread_channels": *cfg.ServiceSettings.ExperimentalGroupUnreadChannels,
"collapsed_threads": *cfg.ServiceSettings.CollapsedThreads,
"websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
"allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
"enable_api_team_deletion": *cfg.ServiceSettings.EnableAPITeamDeletion,
"enable_api_trigger_admin_notification": *cfg.ServiceSettings.EnableAPITriggerAdminNotifications,
"enable_api_user_deletion": *cfg.ServiceSettings.EnableAPIUserDeletion,
"enable_api_channel_deletion": *cfg.ServiceSettings.EnableAPIChannelDeletion,
"experimental_enable_hardened_mode": *cfg.ServiceSettings.ExperimentalEnableHardenedMode,
"experimental_strict_csrf_enforcement": *cfg.ServiceSettings.ExperimentalStrictCSRFEnforcement,
"enable_email_invitations": *cfg.ServiceSettings.EnableEmailInvitations,
"disable_bots_when_owner_is_deactivated": *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated,
"enable_bot_account_creation": *cfg.ServiceSettings.EnableBotAccountCreation,
"enable_svgs": *cfg.ServiceSettings.EnableSVGs,
"enable_latex": *cfg.ServiceSettings.EnableLatex,
"enable_inline_latex": *cfg.ServiceSettings.EnableInlineLatex,
"enable_opentracing": *cfg.ServiceSettings.EnableOpenTracing,
"enable_local_mode": *cfg.ServiceSettings.EnableLocalMode,
"managed_resource_paths": isDefault(*cfg.ServiceSettings.ManagedResourcePaths, ""),
"thread_auto_follow": *cfg.ServiceSettings.ThreadAutoFollow,
"enable_link_previews": *cfg.ServiceSettings.EnableLinkPreviews,
"enable_permalink_previews": *cfg.ServiceSettings.EnablePermalinkPreviews,
"enable_file_search": *cfg.ServiceSettings.EnableFileSearch,
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
"post_priority": *cfg.ServiceSettings.PostPriority,
"self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase,
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
"self_hosted_expansion": *cfg.ServiceSettings.SelfHostedExpansion,
})
ts.SendTelemetry(TrackConfigTeam, map[string]any{
"enable_user_creation": cfg.TeamSettings.EnableUserCreation,
"enable_open_server": *cfg.TeamSettings.EnableOpenServer,
"enable_user_deactivation": *cfg.TeamSettings.EnableUserDeactivation,
"enable_custom_user_statuses": *cfg.TeamSettings.EnableCustomUserStatuses,
"enable_last_active_time": *cfg.TeamSettings.EnableLastActiveTime,
"enable_custom_brand": *cfg.TeamSettings.EnableCustomBrand,
"restrict_direct_message": *cfg.TeamSettings.RestrictDirectMessage,
"max_notifications_per_channel": *cfg.TeamSettings.MaxNotificationsPerChannel,
"enable_confirm_notifications_to_channel": *cfg.TeamSettings.EnableConfirmNotificationsToChannel,
"max_users_per_team": *cfg.TeamSettings.MaxUsersPerTeam,
"max_channels_per_team": *cfg.TeamSettings.MaxChannelsPerTeam,
"teammate_name_display": *cfg.TeamSettings.TeammateNameDisplay,
"experimental_view_archived_channels": *cfg.TeamSettings.ExperimentalViewArchivedChannels,
"lock_teammate_name_display": *cfg.TeamSettings.LockTeammateNameDisplay,
"isdefault_site_name": isDefault(cfg.TeamSettings.SiteName, "Mattermost"),
"isdefault_custom_brand_text": isDefault(*cfg.TeamSettings.CustomBrandText, model.TeamSettingsDefaultCustomBrandText),
"isdefault_custom_description_text": isDefault(*cfg.TeamSettings.CustomDescriptionText, model.TeamSettingsDefaultCustomDescriptionText),
"isdefault_user_status_away_timeout": isDefault(*cfg.TeamSettings.UserStatusAwayTimeout, model.TeamSettingsDefaultUserStatusAwayTimeout),
"experimental_enable_automatic_replies": *cfg.TeamSettings.ExperimentalEnableAutomaticReplies,
"experimental_primary_team": isDefault(*cfg.TeamSettings.ExperimentalPrimaryTeam, ""),
"experimental_default_channels": len(cfg.TeamSettings.ExperimentalDefaultChannels),
})
ts.SendTelemetry(TrackConfigClientReq, map[string]any{
"android_latest_version": cfg.ClientRequirements.AndroidLatestVersion,
"android_min_version": cfg.ClientRequirements.AndroidMinVersion,
"ios_latest_version": cfg.ClientRequirements.IosLatestVersion,
"ios_min_version": cfg.ClientRequirements.IosMinVersion,
})
ts.SendTelemetry(TrackConfigSQL, map[string]any{
"driver_name": *cfg.SqlSettings.DriverName,
"trace": cfg.SqlSettings.Trace,
"max_idle_conns": *cfg.SqlSettings.MaxIdleConns,
"conn_max_lifetime_milliseconds": *cfg.SqlSettings.ConnMaxLifetimeMilliseconds,
"conn_max_idletime_milliseconds": *cfg.SqlSettings.ConnMaxIdleTimeMilliseconds,
"max_open_conns": *cfg.SqlSettings.MaxOpenConns,
"data_source_replicas": len(cfg.SqlSettings.DataSourceReplicas),
"data_source_search_replicas": len(cfg.SqlSettings.DataSourceSearchReplicas),
"query_timeout": *cfg.SqlSettings.QueryTimeout,
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
})
ts.SendTelemetry(TrackConfigLog, map[string]any{
"enable_console": cfg.LogSettings.EnableConsole,
"console_level": cfg.LogSettings.ConsoleLevel,
"console_json": *cfg.LogSettings.ConsoleJson,
"enable_file": cfg.LogSettings.EnableFile,
"file_level": cfg.LogSettings.FileLevel,
"file_json": cfg.LogSettings.FileJson,
"enable_webhook_debugging": cfg.LogSettings.EnableWebhookDebugging,
"isdefault_file_location": isDefault(cfg.LogSettings.FileLocation, ""),
"advanced_logging_config": *cfg.LogSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigAudit, map[string]any{
"file_enabled": *cfg.ExperimentalAuditSettings.FileEnabled,
"file_max_size_mb": *cfg.ExperimentalAuditSettings.FileMaxSizeMB,
"file_max_age_days": *cfg.ExperimentalAuditSettings.FileMaxAgeDays,
"file_max_backups": *cfg.ExperimentalAuditSettings.FileMaxBackups,
"file_compress": *cfg.ExperimentalAuditSettings.FileCompress,
"file_max_queue_size": *cfg.ExperimentalAuditSettings.FileMaxQueueSize,
"advanced_logging_config": *cfg.ExperimentalAuditSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigNotificationLog, map[string]any{
"enable_console": *cfg.NotificationLogSettings.EnableConsole,
"console_level": *cfg.NotificationLogSettings.ConsoleLevel,
"console_json": *cfg.NotificationLogSettings.ConsoleJson,
"enable_file": *cfg.NotificationLogSettings.EnableFile,
"file_level": *cfg.NotificationLogSettings.FileLevel,
"file_json": *cfg.NotificationLogSettings.FileJson,
"isdefault_file_location": isDefault(*cfg.NotificationLogSettings.FileLocation, ""),
"advanced_logging_config": *cfg.NotificationLogSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigPassword, map[string]any{
"minimum_length": *cfg.PasswordSettings.MinimumLength,
"lowercase": *cfg.PasswordSettings.Lowercase,
"number": *cfg.PasswordSettings.Number,
"uppercase": *cfg.PasswordSettings.Uppercase,
"symbol": *cfg.PasswordSettings.Symbol,
})
ts.SendTelemetry(TrackConfigFile, map[string]any{
"enable_public_links": cfg.FileSettings.EnablePublicLink,
"driver_name": *cfg.FileSettings.DriverName,
"isdefault_directory": isDefault(*cfg.FileSettings.Directory, model.FileSettingsDefaultDirectory),
"isabsolute_directory": filepath.IsAbs(*cfg.FileSettings.Directory),
"extract_content": *cfg.FileSettings.ExtractContent,
"archive_recursion": *cfg.FileSettings.ArchiveRecursion,
"amazon_s3_ssl": *cfg.FileSettings.AmazonS3SSL,
"amazon_s3_sse": *cfg.FileSettings.AmazonS3SSE,
"amazon_s3_signv2": *cfg.FileSettings.AmazonS3SignV2,
"amazon_s3_trace": *cfg.FileSettings.AmazonS3Trace,
"max_file_size": *cfg.FileSettings.MaxFileSize,
"max_image_resolution": *cfg.FileSettings.MaxImageResolution,
"max_image_decoder_concurrency": *cfg.FileSettings.MaxImageDecoderConcurrency,
"enable_file_attachments": *cfg.FileSettings.EnableFileAttachments,
"enable_mobile_upload": *cfg.FileSettings.EnableMobileUpload,
"enable_mobile_download": *cfg.FileSettings.EnableMobileDownload,
})
ts.SendTelemetry(TrackConfigEmail, map[string]any{
"enable_sign_up_with_email": cfg.EmailSettings.EnableSignUpWithEmail,
"enable_sign_in_with_email": *cfg.EmailSettings.EnableSignInWithEmail,
"enable_sign_in_with_username": *cfg.EmailSettings.EnableSignInWithUsername,
"require_email_verification": cfg.EmailSettings.RequireEmailVerification,
"send_email_notifications": cfg.EmailSettings.SendEmailNotifications,
"use_channel_in_email_notifications": *cfg.EmailSettings.UseChannelInEmailNotifications,
"email_notification_contents_type": *cfg.EmailSettings.EmailNotificationContentsType,
"enable_smtp_auth": *cfg.EmailSettings.EnableSMTPAuth,
"connection_security": cfg.EmailSettings.ConnectionSecurity,
"send_push_notifications": *cfg.EmailSettings.SendPushNotifications,
"push_notification_contents": *cfg.EmailSettings.PushNotificationContents,
"enable_email_batching": *cfg.EmailSettings.EnableEmailBatching,
"email_batching_buffer_size": *cfg.EmailSettings.EmailBatchingBufferSize,
"email_batching_interval": *cfg.EmailSettings.EmailBatchingInterval,
"enable_preview_mode_banner": *cfg.EmailSettings.EnablePreviewModeBanner,
"isdefault_feedback_name": isDefault(cfg.EmailSettings.FeedbackName, ""),
"isdefault_feedback_email": isDefault(cfg.EmailSettings.FeedbackEmail, ""),
"isdefault_reply_to_address": isDefault(cfg.EmailSettings.ReplyToAddress, ""),
"isdefault_feedback_organization": isDefault(*cfg.EmailSettings.FeedbackOrganization, model.EmailSettingsDefaultFeedbackOrganization),
"skip_server_certificate_verification": *cfg.EmailSettings.SkipServerCertificateVerification,
"isdefault_login_button_color": isDefault(*cfg.EmailSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.EmailSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.EmailSettings.LoginButtonTextColor, ""),
"smtp_server_timeout": *cfg.EmailSettings.SMTPServerTimeout,
})
ts.SendTelemetry(TrackConfigRate, map[string]any{
"enable_rate_limiter": *cfg.RateLimitSettings.Enable,
"vary_by_remote_address": *cfg.RateLimitSettings.VaryByRemoteAddr,
"vary_by_user": *cfg.RateLimitSettings.VaryByUser,
"per_sec": *cfg.RateLimitSettings.PerSec,
"max_burst": *cfg.RateLimitSettings.MaxBurst,
"memory_store_size": *cfg.RateLimitSettings.MemoryStoreSize,
"isdefault_vary_by_header": isDefault(cfg.RateLimitSettings.VaryByHeader, ""),
})
ts.SendTelemetry(TrackConfigPrivacy, map[string]any{
"show_email_address": cfg.PrivacySettings.ShowEmailAddress,
"show_full_name": cfg.PrivacySettings.ShowFullName,
})
ts.SendTelemetry(TrackConfigTheme, map[string]any{
"enable_theme_selection": *cfg.ThemeSettings.EnableThemeSelection,
"isdefault_default_theme": isDefault(*cfg.ThemeSettings.DefaultTheme, model.TeamSettingsDefaultTeamText),
"allow_custom_themes": *cfg.ThemeSettings.AllowCustomThemes,
"allowed_themes": len(cfg.ThemeSettings.AllowedThemes),
})
ts.SendTelemetry(TrackConfigOAuth, map[string]any{
"enable_gitlab": cfg.GitLabSettings.Enable,
"openid_gitlab": *cfg.GitLabSettings.Enable && strings.Contains(*cfg.GitLabSettings.Scope, model.ServiceOpenid),
"enable_google": cfg.GoogleSettings.Enable,
"openid_google": *cfg.GoogleSettings.Enable && strings.Contains(*cfg.GoogleSettings.Scope, model.ServiceOpenid),
"enable_office365": cfg.Office365Settings.Enable,
"openid_office365": *cfg.Office365Settings.Enable && strings.Contains(*cfg.Office365Settings.Scope, model.ServiceOpenid),
"enable_openid": cfg.OpenIdSettings.Enable,
})
ts.SendTelemetry(TrackConfigSupport, map[string]any{
"isdefault_terms_of_service_link": isDefault(*cfg.SupportSettings.TermsOfServiceLink, model.SupportSettingsDefaultTermsOfServiceLink),
"isdefault_privacy_policy_link": isDefault(*cfg.SupportSettings.PrivacyPolicyLink, model.SupportSettingsDefaultPrivacyPolicyLink),
"isdefault_about_link": isDefault(*cfg.SupportSettings.AboutLink, model.SupportSettingsDefaultAboutLink),
"isdefault_help_link": isDefault(*cfg.SupportSettings.HelpLink, model.SupportSettingsDefaultHelpLink),
"isdefault_report_a_problem_link": isDefault(*cfg.SupportSettings.ReportAProblemLink, model.SupportSettingsDefaultReportAProblemLink),
"isdefault_support_email": isDefault(*cfg.SupportSettings.SupportEmail, model.SupportSettingsDefaultSupportEmail),
"custom_terms_of_service_enabled": *cfg.SupportSettings.CustomTermsOfServiceEnabled,
"custom_terms_of_service_re_acceptance_period": *cfg.SupportSettings.CustomTermsOfServiceReAcceptancePeriod,
"enable_ask_community_link": *cfg.SupportSettings.EnableAskCommunityLink,
})
ts.SendTelemetry(TrackConfigLDAP, map[string]any{
"enable": *cfg.LdapSettings.Enable,
"enable_sync": *cfg.LdapSettings.EnableSync,
"enable_admin_filter": *cfg.LdapSettings.EnableAdminFilter,
"connection_security": *cfg.LdapSettings.ConnectionSecurity,
"skip_certificate_verification": *cfg.LdapSettings.SkipCertificateVerification,
"sync_interval_minutes": *cfg.LdapSettings.SyncIntervalMinutes,
"query_timeout": *cfg.LdapSettings.QueryTimeout,
"max_page_size": *cfg.LdapSettings.MaxPageSize,
"isdefault_first_name_attribute": isDefault(*cfg.LdapSettings.FirstNameAttribute, model.LdapSettingsDefaultFirstNameAttribute),
"isdefault_last_name_attribute": isDefault(*cfg.LdapSettings.LastNameAttribute, model.LdapSettingsDefaultLastNameAttribute),
"isdefault_email_attribute": isDefault(*cfg.LdapSettings.EmailAttribute, model.LdapSettingsDefaultEmailAttribute),
"isdefault_username_attribute": isDefault(*cfg.LdapSettings.UsernameAttribute, model.LdapSettingsDefaultUsernameAttribute),
"isdefault_nickname_attribute": isDefault(*cfg.LdapSettings.NicknameAttribute, model.LdapSettingsDefaultNicknameAttribute),
"isdefault_id_attribute": isDefault(*cfg.LdapSettings.IdAttribute, model.LdapSettingsDefaultIdAttribute),
"isdefault_position_attribute": isDefault(*cfg.LdapSettings.PositionAttribute, model.LdapSettingsDefaultPositionAttribute),
"isdefault_login_id_attribute": isDefault(*cfg.LdapSettings.LoginIdAttribute, ""),
"isdefault_login_field_name": isDefault(*cfg.LdapSettings.LoginFieldName, model.LdapSettingsDefaultLoginFieldName),
"isdefault_login_button_color": isDefault(*cfg.LdapSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.LdapSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.LdapSettings.LoginButtonTextColor, ""),
"isempty_group_filter": isDefault(*cfg.LdapSettings.GroupFilter, ""),
"isdefault_group_display_name_attribute": isDefault(*cfg.LdapSettings.GroupDisplayNameAttribute, model.LdapSettingsDefaultGroupDisplayNameAttribute),
"isdefault_group_id_attribute": isDefault(*cfg.LdapSettings.GroupIdAttribute, model.LdapSettingsDefaultGroupIdAttribute),
"isempty_guest_filter": isDefault(*cfg.LdapSettings.GuestFilter, ""),
"isempty_admin_filter": isDefault(*cfg.LdapSettings.AdminFilter, ""),
"isnotempty_picture_attribute": !isDefault(*cfg.LdapSettings.PictureAttribute, ""),
"isnotempty_public_certificate": !isDefault(*cfg.LdapSettings.PublicCertificateFile, ""),
"isnotempty_private_key": !isDefault(*cfg.LdapSettings.PrivateKeyFile, ""),
})
ts.SendTelemetry(TrackConfigCompliance, map[string]any{
"enable": *cfg.ComplianceSettings.Enable,
"enable_daily": *cfg.ComplianceSettings.EnableDaily,
})
ts.SendTelemetry(TrackConfigLocalization, map[string]any{
"default_server_locale": *cfg.LocalizationSettings.DefaultServerLocale,
"default_client_locale": *cfg.LocalizationSettings.DefaultClientLocale,
"available_locales": *cfg.LocalizationSettings.AvailableLocales,
})
ts.SendTelemetry(TrackConfigSAML, map[string]any{
"enable": *cfg.SamlSettings.Enable,
"enable_sync_with_ldap": *cfg.SamlSettings.EnableSyncWithLdap,
"enable_sync_with_ldap_include_auth": *cfg.SamlSettings.EnableSyncWithLdapIncludeAuth,
"ignore_guests_ldap_sync": *cfg.SamlSettings.IgnoreGuestsLdapSync,
"enable_admin_attribute": *cfg.SamlSettings.EnableAdminAttribute,
"verify": *cfg.SamlSettings.Verify,
"encrypt": *cfg.SamlSettings.Encrypt,
"sign_request": *cfg.SamlSettings.SignRequest,
"isdefault_signature_algorithm": isDefault(*cfg.SamlSettings.SignatureAlgorithm, ""),
"isdefault_canonical_algorithm": isDefault(*cfg.SamlSettings.CanonicalAlgorithm, ""),
"isdefault_scoping_idp_provider_id": isDefault(*cfg.SamlSettings.ScopingIDPProviderId, ""),
"isdefault_scoping_idp_name": isDefault(*cfg.SamlSettings.ScopingIDPName, ""),
"isdefault_id_attribute": isDefault(*cfg.SamlSettings.IdAttribute, model.SamlSettingsDefaultIdAttribute),
"isdefault_guest_attribute": isDefault(*cfg.SamlSettings.GuestAttribute, model.SamlSettingsDefaultGuestAttribute),
"isdefault_admin_attribute": isDefault(*cfg.SamlSettings.AdminAttribute, model.SamlSettingsDefaultAdminAttribute),
"isdefault_first_name_attribute": isDefault(*cfg.SamlSettings.FirstNameAttribute, model.SamlSettingsDefaultFirstNameAttribute),
"isdefault_last_name_attribute": isDefault(*cfg.SamlSettings.LastNameAttribute, model.SamlSettingsDefaultLastNameAttribute),
"isdefault_email_attribute": isDefault(*cfg.SamlSettings.EmailAttribute, model.SamlSettingsDefaultEmailAttribute),
"isdefault_username_attribute": isDefault(*cfg.SamlSettings.UsernameAttribute, model.SamlSettingsDefaultUsernameAttribute),
"isdefault_nickname_attribute": isDefault(*cfg.SamlSettings.NicknameAttribute, model.SamlSettingsDefaultNicknameAttribute),
"isdefault_locale_attribute": isDefault(*cfg.SamlSettings.LocaleAttribute, model.SamlSettingsDefaultLocaleAttribute),
"isdefault_position_attribute": isDefault(*cfg.SamlSettings.PositionAttribute, model.SamlSettingsDefaultPositionAttribute),
"isdefault_login_button_text": isDefault(*cfg.SamlSettings.LoginButtonText, model.UserAuthServiceSamlText),
"isdefault_login_button_color": isDefault(*cfg.SamlSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.SamlSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.SamlSettings.LoginButtonTextColor, ""),
})
ts.SendTelemetry(TrackConfigCluster, map[string]any{
"enable": *cfg.ClusterSettings.Enable,
"network_interface": isDefault(*cfg.ClusterSettings.NetworkInterface, ""),
"bind_address": isDefault(*cfg.ClusterSettings.BindAddress, ""),
"advertise_address": isDefault(*cfg.ClusterSettings.AdvertiseAddress, ""),
"use_ip_address": *cfg.ClusterSettings.UseIPAddress,
"enable_experimental_gossip_encryption": *cfg.ClusterSettings.EnableExperimentalGossipEncryption,
"enable_gossip_compression": *cfg.ClusterSettings.EnableGossipCompression,
"read_only_config": *cfg.ClusterSettings.ReadOnlyConfig,
})
ts.SendTelemetry(TrackConfigMetrics, map[string]any{
"enable": *cfg.MetricsSettings.Enable,
"block_profile_rate": *cfg.MetricsSettings.BlockProfileRate,
})
ts.SendTelemetry(TrackConfigNativeApp, map[string]any{
"isdefault_app_custom_url_schemes": isDefaultArray(cfg.NativeAppSettings.AppCustomURLSchemes, model.GetDefaultAppCustomURLSchemes()),
"isdefault_app_download_link": isDefault(*cfg.NativeAppSettings.AppDownloadLink, model.NativeappSettingsDefaultAppDownloadLink),
"isdefault_android_app_download_link": isDefault(*cfg.NativeAppSettings.AndroidAppDownloadLink, model.NativeappSettingsDefaultAndroidAppDownloadLink),
"isdefault_iosapp_download_link": isDefault(*cfg.NativeAppSettings.IosAppDownloadLink, model.NativeappSettingsDefaultIosAppDownloadLink),
})
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
})
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
"isdefault_max_users_for_statistics": isDefault(*cfg.AnalyticsSettings.MaxUsersForStatistics, model.AnalyticsSettingsDefaultMaxUsersForStatistics),
})
ts.SendTelemetry(TrackConfigAnnouncement, map[string]any{
"enable_banner": *cfg.AnnouncementSettings.EnableBanner,
"isdefault_banner_color": isDefault(*cfg.AnnouncementSettings.BannerColor, model.AnnouncementSettingsDefaultBannerColor),
"isdefault_banner_text_color": isDefault(*cfg.AnnouncementSettings.BannerTextColor, model.AnnouncementSettingsDefaultBannerTextColor),
"allow_banner_dismissal": *cfg.AnnouncementSettings.AllowBannerDismissal,
"admin_notices_enabled": *cfg.AnnouncementSettings.AdminNoticesEnabled,
"user_notices_enabled": *cfg.AnnouncementSettings.UserNoticesEnabled,
})
ts.SendTelemetry(TrackConfigElasticsearch, map[string]any{
"isdefault_connection_url": isDefault(*cfg.ElasticsearchSettings.ConnectionURL, model.ElasticsearchSettingsDefaultConnectionURL),
"isdefault_username": isDefault(*cfg.ElasticsearchSettings.Username, model.ElasticsearchSettingsDefaultUsername),
"isdefault_password": isDefault(*cfg.ElasticsearchSettings.Password, model.ElasticsearchSettingsDefaultPassword),
"enable_indexing": *cfg.ElasticsearchSettings.EnableIndexing,
"enable_searching": *cfg.ElasticsearchSettings.EnableSearching,
"enable_autocomplete": *cfg.ElasticsearchSettings.EnableAutocomplete,
"sniff": *cfg.ElasticsearchSettings.Sniff,
"post_index_replicas": *cfg.ElasticsearchSettings.PostIndexReplicas,
"post_index_shards": *cfg.ElasticsearchSettings.PostIndexShards,
"channel_index_replicas": *cfg.ElasticsearchSettings.ChannelIndexReplicas,
"channel_index_shards": *cfg.ElasticsearchSettings.ChannelIndexShards,
"user_index_replicas": *cfg.ElasticsearchSettings.UserIndexReplicas,
"user_index_shards": *cfg.ElasticsearchSettings.UserIndexShards,
"isdefault_index_prefix": isDefault(*cfg.ElasticsearchSettings.IndexPrefix, model.ElasticsearchSettingsDefaultIndexPrefix),
"live_indexing_batch_size": *cfg.ElasticsearchSettings.LiveIndexingBatchSize,
"bulk_indexing_batch_size": *cfg.ElasticsearchSettings.BatchSize,
"request_timeout_seconds": *cfg.ElasticsearchSettings.RequestTimeoutSeconds,
"skip_tls_verification": *cfg.ElasticsearchSettings.SkipTLSVerification,
"isdefault_ca": isDefault(*cfg.ElasticsearchSettings.CA, ""),
"isdefault_client_cert": isDefault(*cfg.ElasticsearchSettings.ClientCert, ""),
"isdefault_client_key": isDefault(*cfg.ElasticsearchSettings.ClientKey, ""),
"trace": *cfg.ElasticsearchSettings.Trace,
})
ts.trackPluginConfig(cfg, model.PluginSettingsDefaultMarketplaceURL)
ts.SendTelemetry(TrackConfigDataRetention, map[string]any{
"enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion,
"enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion,
"enable_boards_deletion": *cfg.DataRetentionSettings.EnableBoardsDeletion,
"message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays,
"file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays,
"boards_retention_days": *cfg.DataRetentionSettings.BoardsRetentionDays,
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize,
"cleanup_jobs_threshold_days": *cfg.JobSettings.CleanupJobsThresholdDays,
"cleanup_config_threshold_days": *cfg.JobSettings.CleanupConfigThresholdDays,
})
ts.SendTelemetry(TrackConfigMessageExport, map[string]any{
"enable_message_export": *cfg.MessageExportSettings.EnableExport,
"export_format": *cfg.MessageExportSettings.ExportFormat,
"daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
"default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
"batch_size": *cfg.MessageExportSettings.BatchSize,
"global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType,
"is_default_global_relay_smtp_username": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SMTPUsername, ""),
"is_default_global_relay_smtp_password": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SMTPPassword, ""),
"is_default_global_relay_email_address": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.EmailAddress, ""),
"global_relay_smtp_server_timeout": *cfg.MessageExportSettings.GlobalRelaySettings.SMTPServerTimeout,
"download_export_results": *cfg.MessageExportSettings.DownloadExportResults,
})
ts.SendTelemetry(TrackConfigDisplay, map[string]any{
"experimental_timezone": *cfg.DisplaySettings.ExperimentalTimezone,
"isdefault_custom_url_schemes": len(cfg.DisplaySettings.CustomURLSchemes) != 0,
})
ts.SendTelemetry(TrackConfigGuestAccounts, map[string]any{
"enable": *cfg.GuestAccountsSettings.Enable,
"allow_email_accounts": *cfg.GuestAccountsSettings.AllowEmailAccounts,
"enforce_multifactor_authentication": *cfg.GuestAccountsSettings.EnforceMultifactorAuthentication,
"isdefault_restrict_creation_to_domains": isDefault(*cfg.GuestAccountsSettings.RestrictCreationToDomains, ""),
})
ts.SendTelemetry(TrackConfigImageProxy, map[string]any{
"enable": *cfg.ImageProxySettings.Enable,
"image_proxy_type": *cfg.ImageProxySettings.ImageProxyType,
"isdefault_remote_image_proxy_url": isDefault(*cfg.ImageProxySettings.RemoteImageProxyURL, ""),
"isdefault_remote_image_proxy_options": isDefault(*cfg.ImageProxySettings.RemoteImageProxyOptions, ""),
})
ts.SendTelemetry(TrackConfigBleve, map[string]any{
"enable_indexing": *cfg.BleveSettings.EnableIndexing,
"enable_searching": *cfg.BleveSettings.EnableSearching,
"enable_autocomplete": *cfg.BleveSettings.EnableAutocomplete,
"bulk_indexing_batch_size": *cfg.BleveSettings.BatchSize,
})
ts.SendTelemetry(TrackConfigExport, map[string]any{
"retention_days": *cfg.ExportSettings.RetentionDays,
})
ts.SendTelemetry(TrackConfigProducts, map[string]any{
"enable_public_shared_boards": *cfg.ProductSettings.EnablePublicSharedBoards,
})
// Convert feature flags to map[string]any for sending
flags := cfg.FeatureFlags.ToMap()
interfaceFlags := make(map[string]any)
for k, v := range flags {
interfaceFlags[k] = v
}
ts.SendTelemetry(TrackFeatureFlags, interfaceFlags)
}
func (ts *TelemetryService) trackLicense() {
if license := ts.srv.License(); license != nil {
data := map[string]any{
"customer_id": license.Customer.Id,
"license_id": license.Id,
"issued": license.IssuedAt,
"start": license.StartsAt,
"expire": license.ExpiresAt,
"users": *license.Features.Users,
"edition": license.SkuShortName,
}
features := license.Features.ToMap()
for featureName, featureValue := range features {
data["feature_"+featureName] = featureValue
}
ts.SendTelemetry(TrackLicense, data)
}
}
func (ts *TelemetryService) trackPlugins() {
pluginsEnvironment := ts.srv.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return
}
totalEnabledCount := 0
webappEnabledCount := 0
backendEnabledCount := 0
totalDisabledCount := 0
totalCoreDisabledCount := 0
webappDisabledCount := 0
backendDisabledCount := 0
brokenManifestCount := 0
settingsCount := 0
pluginStates := ts.srv.Config().PluginSettings.PluginStates
plugins, _ := pluginsEnvironment.Available()
if pluginStates != nil && plugins != nil {
for _, plugin := range plugins {
if plugin.Manifest == nil {
brokenManifestCount += 1
continue
}
if state, ok := pluginStates[plugin.Manifest.Id]; ok && state.Enable {
totalEnabledCount += 1
if plugin.Manifest.HasServer() {
backendEnabledCount += 1
}
if plugin.Manifest.HasWebapp() {
webappEnabledCount += 1
}
} else {
totalDisabledCount += 1
if plugin.Manifest.HasServer() {
backendDisabledCount += 1
}
if plugin.Manifest.HasWebapp() {
webappDisabledCount += 1
}
if _, isCorePlugin := model.InstalledIntegrationsIgnoredPlugins[plugin.Manifest.Id]; isCorePlugin {
totalCoreDisabledCount += 1
}
}
if plugin.Manifest.SettingsSchema != nil {
settingsCount += 1
}
}
} else {
totalEnabledCount = -1 // -1 to indicate disabled or error
totalCoreDisabledCount = -1 // -1 to indicate disabled or error
totalDisabledCount = -1 // -1 to indicate disabled or error
}
ts.SendTelemetry(TrackPlugins, map[string]any{
"enabled_plugins": totalEnabledCount,
"enabled_webapp_plugins": webappEnabledCount,
"enabled_backend_plugins": backendEnabledCount,
"disabled_plugins": totalDisabledCount,
"disabled_default_plugins": totalCoreDisabledCount,
"disabled_webapp_plugins": webappDisabledCount,
"disabled_backend_plugins": backendDisabledCount,
"plugins_with_settings": settingsCount,
"plugins_with_broken_manifests": brokenManifestCount,
})
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
hooks.OnSendDailyTelemetry()
return true
}, plugin.OnSendDailyTelemetryID)
}
func (ts *TelemetryService) trackProducts() {
hm := ts.srv.HooksManager()
if hm == nil {
return
}
hm.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnSendDailyTelemetry()
return true
}, plugin.OnSendDailyTelemetryID)
}
func (ts *TelemetryService) trackServer() {
data := map[string]any{
"edition": model.BuildEnterpriseReady,
"version": model.CurrentVersion,
"database_type": *ts.srv.Config().SqlSettings.DriverName,
"operating_system": runtime.GOOS,
"installation_type": os.Getenv(EnvVarInstallType),
}
if scr, err := ts.dbStore.User().AnalyticsGetSystemAdminCount(); err == nil {
data["system_admins"] = scr
}
if scr, err := ts.dbStore.GetDbVersion(false); err == nil {
data["database_version"] = scr
}
ts.SendTelemetry(TrackServer, data)
}
func (ts *TelemetryService) trackPermissions() {
phase1Complete := false
if _, err := ts.dbStore.System().GetByName(model.AdvancedPermissionsMigrationKey); err == nil {
phase1Complete = true
}
phase2Complete := false
if _, err := ts.dbStore.System().GetByName(model.MigrationKeyAdvancedPermissionsPhase2); err == nil {
phase2Complete = true
}
ts.SendTelemetry(TrackPermissionsGeneral, map[string]any{
"phase_1_migration_complete": phase1Complete,
"phase_2_migration_complete": phase2Complete,
})
systemAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemAdminRoleId); err == nil {
systemAdminPermissions = strings.Join(role.Permissions, " ")
}
systemUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemUserRoleId); err == nil {
systemUserPermissions = strings.Join(role.Permissions, " ")
}
teamAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamAdminRoleId); err == nil {
teamAdminPermissions = strings.Join(role.Permissions, " ")
}
teamUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamUserRoleId); err == nil {
teamUserPermissions = strings.Join(role.Permissions, " ")
}
teamGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamGuestRoleId); err == nil {
teamGuestPermissions = strings.Join(role.Permissions, " ")
}
channelAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelAdminRoleId); err == nil {
channelAdminPermissions = strings.Join(role.Permissions, " ")
}
channelUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelUserRoleId); err == nil {
channelUserPermissions = strings.Join(role.Permissions, " ")
}
channelGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelGuestRoleId); err == nil {
channelGuestPermissions = strings.Join(role.Permissions, " ")
}
systemManagerPermissions := ""
systemManagerPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemManagerRoleId); err == nil {
systemManagerPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemManagerDefaultPermissions})) > 0
systemManagerPermissions = strings.Join(role.Permissions, " ")
}
systemManagerCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemManagerRoleId}})
if countErr != nil {
systemManagerCount = 0
}
systemUserManagerPermissions := ""
systemUserManagerPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemUserManagerRoleId); err == nil {
systemUserManagerPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemUserManagerDefaultPermissions})) > 0
systemUserManagerPermissions = strings.Join(role.Permissions, " ")
}
systemUserManagerCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemUserManagerRoleId}})
if countErr != nil {
systemManagerCount = 0
}
systemReadOnlyAdminPermissions := ""
systemReadOnlyAdminPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemReadOnlyAdminRoleId); err == nil {
systemReadOnlyAdminPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemReadOnlyAdminDefaultPermissions})) > 0
systemReadOnlyAdminPermissions = strings.Join(role.Permissions, " ")
}
systemReadOnlyAdminCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemReadOnlyAdminRoleId}})
if countErr != nil {
systemReadOnlyAdminCount = 0
}
systemCustomGroupAdminPermissions := ""
systemCustomGroupAdminPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemCustomGroupAdminRoleId); err == nil {
systemCustomGroupAdminPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemReadOnlyAdminDefaultPermissions})) > 0
systemCustomGroupAdminPermissions = strings.Join(role.Permissions, " ")
}
systemCustomGroupAdminCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemCustomGroupAdminRoleId}})
if countErr != nil {
systemCustomGroupAdminCount = 0
}
ts.SendTelemetry(TrackPermissionsSystemScheme, map[string]any{
"system_admin_permissions": systemAdminPermissions,
"system_user_permissions": systemUserPermissions,
"system_manager_permissions": systemManagerPermissions,
"system_user_manager_permissions": systemUserManagerPermissions,
"system_read_only_admin_permissions": systemReadOnlyAdminPermissions,
"team_admin_permissions": teamAdminPermissions,
"team_user_permissions": teamUserPermissions,
"team_guest_permissions": teamGuestPermissions,
"channel_admin_permissions": channelAdminPermissions,
"channel_user_permissions": channelUserPermissions,
"channel_guest_permissions": channelGuestPermissions,
"system_manager_permissions_modified": systemManagerPermissionsModified,
"system_manager_count": systemManagerCount,
"system_user_manager_permissions_modified": systemUserManagerPermissionsModified,
"system_user_manager_count": systemUserManagerCount,
"system_read_only_admin_permissions_modified": systemReadOnlyAdminPermissionsModified,
"system_read_only_admin_count": systemReadOnlyAdminCount,
"system_custom_group_admin_permissions": systemCustomGroupAdminPermissions,
"system_custom_group_admin_permissions_modified": systemCustomGroupAdminPermissionsModified,
"system_custom_group_admin_count": systemCustomGroupAdminCount,
})
if schemes, err := ts.srv.GetSchemes(model.SchemeScopeTeam, 0, 100); err == nil {
for _, scheme := range schemes {
teamAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamAdminRole); err == nil {
teamAdminPermissions = strings.Join(role.Permissions, " ")
}
teamUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamUserRole); err == nil {
teamUserPermissions = strings.Join(role.Permissions, " ")
}
teamGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamGuestRole); err == nil {
teamGuestPermissions = strings.Join(role.Permissions, " ")
}
channelAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelAdminRole); err == nil {
channelAdminPermissions = strings.Join(role.Permissions, " ")
}
channelUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelUserRole); err == nil {
channelUserPermissions = strings.Join(role.Permissions, " ")
}
channelGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelGuestRole); err == nil {
channelGuestPermissions = strings.Join(role.Permissions, " ")
}
count, _ := ts.dbStore.Team().AnalyticsGetTeamCountForScheme(scheme.Id)
ts.SendTelemetry(TrackPermissionsTeamSchemes, map[string]any{
"scheme_id": scheme.Id,
"team_admin_permissions": teamAdminPermissions,
"team_user_permissions": teamUserPermissions,
"team_guest_permissions": teamGuestPermissions,
"channel_admin_permissions": channelAdminPermissions,
"channel_user_permissions": channelUserPermissions,
"channel_guest_permissions": channelGuestPermissions,
"team_count": count,
})
}
}
}
func (ts *TelemetryService) trackElasticsearch() {
data := map[string]any{}
for _, engine := range ts.searchEngine.GetActiveEngines() {
if engine.GetVersion() != 0 && engine.GetName() == "elasticsearch" {
data["elasticsearch_server_version"] = engine.GetVersion()
}
}
ts.SendTelemetry(TrackElasticsearch, data)
}
func (ts *TelemetryService) trackGroups() {
groupCount, err := ts.dbStore.Group().GroupCount()
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
ldapGroupCount, err := ts.dbStore.Group().GroupCountBySource(model.GroupSourceLdap)
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
customGroupCount, err := ts.dbStore.Group().GroupCountBySource(model.GroupSourceCustom)
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
groupTeamCount, err := ts.dbStore.Group().GroupTeamCount()
if err != nil {
ts.log.Debug("Could not get group_team_count", mlog.Err(err))
}
groupChannelCount, err := ts.dbStore.Group().GroupChannelCount()
if err != nil {
ts.log.Debug("Could not get group_channel_count", mlog.Err(err))
}
groupSyncedTeamCount, nErr := ts.dbStore.Team().GroupSyncedTeamCount()
if nErr != nil {
ts.log.Debug("Could not get group_synced_team_count", mlog.Err(nErr))
}
groupSyncedChannelCount, nErr := ts.dbStore.Channel().GroupSyncedChannelCount()
if nErr != nil {
ts.log.Debug("Could not get group_synced_channel_count", mlog.Err(nErr))
}
groupMemberCount, err := ts.dbStore.Group().GroupMemberCount()
if err != nil {
ts.log.Debug("Could not get group_member_count", mlog.Err(err))
}
distinctGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCount()
if err != nil {
ts.log.Debug("Could not get distinct_group_member_count", mlog.Err(err))
}
distinctCustomGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
if err != nil {
ts.log.Debug("Could not get distinct_custom_group_member_count", mlog.Err(err))
}
distinctLdapGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
if err != nil {
ts.log.Debug("Could not get distinct_ldap_group_member_count", mlog.Err(err))
}
groupCountWithAllowReference, err := ts.dbStore.Group().GroupCountWithAllowReference()
if err != nil {
ts.log.Debug("Could not get group_count_with_allow_reference", mlog.Err(err))
}
ts.SendTelemetry(TrackGroups, map[string]any{
"group_count": groupCount,
"ldap_group_count": ldapGroupCount,
"custom_group_count": customGroupCount,
"group_team_count": groupTeamCount,
"group_channel_count": groupChannelCount,
"group_synced_team_count": groupSyncedTeamCount,
"group_synced_channel_count": groupSyncedChannelCount,
"group_member_count": groupMemberCount,
"distinct_group_member_count": distinctGroupMemberCount,
"distinct_custom_group_member_count": distinctCustomGroupMemberCount,
"distinct_ldap_group_member_count": distinctLdapGroupMemberCount,
"group_count_with_allow_reference": groupCountWithAllowReference,
})
}
func (ts *TelemetryService) trackChannelModeration() {
channelSchemeCount, err := ts.dbStore.Scheme().CountByScope(model.SchemeScopeChannel)
if err != nil {
ts.log.Debug("Could not get channel_scheme_count", mlog.Err(err))
}
createPostUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionCreatePost.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get create_post_user_disabled_count", mlog.Err(err))
}
createPostGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionCreatePost.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get create_post_guest_disabled_count", mlog.Err(err))
}
// only need to track one of 'add_reaction' or 'remove_reaction` because they're both toggled together by the channel moderation feature
postReactionsUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionAddReaction.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get post_reactions_user_disabled_count", mlog.Err(err))
}
postReactionsGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionAddReaction.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get post_reactions_guest_disabled_count", mlog.Err(err))
}
// only need to track one of 'manage_public_channel_members' or 'manage_private_channel_members` because they're both toggled together by the channel moderation feature
manageMembersUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionManagePublicChannelMembers.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get manage_members_user_disabled_count", mlog.Err(err))
}
useChannelMentionsUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionUseChannelMentions.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get use_channel_mentions_user_disabled_count", mlog.Err(err))
}
useChannelMentionsGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionUseChannelMentions.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get use_channel_mentions_guest_disabled_count", mlog.Err(err))
}
ts.SendTelemetry(TrackChannelModeration, map[string]any{
"channel_scheme_count": channelSchemeCount,
"create_post_user_disabled_count": createPostUser,
"create_post_guest_disabled_count": createPostGuest,
"post_reactions_user_disabled_count": postReactionsUser,
"post_reactions_guest_disabled_count": postReactionsGuest,
"manage_members_user_disabled_count": manageMembersUser, // the UI does not allow this to be removed for guests
"use_channel_mentions_user_disabled_count": useChannelMentionsUser,
"use_channel_mentions_guest_disabled_count": useChannelMentionsGuest,
})
}
func (ts *TelemetryService) initRudder(endpoint string, rudderKey string) {
if ts.rudderClient == nil {
config := rudder.Config{}
config.Logger = rudder.StdLogger(ts.log.With(mlog.String("source", "rudder")).StdLogger(mlog.LvlDebug))
config.Endpoint = endpoint
config.Verbose = ts.verbose
// For testing
if endpoint != RudderDataplaneURL {
config.BatchSize = 1
}
client, err := rudder.NewWithConfig(rudderKey, endpoint, config)
if err != nil {
ts.log.Error("Failed to create Rudder instance", mlog.Err(err))
return
}
client.Enqueue(rudder.Identify{
UserId: ts.TelemetryID,
})
ts.rudderClient = client
}
}
func (ts *TelemetryService) doTelemetryIfNeeded(firstRun time.Time) {
hoursSinceFirstServerRun := time.Since(firstRun).Hours()
// Send once every 10 minutes for the first hour
// Send once every hour thereafter for the first 12 hours
// Send at the 24 hour mark and every 24 hours after
if hoursSinceFirstServerRun < 1 {
ts.doTelemetry()
} else if hoursSinceFirstServerRun <= 12 && time.Since(ts.timestampLastTelemetrySent) >= time.Hour {
ts.doTelemetry()
} else if hoursSinceFirstServerRun > 12 && time.Since(ts.timestampLastTelemetrySent) >= 24*time.Hour {
ts.doTelemetry()
}
}
func (ts *TelemetryService) RunTelemetryJob(firstRun int64) {
// Send on boot
ts.doTelemetry()
model.CreateRecurringTask("Telemetry", func() {
ts.doTelemetryIfNeeded(utils.TimeFromMillis(firstRun))
}, time.Minute*10)
}
func (ts *TelemetryService) doTelemetry() {
if *ts.srv.Config().LogSettings.EnableDiagnostics {
ts.timestampLastTelemetrySent = time.Now()
ts.sendDailyTelemetry(false)
}
}
// Shutdown closes the telemetry client.
func (ts *TelemetryService) Shutdown() error {
if ts.rudderClient != nil {
return ts.rudderClient.Close()
}
return nil
}
func (ts *TelemetryService) trackWarnMetrics() {
systemDataList, nErr := ts.dbStore.System().Get()
if nErr != nil {
return
}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WarnMetricStatusStorePrefix) {
if _, ok := model.WarnMetricsTable[key]; ok {
ts.SendTelemetry(TrackWarnMetrics, map[string]any{
key: value != "false",
})
}
}
}
}
func (ts *TelemetryService) trackPluginConfig(cfg *model.Config, marketplaceURL string) {
pluginConfigData := map[string]any{
"enable_nps_survey": pluginSetting(&cfg.PluginSettings, model.PluginIdNPS, "enablesurvey", true),
"enable": *cfg.PluginSettings.Enable,
"enable_uploads": *cfg.PluginSettings.EnableUploads,
"allow_insecure_download_url": *cfg.PluginSettings.AllowInsecureDownloadURL,
"enable_health_check": *cfg.PluginSettings.EnableHealthCheck,
"enable_marketplace": *cfg.PluginSettings.EnableMarketplace,
"require_pluginSignature": *cfg.PluginSettings.RequirePluginSignature,
"enable_remote_marketplace": *cfg.PluginSettings.EnableRemoteMarketplace,
"automatic_prepackaged_plugins": *cfg.PluginSettings.AutomaticPrepackagedPlugins,
"is_default_marketplace_url": isDefault(*cfg.PluginSettings.MarketplaceURL, model.PluginSettingsDefaultMarketplaceURL),
"signature_public_key_files": len(cfg.PluginSettings.SignaturePublicKeyFiles),
"chimera_oauth_proxy_url": *cfg.PluginSettings.ChimeraOAuthProxyURL,
}
// knownPluginIDs lists all known plugin IDs in the Marketplace
knownPluginIDs := []string{
"antivirus",
"com.github.manland.mattermost-plugin-gitlab",
"com.github.moussetc.mattermost.plugin.giphy",
"com.github.phillipahereza.mattermost-plugin-digitalocean",
"com.mattermost.aws-sns",
"com.mattermost.confluence",
"com.mattermost.custom-attributes",
"com.mattermost.mscalendar",
"com.mattermost.nps",
"com.mattermost.plugin-channel-export",
"com.mattermost.plugin-incident-management",
"playbooks",
"com.mattermost.plugin-todo",
"com.mattermost.webex",
"com.mattermost.welcomebot",
"github",
"jenkins",
"jira",
"jitsi",
"mattermost-autolink",
"memes",
"skype4business",
"zoom",
"focalboard",
}
marketplacePlugins, err := ts.GetAllMarketplacePlugins(marketplaceURL)
if err != nil {
mlog.Info("Failed to fetch marketplace plugins for telemetry. Using predefined list.", mlog.Err(err))
for _, id := range knownPluginIDs {
pluginConfigData["enable_"+id] = pluginActivated(cfg.PluginSettings.PluginStates, id)
}
} else {
for _, p := range marketplacePlugins {
id := p.Manifest.Id
pluginConfigData["enable_"+id] = pluginActivated(cfg.PluginSettings.PluginStates, id)
}
}
pluginsEnvironment := ts.srv.GetPluginsEnvironment()
if pluginsEnvironment != nil {
if plugins, appErr := pluginsEnvironment.Available(); appErr != nil {
ts.log.Warn("Unable to add plugin versions to telemetry", mlog.Err(appErr))
} else {
// If marketplace request failed, use predefined list
if marketplacePlugins == nil {
for _, id := range knownPluginIDs {
pluginConfigData["version_"+id] = pluginVersion(plugins, id)
}
} else {
for _, p := range marketplacePlugins {
id := p.Manifest.Id
pluginConfigData["version_"+id] = pluginVersion(plugins, id)
}
}
}
}
ts.SendTelemetry(TrackConfigPlugin, pluginConfigData)
}
func (ts *TelemetryService) GetAllMarketplacePlugins(marketplaceURL string) ([]*model.BaseMarketplacePlugin, error) {
marketplaceClient, err := marketplace.NewClient(
marketplaceURL,
ts.srv.HTTPService(),
)
if err != nil {
return nil, err
}
// Fetch all plugins from marketplace.
filter := &model.MarketplacePluginFilter{
PerPage: -1,
ServerVersion: model.CurrentVersion,
}
license := ts.srv.License()
if license != nil && *license.Features.EnterprisePlugins {
filter.EnterprisePlugins = true
}
if model.BuildEnterpriseReady == "true" {
filter.BuildEnterpriseReady = true
}
return marketplaceClient.GetPlugins(filter)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package timezones
type Timezones struct {
supportedZones []string
}
func New() *Timezones {
timezones := Timezones{}
timezones.supportedZones = DefaultSupportedTimezones
return &timezones
}
func (t *Timezones) GetSupported() []string {
return t.supportedZones
}
func DefaultUserTimezone() map[string]string {
defaultTimezone := make(map[string]string)
defaultTimezone["useAutomaticTimezone"] = "true"
defaultTimezone["automaticTimezone"] = ""
defaultTimezone["manualTimezone"] = ""
return defaultTimezone
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package tracing
import (
"context"
"io"
"time"
opentracing "github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/zipkin"
"github.com/uber/jaeger-lib/metrics"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Tracer is a wrapper around Jaeger OpenTracing client, used to properly de-initialize jaeger on exit
type Tracer struct {
closer io.Closer
}
type LogrusAdapter struct {
}
// Error - logrus adapter for span errors
func (LogrusAdapter) Error(msg string) {
mlog.Error(msg)
}
// Infof - logrus adapter for span info logging
func (LogrusAdapter) Infof(msg string, args ...any) {
// we ignore Info messages from opentracing
}
// New instantiates Jaeger opentracing client with default options
// To override the defaults use environment variables listed here: https://github.com/jaegertracing/jaeger-client-go/blob/master/config/config.go
func New() (*Tracer, error) {
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
},
}
zipkinPropagator := zipkin.NewZipkinB3HTTPHeaderPropagator()
closer, err := cfg.InitGlobalTracer(
"mattermost",
jaegercfg.Logger(LogrusAdapter{}),
jaegercfg.Metrics(metrics.NullFactory),
jaegercfg.Tag("serverStartTime", time.Now().UTC().Format(time.RFC3339)),
jaegercfg.Injector(opentracing.HTTPHeaders, zipkinPropagator),
jaegercfg.Extractor(opentracing.HTTPHeaders, zipkinPropagator),
jaegercfg.ZipkinSharedRPCSpan(true),
)
if err != nil {
return nil, err
}
mlog.Info("Opentracing initialized")
return &Tracer{
closer: closer,
}, nil
}
func (t *Tracer) Close() error {
return t.closer.Close()
}
func StartRootSpanByContext(ctx context.Context, operationName string) (opentracing.Span, context.Context) {
return opentracing.StartSpanFromContext(ctx, operationName)
}
func StartSpanWithParentByContext(ctx context.Context, operationName string) (opentracing.Span, context.Context) {
parentSpan := opentracing.SpanFromContext(ctx)
if parentSpan == nil {
return StartRootSpanByContext(ctx, operationName)
}
return opentracing.StartSpanFromContext(ctx, operationName, opentracing.ChildOf(parentSpan.Context()))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package upgrader
import (
"fmt"
)
// InvalidArch indicates that the current operating system or cpu architecture doesn't support upgrades
type InvalidArch struct{}
func NewInvalidArch() *InvalidArch {
return &InvalidArch{}
}
func (e *InvalidArch) Error() string {
return "invalid operating system or processor architecture"
}
// InvalidSignature indicates that the downloaded file doesn't have a valid signature.
type InvalidSignature struct{}
func NewInvalidSignature() *InvalidSignature {
return &InvalidSignature{}
}
func (e *InvalidSignature) Error() string {
return "invalid file signature"
}
// InvalidPermissions indicates that the file permissions doesn't allow to upgrade
type InvalidPermissions struct {
ErrType string
Path string
FileUsername string
MattermostUsername string
}
func NewInvalidPermissions(errType string, path string, mattermostUsername string, fileUsername string) *InvalidPermissions {
return &InvalidPermissions{
ErrType: errType,
Path: path,
FileUsername: fileUsername,
MattermostUsername: mattermostUsername,
}
}
func (e *InvalidPermissions) Error() string {
return fmt.Sprintf("the user %s is unable to update the %s file", e.MattermostUsername, e.Path)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !linux
// +build !linux
package upgrader
func CanIUpgradeToE0() error {
return &InvalidArch{}
}
func UpgradeToE0() error {
return &InvalidArch{}
}
func UpgradeToE0Status() (int64, error) {
return 0, &InvalidArch{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"context"
"io"
"time"
"github.com/pkg/errors"
)
const (
driverS3 = "amazons3"
driverLocal = "local"
)
type ReadCloseSeeker interface {
io.ReadCloser
io.Seeker
}
type FileBackend interface {
TestConnection() error
Reader(path string) (ReadCloseSeeker, error)
ReadFile(path string) ([]byte, error)
FileExists(path string) (bool, error)
FileSize(path string) (int64, error)
CopyFile(oldPath, newPath string) error
MoveFile(oldPath, newPath string) error
WriteFile(fr io.Reader, path string) (int64, error)
AppendFile(fr io.Reader, path string) (int64, error)
RemoveFile(path string) error
FileModTime(path string) (time.Time, error)
ListDirectory(path string) ([]string, error)
ListDirectoryRecursively(path string) ([]string, error)
RemoveDirectory(path string) error
}
type FileBackendSettings struct {
DriverName string
Directory string
AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string
AmazonS3Bucket string
AmazonS3PathPrefix string
AmazonS3Region string
AmazonS3Endpoint string
AmazonS3SSL bool
AmazonS3SignV2 bool
AmazonS3SSE bool
AmazonS3Trace bool
SkipVerify bool
AmazonS3RequestTimeoutMilliseconds int64
}
func (settings *FileBackendSettings) CheckMandatoryS3Fields() error {
if settings.AmazonS3Bucket == "" {
return errors.New("missing s3 bucket settings")
}
// if S3 endpoint is not set call the set defaults to set that
if settings.AmazonS3Endpoint == "" {
settings.AmazonS3Endpoint = "s3.amazonaws.com"
}
return nil
}
func NewFileBackend(settings FileBackendSettings) (FileBackend, error) {
switch settings.DriverName {
case driverS3:
backend, err := NewS3FileBackend(settings)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the s3 backend")
}
return backend, nil
case driverLocal:
return &LocalFileBackend{
directory: settings.Directory,
}, nil
}
return nil, errors.New("no valid filestorage driver found")
}
// TryWriteFileContext checks if the file backend supports context writes and passes the context in that case.
// Should the file backend not support contexts, it just calls WriteFile instead. This can be used to disable
// the timeouts for long writes (like exports).
func TryWriteFileContext(fb FileBackend, ctx context.Context, fr io.Reader, path string) (int64, error) {
type ContextWriter interface {
WriteFileContext(context.Context, io.Reader, string) (int64, error)
}
if cw, ok := fb.(ContextWriter); ok {
return cw.WriteFileContext(ctx, fr, path)
}
return fb.WriteFile(fr, path)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"bytes"
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TestFilePath = "/testfile"
)
type LocalFileBackend struct {
directory string
}
// copyFile will copy a file from src path to dst path.
// Overwrites any existing files at dst.
// Permissions are copied from file at src to the new file at dst.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return
}
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
stat, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, stat.Mode())
if err != nil {
return
}
return
}
func (b *LocalFileBackend) TestConnection() error {
f := bytes.NewReader([]byte("testingwrite"))
if _, err := writeFileLocally(f, filepath.Join(b.directory, TestFilePath)); err != nil {
return errors.Wrap(err, "unable to write to the local filesystem storage")
}
os.Remove(filepath.Join(b.directory, TestFilePath))
mlog.Debug("Able to write files to local storage.")
return nil
}
func (b *LocalFileBackend) Reader(path string) (ReadCloseSeeker, error) {
f, err := os.Open(filepath.Join(b.directory, path))
if err != nil {
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
return f, nil
}
func (b *LocalFileBackend) ReadFile(path string) ([]byte, error) {
f, err := os.ReadFile(filepath.Join(b.directory, path))
if err != nil {
return nil, errors.Wrapf(err, "unable to read file %s", path)
}
return f, nil
}
func (b *LocalFileBackend) FileExists(path string) (bool, error) {
_, err := os.Stat(filepath.Join(b.directory, path))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, errors.Wrapf(err, "unable to know if file %s exists", path)
}
return true, nil
}
func (b *LocalFileBackend) FileSize(path string) (int64, error) {
info, err := os.Stat(filepath.Join(b.directory, path))
if err != nil {
return 0, errors.Wrapf(err, "unable to get file size for %s", path)
}
return info.Size(), nil
}
func (b *LocalFileBackend) FileModTime(path string) (time.Time, error) {
info, err := os.Stat(filepath.Join(b.directory, path))
if err != nil {
return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
}
return info.ModTime(), nil
}
func (b *LocalFileBackend) CopyFile(oldPath, newPath string) error {
if err := copyFile(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
}
return nil
}
func (b *LocalFileBackend) MoveFile(oldPath, newPath string) error {
if err := os.MkdirAll(filepath.Dir(filepath.Join(b.directory, newPath)), 0750); err != nil {
return errors.Wrapf(err, "unable to create the new destination directory %s", filepath.Dir(newPath))
}
if err := os.Rename(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
return errors.Wrapf(err, "unable to move the file to %s to the destination directory", newPath)
}
return nil
}
func (b *LocalFileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
return writeFileLocally(fr, filepath.Join(b.directory, path))
}
func writeFileLocally(fr io.Reader, path string) (int64, error) {
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
directory, _ := filepath.Abs(filepath.Dir(path))
return 0, errors.Wrapf(err, "unable to create the directory %s for the file %s", directory, path)
}
fw, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return 0, errors.Wrapf(err, "unable to open the file %s to write the data", path)
}
defer fw.Close()
written, err := io.Copy(fw, fr)
if err != nil {
return written, errors.Wrapf(err, "unable write the data in the file %s", path)
}
return written, nil
}
func (b *LocalFileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
fp := filepath.Join(b.directory, path)
if _, err := os.Stat(fp); err != nil {
return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
}
fw, err := os.OpenFile(fp, os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return 0, errors.Wrapf(err, "unable to open the file %s to append the data", path)
}
defer fw.Close()
written, err := io.Copy(fw, fr)
if err != nil {
return written, errors.Wrapf(err, "unable append the data in the file %s", path)
}
return written, nil
}
func (b *LocalFileBackend) RemoveFile(path string) error {
if err := os.Remove(filepath.Join(b.directory, path)); err != nil {
return errors.Wrapf(err, "unable to remove the file %s", path)
}
return nil
}
// basePath: path to get to the file but won't be added to the end result
// path: basePath+path current directory we are looking at
// maxDepth: parameter to prevent infinite recursion, once this is reached we won't look any further
func appendRecursively(basePath, path string, maxDepth int) ([]string, error) {
results := []string{}
dirEntries, err := os.ReadDir(filepath.Join(basePath, path))
if err != nil {
if os.IsNotExist(err) {
return results, nil
}
return results, errors.Wrapf(err, "unable to list the directory %s", path)
}
for _, dirEntry := range dirEntries {
entryName := dirEntry.Name()
entryPath := filepath.Join(path, entryName)
if entryName == "." || entryName == ".." || entryPath == path {
continue
}
if dirEntry.IsDir() {
if maxDepth <= 0 {
mlog.Warn("Max Depth reached", mlog.String("path", entryPath))
results = append(results, entryPath)
continue // we'll ignore it if max depth is reached.
}
nestedResults, err := appendRecursively(basePath, entryPath, maxDepth-1)
if err != nil {
return results, err
}
results = append(results, nestedResults...)
} else {
results = append(results, entryPath)
}
}
return results, nil
}
func (b *LocalFileBackend) ListDirectory(path string) ([]string, error) {
return appendRecursively(b.directory, path, 0)
}
func (b *LocalFileBackend) ListDirectoryRecursively(path string) ([]string, error) {
return appendRecursively(b.directory, path, 10)
}
func (b *LocalFileBackend) RemoveDirectory(path string) error {
if err := os.RemoveAll(filepath.Join(b.directory, path)); err != nil {
return errors.Wrapf(err, "unable to remove the directory %s", path)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"context"
"net/http"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// customTransport is used to point the request to a different server.
// This is helpful in situations where a different service is handling AWS S3 requests
// from multiple Mattermost applications, and the Mattermost service itself does not
// have any S3 credentials.
type customTransport struct {
host string
scheme string
client http.Client
}
// RoundTrip implements the http.Roundtripper interface.
func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Roundtrippers should not modify the original request.
newReq := req.Clone(context.Background())
*newReq.URL = *req.URL
req.URL.Scheme = t.scheme
req.URL.Host = t.host
return t.client.Do(req)
}
// customProvider is a dummy credentials provider for the minio client to work
// without actually providing credentials. This is needed with a custom transport
// in cases where the minio client does not actually have credentials with itself,
// rather needs responses from another entity.
//
// It satisfies the credentials.Provider interface.
type customProvider struct {
isSignV2 bool
}
// Retrieve just returns empty credentials.
func (cp customProvider) Retrieve() (credentials.Value, error) {
sign := credentials.SignatureV4
if cp.isSignV2 {
sign = credentials.SignatureV2
}
return credentials.Value{
SignerType: sign,
}, nil
}
// IsExpired always returns false.
func (cp customProvider) IsExpired() bool { return false }
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/encrypt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// S3FileBackend contains all necessary information to communicate with
// an AWS S3 compatible API backend.
type S3FileBackend struct {
endpoint string
accessKey string
secretKey string
secure bool
signV2 bool
region string
bucket string
pathPrefix string
encrypt bool
trace bool
client *s3.Client
skipVerify bool
timeout time.Duration
}
type S3FileBackendAuthError struct {
DetailedError string
}
// S3FileBackendNoBucketError is returned when testing a connection and no S3 bucket is found
type S3FileBackendNoBucketError struct{}
const (
// This is not exported by minio. See: https://github.com/minio/minio-go/issues/1339
bucketNotFound = "NoSuchBucket"
)
var (
imageExtensions = map[string]bool{".jpg": true, ".jpeg": true, ".gif": true, ".bmp": true, ".png": true, ".tiff": true, "tif": true}
imageMimeTypes = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff", ".tif": "image/tif"}
)
var (
// Ensure that the ReaderAt interface is implemented.
_ io.ReaderAt = (*s3WithCancel)(nil)
)
func isFileExtImage(ext string) bool {
ext = strings.ToLower(ext)
return imageExtensions[ext]
}
func getImageMimeType(ext string) string {
ext = strings.ToLower(ext)
if imageMimeTypes[ext] == "" {
return "image"
}
return imageMimeTypes[ext]
}
func (s *S3FileBackendAuthError) Error() string {
return s.DetailedError
}
func (s *S3FileBackendNoBucketError) Error() string {
return "no such bucket"
}
// NewS3FileBackend returns an instance of an S3FileBackend.
func NewS3FileBackend(settings FileBackendSettings) (*S3FileBackend, error) {
timeout := time.Duration(settings.AmazonS3RequestTimeoutMilliseconds) * time.Millisecond
backend := &S3FileBackend{
endpoint: settings.AmazonS3Endpoint,
accessKey: settings.AmazonS3AccessKeyId,
secretKey: settings.AmazonS3SecretAccessKey,
secure: settings.AmazonS3SSL,
signV2: settings.AmazonS3SignV2,
region: settings.AmazonS3Region,
bucket: settings.AmazonS3Bucket,
pathPrefix: settings.AmazonS3PathPrefix,
encrypt: settings.AmazonS3SSE,
trace: settings.AmazonS3Trace,
skipVerify: settings.SkipVerify,
timeout: timeout,
}
cli, err := backend.s3New()
if err != nil {
return nil, err
}
backend.client = cli
return backend, nil
}
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func (b *S3FileBackend) s3New() (*s3.Client, error) {
var creds *credentials.Credentials
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
creds = credentials.New(customProvider{isSignV2: b.signV2})
} else if b.accessKey == "" && b.secretKey == "" {
creds = credentials.NewIAM("")
} else if b.signV2 {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4)
}
opts := s3.Options{
Creds: creds,
Secure: b.secure,
Region: b.region,
}
tr, err := s3.DefaultTransport(b.secure)
if err != nil {
return nil, err
}
if b.skipVerify {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
opts.Transport = tr
// If this is a cloud installation, we override the default transport.
if isCloud {
scheme := "http"
if b.secure {
scheme = "https"
}
newTransport := http.DefaultTransport.(*http.Transport).Clone()
newTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: b.skipVerify}
opts.Transport = &customTransport{
host: b.endpoint,
scheme: scheme,
client: http.Client{Transport: newTransport},
}
}
s3Clnt, err := s3.New(b.endpoint, &opts)
if err != nil {
return nil, err
}
if b.trace {
s3Clnt.TraceOn(os.Stdout)
}
return s3Clnt, nil
}
func (b *S3FileBackend) TestConnection() error {
exists := true
var err error
// If a path prefix is present, we attempt to test the bucket by listing objects under the path
// and just checking the first response. This is because the BucketExists call is only at a bucket level
// and sometimes the user might only be allowed access to the specified path prefix.
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if b.pathPrefix != "" {
obj := <-b.client.ListObjects(ctx, b.bucket, s3.ListObjectsOptions{Prefix: b.pathPrefix})
if obj.Err != nil {
typedErr := s3.ToErrorResponse(obj.Err)
if typedErr.Code != bucketNotFound {
return &S3FileBackendAuthError{DetailedError: "unable to list objects in the S3 bucket"}
}
exists = false
}
} else {
exists, err = b.client.BucketExists(ctx, b.bucket)
if err != nil {
return &S3FileBackendAuthError{DetailedError: "unable to check if the S3 bucket exists"}
}
}
if !exists {
return &S3FileBackendNoBucketError{}
}
mlog.Debug("Connection to S3 or minio is good. Bucket exists.")
return nil
}
func (b *S3FileBackend) MakeBucket() error {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
err := b.client.MakeBucket(ctx, b.bucket, s3.MakeBucketOptions{Region: b.region})
if err != nil {
return errors.Wrap(err, "unable to create the s3 bucket")
}
return nil
}
// s3WithCancel is a wrapper struct which cancels the context
// when the object is closed.
type s3WithCancel struct {
*s3.Object
timer *time.Timer
cancel context.CancelFunc
}
func (sc *s3WithCancel) Close() error {
sc.timer.Stop()
sc.cancel()
return sc.Object.Close()
}
// CancelTimeout attempts to cancel the timeout for this reader. It allows calling
// code to ignore the timeout in case of longer running operations. The methods returns
// false if the timeout has already fired.
func (sc *s3WithCancel) CancelTimeout() bool {
return sc.timer.Stop()
}
// Caller must close the first return value
func (b *S3FileBackend) Reader(path string) (ReadCloseSeeker, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithCancel(context.Background())
minioObject, err := b.client.GetObject(ctx, b.bucket, path, s3.GetObjectOptions{})
if err != nil {
cancel()
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
sc := &s3WithCancel{
Object: minioObject,
timer: time.AfterFunc(b.timeout, cancel),
cancel: cancel,
}
return sc, nil
}
func (b *S3FileBackend) ReadFile(path string) ([]byte, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
minioObject, err := b.client.GetObject(ctx, b.bucket, path, s3.GetObjectOptions{})
if err != nil {
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
defer minioObject.Close()
f, err := io.ReadAll(minioObject)
if err != nil {
return nil, errors.Wrapf(err, "unable to read file %s", path)
}
return f, nil
}
func (b *S3FileBackend) FileExists(path string) (bool, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
_, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err == nil {
return true, nil
}
var s3Err s3.ErrorResponse
if errors.As(err, &s3Err); s3Err.Code == "NoSuchKey" {
return false, nil
}
return false, errors.Wrapf(err, "unable to know if file %s exists", path)
}
func (b *S3FileBackend) FileSize(path string) (int64, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
info, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err != nil {
return 0, errors.Wrapf(err, "unable to get file size for %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) FileModTime(path string) (time.Time, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
info, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err != nil {
return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
}
return info.LastModified, nil
}
func (b *S3FileBackend) CopyFile(oldPath, newPath string) error {
oldPath = filepath.Join(b.pathPrefix, oldPath)
newPath = filepath.Join(b.pathPrefix, newPath)
srcOpts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: oldPath,
}
if b.encrypt {
srcOpts.Encryption = encrypt.NewSSE()
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: newPath,
}
if b.encrypt {
dstOpts.Encryption = encrypt.NewSSE()
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.CopyObject(ctx, dstOpts, srcOpts); err != nil {
return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
}
return nil
}
func (b *S3FileBackend) MoveFile(oldPath, newPath string) error {
oldPath = filepath.Join(b.pathPrefix, oldPath)
newPath = filepath.Join(b.pathPrefix, newPath)
srcOpts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: oldPath,
}
if b.encrypt {
srcOpts.Encryption = encrypt.NewSSE()
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: newPath,
}
if b.encrypt {
dstOpts.Encryption = encrypt.NewSSE()
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.CopyObject(ctx, dstOpts, srcOpts); err != nil {
return errors.Wrapf(err, "unable to copy the file to %s to the new destination", newPath)
}
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
if err := b.client.RemoveObject(ctx2, b.bucket, oldPath, s3.RemoveObjectOptions{}); err != nil {
return errors.Wrapf(err, "unable to remove the file old file %s", oldPath)
}
return nil
}
func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
return b.WriteFileContext(ctx, fr, path)
}
func (b *S3FileBackend) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, error) {
var contentType string
path = filepath.Join(b.pathPrefix, path)
if ext := filepath.Ext(path); isFileExtImage(ext) {
contentType = getImageMimeType(ext)
} else {
contentType = "binary/octet-stream"
}
options := s3PutOptions(b.encrypt, contentType)
objSize := int64(-1)
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
options.DisableContentSha256 = true
} else {
// We pass an object size only in situations where bifrost is not
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
switch t := fr.(type) {
case *bytes.Buffer:
objSize = int64(t.Len())
case *os.File:
if s, err := t.Stat(); err == nil {
objSize = s.Size()
}
}
}
info, err := b.client.PutObject(ctx, b.bucket, path, fr, objSize, options)
if err != nil {
return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
fp := filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.StatObject(ctx, b.bucket, fp, s3.StatObjectOptions{}); err != nil {
return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
}
var contentType string
if ext := filepath.Ext(fp); isFileExtImage(ext) {
contentType = getImageMimeType(ext)
} else {
contentType = "binary/octet-stream"
}
options := s3PutOptions(b.encrypt, contentType)
sse := options.ServerSideEncryption
partName := fp + ".part"
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
objSize := -1
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
options.DisableContentSha256 = true
}
// We pass an object size only in situations where bifrost is not
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
if buf, ok := fr.(*bytes.Buffer); ok && !isCloud {
objSize = buf.Len()
}
info, err := b.client.PutObject(ctx2, b.bucket, partName, fr, int64(objSize), options)
if err != nil {
return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
}
defer func() {
ctx4, cancel4 := context.WithTimeout(context.Background(), b.timeout)
defer cancel4()
b.client.RemoveObject(ctx4, b.bucket, partName, s3.RemoveObjectOptions{})
}()
src1Opts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: fp,
}
src2Opts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: partName,
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: fp,
Encryption: sse,
}
ctx3, cancel3 := context.WithTimeout(context.Background(), b.timeout)
defer cancel3()
_, err = b.client.ComposeObject(ctx3, dstOpts, src1Opts, src2Opts)
if err != nil {
return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) RemoveFile(path string) error {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if err := b.client.RemoveObject(ctx, b.bucket, path, s3.RemoveObjectOptions{}); err != nil {
return errors.Wrapf(err, "unable to remove the file %s", path)
}
return nil
}
func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan s3.ObjectInfo {
out := make(chan s3.ObjectInfo, 1)
go func() {
defer close(out)
for {
info, done := <-in
if !done {
break
}
out <- info
}
}()
return out
}
func (b *S3FileBackend) listDirectory(path string, recursion bool) ([]string, error) {
path = filepath.Join(b.pathPrefix, path)
if !strings.HasSuffix(path, "/") && path != "" {
// s3Clnt returns only the path itself when "/" is not present
// appending "/" to make it consistent across all filestores
path = path + "/"
}
opts := s3.ListObjectsOptions{
Prefix: path,
Recursive: recursion,
}
var paths []string
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
for object := range b.client.ListObjects(ctx, b.bucket, opts) {
if object.Err != nil {
return nil, errors.Wrapf(object.Err, "unable to list the directory %s", path)
}
// We strip the path prefix that gets applied,
// so that it remains transparent to the application.
object.Key = strings.TrimPrefix(object.Key, b.pathPrefix)
trimmed := strings.Trim(object.Key, "/")
if trimmed != "" {
paths = append(paths, trimmed)
}
}
return paths, nil
}
func (b *S3FileBackend) ListDirectory(path string) ([]string, error) {
return b.listDirectory(path, false)
}
func (b *S3FileBackend) ListDirectoryRecursively(path string) ([]string, error) {
return b.listDirectory(path, true)
}
func (b *S3FileBackend) RemoveDirectory(path string) error {
opts := s3.ListObjectsOptions{
Prefix: filepath.Join(b.pathPrefix, path),
Recursive: true,
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
list := b.client.ListObjects(ctx, b.bucket, opts)
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
objectsCh := b.client.RemoveObjects(ctx2, b.bucket, getPathsFromObjectInfos(list), s3.RemoveObjectsOptions{})
for err := range objectsCh {
if err.Err != nil {
return errors.Wrapf(err.Err, "unable to remove the directory %s", path)
}
}
return nil
}
func s3PutOptions(encrypted bool, contentType string) s3.PutObjectOptions {
options := s3.PutObjectOptions{}
if encrypted {
options.ServerSideEncryption = encrypt.NewSSE()
}
options.ContentType = contentType
// We set the part size to the minimum allowed value of 5MBs
// to avoid an excessive allocation in minio.PutObject implementation.
options.PartSize = 1024 * 1024 * 5
return options
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package i18n
import (
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/go-i18n/i18n/bundle"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const defaultLocale = "en"
// TranslateFunc is the type of the translate functions
type TranslateFunc func(translationID string, args ...any) string
// TranslationFuncByLocal is the type of function that takes local as a string and returns the translation function
type TranslationFuncByLocal func(locale string) TranslateFunc
// T is the translate function using the default server language as fallback language
var T TranslateFunc
// TDefault is the translate function using english as fallback language
var TDefault TranslateFunc
var locales map[string]string = make(map[string]string)
var defaultServerLocale string
var defaultClientLocale string
// TranslationsPreInit loads translations from filesystem if they are not
// loaded already and assigns english while loading server config
func TranslationsPreInit(translationsDir string) error {
if T != nil {
return nil
}
// Set T even if we fail to load the translations. Lots of shutdown handling code will
// segfault trying to handle the error, and the untranslated IDs are strictly better.
T = tfuncWithFallback(defaultLocale)
TDefault = tfuncWithFallback(defaultLocale)
return initTranslationsWithDir(translationsDir)
}
// InitTranslations set the defaults configured in the server and initialize
// the T function using the server default as fallback language
func InitTranslations(serverLocale, clientLocale string) error {
defaultServerLocale = serverLocale
defaultClientLocale = clientLocale
var err error
T, err = getTranslationsBySystemLocale()
return err
}
func initTranslationsWithDir(dir string) error {
files, _ := os.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) == ".json" {
filename := f.Name()
locales[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
if err := i18n.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
return err
}
}
}
return nil
}
// GetTranslationFuncForDir loads translations from the filesystem into a new instance of the bundle.
// It returns a function to access loaded translations.
func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) {
var availableLocals map[string]string = make(map[string]string)
bundle := bundle.New()
files, _ := os.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) != ".json" {
continue
}
filename := f.Name()
availableLocals[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
if err := bundle.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
return nil, err
}
}
return func(locale string) TranslateFunc {
if _, ok := availableLocals[locale]; !ok {
locale = defaultLocale
}
t, _ := bundle.Tfunc(locale)
return func(translationID string, args ...any) string {
if translated := t(translationID, args...); translated != translationID {
return translated
}
t, _ := bundle.Tfunc(defaultLocale)
return t(translationID, args...)
}
}, nil
}
func getTranslationsBySystemLocale() (TranslateFunc, error) {
locale := defaultServerLocale
if _, ok := locales[locale]; !ok {
mlog.Warn("Failed to load system translations for", mlog.String("locale", locale), mlog.String("attempting to fall back to default locale", defaultLocale))
locale = defaultLocale
}
if locales[locale] == "" {
return nil, fmt.Errorf("failed to load system translations for '%v'", defaultLocale)
}
translations := tfuncWithFallback(locale)
if translations == nil {
return nil, fmt.Errorf("failed to load system translations")
}
mlog.Info("Loaded system translations", mlog.String("for locale", locale), mlog.String("from locale", locales[locale]))
return translations, nil
}
// GetUserTranslations get the translation function for an specific locale
func GetUserTranslations(locale string) TranslateFunc {
if _, ok := locales[locale]; !ok {
locale = defaultLocale
}
translations := tfuncWithFallback(locale)
return translations
}
// GetTranslationsAndLocaleFromRequest return the translation function and the
// locale based on a request headers
func GetTranslationsAndLocaleFromRequest(r *http.Request) (TranslateFunc, string) {
// This is for checking against locales like pt_BR or zn_CN
headerLocaleFull := strings.Split(r.Header.Get("Accept-Language"), ",")[0]
// This is for checking against locales like en, es
headerLocale := strings.Split(strings.Split(r.Header.Get("Accept-Language"), ",")[0], "-")[0]
defaultLocale := defaultClientLocale
if locales[headerLocaleFull] != "" {
translations := tfuncWithFallback(headerLocaleFull)
return translations, headerLocaleFull
} else if locales[headerLocale] != "" {
translations := tfuncWithFallback(headerLocale)
return translations, headerLocale
} else if locales[defaultLocale] != "" {
translations := tfuncWithFallback(defaultLocale)
return translations, headerLocale
}
translations := tfuncWithFallback(defaultLocale)
return translations, defaultLocale
}
// GetSupportedLocales return a map of locale code and the file path with the
// translations
func GetSupportedLocales() map[string]string {
return locales
}
func tfuncWithFallback(pref string) TranslateFunc {
t, _ := i18n.Tfunc(pref)
return func(translationID string, args ...any) string {
if translated := t(translationID, args...); translated != translationID {
return translated
}
t, _ := i18n.Tfunc(defaultLocale)
return t(translationID, args...)
}
}
// TranslateAsHTML translates the translationID provided and return a
// template.HTML object
func TranslateAsHTML(t TranslateFunc, translationID string, args map[string]any) template.HTML {
message := t(translationID, escapeForHTML(args))
message = strings.Replace(message, "[[", "<strong>", -1)
message = strings.Replace(message, "]]", "</strong>", -1)
return template.HTML(message)
}
func escapeForHTML(arg any) any {
switch typedArg := arg.(type) {
case string:
return template.HTMLEscapeString(typedArg)
case *string:
return template.HTMLEscapeString(*typedArg)
case map[string]any:
safeArg := make(map[string]any, len(typedArg))
for key, value := range typedArg {
safeArg[key] = escapeForHTML(value)
}
return safeArg
default:
mlog.Warn(
"Unable to escape value for HTML template",
mlog.Any("html_template", arg),
mlog.String("template_type", reflect.ValueOf(arg).Type().String()),
)
return ""
}
}
// IdentityTfunc returns a translation function that don't translate, only
// returns the same id
func IdentityTfunc() TranslateFunc {
return func(translationID string, args ...any) string {
return translationID
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mail
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
InbucketAPI = "/api/v1/mailbox/"
)
// OutputJSONHeader holds the received Header to test sending emails (inbucket)
type JSONMessageHeaderInbucket []struct {
Mailbox string
ID string `json:"Id"`
From, Subject, Date string
To []string
Size int
}
// OutputJSONMessage holds the received Message fto test sending emails (inbucket)
type JSONMessageInbucket struct {
Mailbox string
ID string `json:"Id"`
From, Subject, Date string
Size int
Header map[string][]string
Body struct {
Text string
HTML string `json:"Html"`
}
Attachments []struct {
Filename string
ContentType string `json:"content-type"`
DownloadLink string `json:"download-link"`
Bytes []byte `json:"-"`
}
}
func ParseEmail(email string) string {
pos := strings.Index(email, "@")
parsedEmail := email[0:pos]
return parsedEmail
}
func GetMailBox(email string) (results JSONMessageHeaderInbucket, err error) {
parsedEmail := ParseEmail(email)
url := fmt.Sprintf("%s%s%s", getInbucketHost(), InbucketAPI, parsedEmail)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
if resp.Body == nil {
return nil, fmt.Errorf("no mailbox")
}
var record JSONMessageHeaderInbucket
err = json.NewDecoder(resp.Body).Decode(&record)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
if len(record) == 0 {
return nil, fmt.Errorf("no mailbox")
}
return record, nil
}
func GetMessageFromMailbox(email, id string) (JSONMessageInbucket, error) {
parsedEmail := ParseEmail(email)
var record JSONMessageInbucket
url := fmt.Sprintf("%s%s%s/%s", getInbucketHost(), InbucketAPI, parsedEmail, id)
emailResponse, err := http.Get(url)
if err != nil {
return record, err
}
defer func() {
io.Copy(io.Discard, emailResponse.Body)
emailResponse.Body.Close()
}()
if err = json.NewDecoder(emailResponse.Body).Decode(&record); err != nil {
return record, err
}
// download attachments
if record.Attachments != nil && len(record.Attachments) > 0 {
for i := range record.Attachments {
var bytes []byte
bytes, err = downloadAttachment(record.Attachments[i].DownloadLink)
if err != nil {
return record, err
}
record.Attachments[i].Bytes = make([]byte, len(bytes))
copy(record.Attachments[i].Bytes, bytes)
}
}
return record, err
}
func downloadAttachment(url string) ([]byte, error) {
attachmentResponse, err := http.Get(url)
if err != nil {
return nil, err
}
defer attachmentResponse.Body.Close()
buf := new(bytes.Buffer)
io.Copy(buf, attachmentResponse.Body)
return buf.Bytes(), nil
}
func DeleteMailBox(email string) (err error) {
parsedEmail := ParseEmail(email)
url := fmt.Sprintf("%s%s%s", getInbucketHost(), InbucketAPI, parsedEmail)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func RetryInbucket(attempts int, callback func() error) (err error) {
for i := 0; ; i++ {
err = callback()
if err == nil {
return nil
}
if i >= (attempts - 1) {
break
}
time.Sleep(5 * time.Second)
fmt.Println("retrying...")
}
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}
func getInbucketHost() (host string) {
inbucket_host := os.Getenv("CI_INBUCKET_HOST")
if inbucket_host == "" {
inbucket_host = "localhost"
}
inbucket_port := os.Getenv("CI_INBUCKET_PORT")
if inbucket_port == "" {
inbucket_port = "9001"
}
return fmt.Sprintf("http://%s:%s", inbucket_host, inbucket_port)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mail
import (
"context"
"crypto/tls"
"fmt"
"io"
"mime"
"net"
"net/mail"
"net/smtp"
"time"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
gomail "gopkg.in/mail.v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TLS = "TLS"
StartTLS = "STARTTLS"
)
type SMTPConfig struct {
ConnectionSecurity string
SkipServerCertificateVerification bool
Hostname string
ServerName string
Server string
Port string
ServerTimeout int
Username string
Password string
EnableSMTPAuth bool
SendEmailNotifications bool
FeedbackName string
FeedbackEmail string
ReplyToAddress string
}
type mailData struct {
mimeTo string
smtpTo string
from mail.Address
cc string
replyTo mail.Address
subject string
htmlBody string
embeddedFiles map[string]io.Reader
mimeHeaders map[string]string
messageID string
inReplyTo string
references string
category string
}
// smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
type smtpClient interface {
Mail(string) error
Rcpt(string) error
Data() (io.WriteCloser, error)
}
func encodeRFC2047Word(s string) string {
return mime.BEncoding.Encode("utf-8", s)
}
type authChooser struct {
smtp.Auth
config *SMTPConfig
}
func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
smtpAddress := a.config.ServerName + ":" + a.config.Port
a.Auth = LoginAuth(a.config.Username, a.config.Password, smtpAddress)
for _, method := range server.Auth {
if method == "PLAIN" {
a.Auth = smtp.PlainAuth("", a.config.Username, a.config.Password, a.config.ServerName+":"+a.config.Port)
break
}
}
return a.Auth.Start(server)
}
type loginAuth struct {
username, password, host string
}
func LoginAuth(username, password, host string) smtp.Auth {
return &loginAuth{username, password, host}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name")
}
return "LOGIN", []byte{}, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, errors.New("Unknown fromServer")
}
}
return nil, nil
}
func ConnectToSMTPServerAdvanced(config *SMTPConfig) (net.Conn, error) {
var conn net.Conn
var err error
smtpAddress := config.Server + ":" + config.Port
dialer := &net.Dialer{
Timeout: time.Duration(config.ServerTimeout) * time.Second,
}
if config.ConnectionSecurity == TLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: config.SkipServerCertificateVerification,
ServerName: config.ServerName,
}
conn, err = tls.DialWithDialer(dialer, "tcp", smtpAddress, tlsconfig)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the SMTP server through TLS")
}
} else {
conn, err = dialer.Dial("tcp", smtpAddress)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
}
return conn, nil
}
func ConnectToSMTPServer(config *SMTPConfig) (net.Conn, error) {
return ConnectToSMTPServerAdvanced(config)
}
func NewSMTPClientAdvanced(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var c *smtp.Client
ec := make(chan error)
go func() {
var err error
c, err = smtp.NewClient(conn, config.ServerName+":"+config.Port)
if err != nil {
ec <- err
return
}
cancel()
}()
select {
case <-ctx.Done():
err := ctx.Err()
if err != nil && err.Error() != "context canceled" {
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
case err := <-ec:
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
if config.Hostname != "" {
err := c.Hello(config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "unable to send hello message")
}
}
if config.ConnectionSecurity == StartTLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: config.SkipServerCertificateVerification,
ServerName: config.ServerName,
}
c.StartTLS(tlsconfig)
}
if config.EnableSMTPAuth {
if err := c.Auth(&authChooser{config: config}); err != nil {
return nil, errors.Wrap(err, "authentication failed")
}
}
return c, nil
}
func NewSMTPClient(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
return NewSMTPClientAdvanced(
ctx,
conn,
config,
)
}
func TestConnection(config *SMTPConfig) error {
conn, err := ConnectToSMTPServer(config)
if err != nil {
return errors.Wrap(err, "unable to connect")
}
defer conn.Close()
sec := config.ServerTimeout
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
defer cancel()
c, err := NewSMTPClient(ctx, conn, config)
if err != nil {
return errors.Wrap(err, "unable to connect")
}
c.Close()
c.Quit()
return nil
}
func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail string, category string) error {
fromMail := mail.Address{Name: config.FeedbackName, Address: config.FeedbackEmail}
replyTo := mail.Address{Name: config.FeedbackName, Address: config.ReplyToAddress}
mail := mailData{
mimeTo: to,
smtpTo: to,
from: fromMail,
cc: ccMail,
replyTo: replyTo,
subject: subject,
htmlBody: htmlBody,
embeddedFiles: embeddedFiles,
messageID: messageID,
inReplyTo: inReplyTo,
references: references,
category: category,
}
return sendMailUsingConfigAdvanced(mail, config)
}
func SendMailUsingConfig(to, subject, htmlBody string, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail, category string) error {
return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, messageID, inReplyTo, references, ccMail, category)
}
// allows for sending an email with differing MIME/SMTP recipients
func sendMailUsingConfigAdvanced(mail mailData, config *SMTPConfig) error {
if config.Server == "" {
return nil
}
conn, err := ConnectToSMTPServer(config)
if err != nil {
return err
}
defer conn.Close()
sec := config.ServerTimeout
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
defer cancel()
c, err := NewSMTPClient(ctx, conn, config)
if err != nil {
return err
}
defer c.Quit()
defer c.Close()
return sendMail(c, mail, time.Now(), config)
}
const SendGridXSMTPAPIHeader = "X-SMTPAPI"
func sendMail(c smtpClient, mail mailData, date time.Time, config *SMTPConfig) error {
mlog.Debug("sending mail", mlog.String("to", mail.smtpTo), mlog.String("subject", mail.subject))
htmlMessage := mail.htmlBody
txtBody, err := html2text.FromString(mail.htmlBody)
if err != nil {
mlog.Warn("Unable to convert html body to text", mlog.Err(err))
txtBody = ""
}
headers := map[string][]string{
"From": {mail.from.String()},
"To": {mail.mimeTo},
"Subject": {encodeRFC2047Word(mail.subject)},
"Content-Transfer-Encoding": {"8bit"},
"Auto-Submitted": {"auto-generated"},
"Precedence": {"bulk"},
}
if mail.category != "" {
sendgridHeader := fmt.Sprintf(`{"category": %q}`, mail.category)
headers[SendGridXSMTPAPIHeader] = []string{sendgridHeader}
}
if mail.replyTo.Address != "" {
headers["Reply-To"] = []string{mail.replyTo.String()}
}
if mail.cc != "" {
headers["CC"] = []string{mail.cc}
}
if mail.messageID != "" {
headers["Message-ID"] = []string{mail.messageID}
} else {
randomStringLength := 16
msgID := fmt.Sprintf("<%s-%d@%s>", model.NewRandomString(randomStringLength), time.Now().Unix(), config.Hostname)
headers["Message-ID"] = []string{msgID}
}
if mail.inReplyTo != "" {
headers["In-Reply-To"] = []string{mail.inReplyTo}
}
if mail.references != "" {
headers["References"] = []string{mail.references}
}
for k, v := range mail.mimeHeaders {
headers[k] = []string{encodeRFC2047Word(v)}
}
m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
m.SetHeaders(headers)
m.SetDateHeader("Date", date)
m.SetBody("text/plain", txtBody)
m.AddAlternative("text/html", htmlMessage)
for name, reader := range mail.embeddedFiles {
m.EmbedReader(name, reader)
}
if err = c.Mail(mail.from.Address); err != nil {
return errors.Wrap(err, "failed to set the from address")
}
if err = c.Rcpt(mail.smtpTo); err != nil {
return errors.Wrap(err, "failed to set the to address")
}
w, err := c.Data()
if err != nil {
return errors.Wrap(err, "failed to add email message data")
}
_, err = m.WriteTo(w)
if err != nil {
return errors.Wrap(err, "failed to write the email message")
}
err = w.Close()
if err != nil {
return errors.Wrap(err, "failed to close connection to the SMTP server")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// Based off of extensions/autolink.c from https://github.com/github/cmark
var (
DefaultURLSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
wwwAutoLinkRegex = regexp.MustCompile(`^www\d{0,3}\.`)
)
// Given a string with a w at the given position, tries to parse and return a range containing a www link.
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
// www_match from the reference code.
func parseWWWAutolink(data string, position int) (Range, bool) {
// Check that this isn't part of another word
if position > 1 {
prevChar := data[position-1]
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
return Range{}, false
}
}
// Check that this starts with www
if len(data)-position < 4 || !wwwAutoLinkRegex.MatchString(data[position:]) {
return Range{}, false
}
end := checkDomain(data[position:], false)
if end == 0 {
return Range{}, false
}
end += position
// Grab all text until the end of the string or the next whitespace character
for end < len(data) && !isWhitespaceByte(data[end]) {
end += 1
}
// Trim trailing punctuation
end = trimTrailingCharactersFromLink(data, position, end)
if position == end {
return Range{}, false
}
return Range{position, end}, true
}
func isAllowedBeforeWWWLink(c byte) bool {
switch c {
case '*', '_', '~', ')':
return true
}
return false
}
// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
// url_match from the reference code.
func parseURLAutolink(data string, position int) (Range, bool) {
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
return Range{}, false
}
start := position - 1
for start > 0 && isAlphanumericByte(data[start-1]) {
start -= 1
}
if start < 0 || position >= len(data) {
return Range{}, false
}
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
scheme := data[start:position]
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
return Range{}, false
}
end := checkDomain(data[position+3:], true)
if end == 0 {
return Range{}, false
}
end += position
// Grab all text until the end of the string or the next whitespace character
for end < len(data) && !isWhitespaceByte(data[end]) {
end += 1
}
// Trim trailing punctuation
end = trimTrailingCharactersFromLink(data, start, end)
if start == end {
return Range{}, false
}
return Range{start, end}, true
}
func isSchemeAllowed(scheme string) bool {
// Note that this doesn't support the custom URL schemes implemented by the client
for _, allowed := range DefaultURLSchemes {
if strings.EqualFold(allowed, scheme) {
return true
}
}
return false
}
// Given a string starting with a URL, returns the number of valid characters that make up the URL's domain.
// Returns 0 if the string doesn't start with a domain name. allowShort determines whether or not the domain
// needs to contain a period to be considered valid. Equivalent to check_domain from the reference code.
func checkDomain(data string, allowShort bool) int {
foundUnderscore := false
foundPeriod := false
i := 1
for ; i < len(data)-1; i++ {
if data[i] == '_' {
foundUnderscore = true
break
} else if data[i] == '.' {
foundPeriod = true
} else if !isValidHostCharacter(data[i:]) && data[i] != '-' {
break
}
}
if foundUnderscore {
return 0
}
if allowShort {
// If allowShort is set, accept any string of valid domain characters
return i
}
// If allowShort isn't set, a valid domain just requires at least a single period. Note that this
// logic isn't entirely necessary because we already know the string starts with "www." when
// this is called from parseWWWAutolink
if foundPeriod {
return i
}
return 0
}
// Returns true if the provided link starts with a valid character for a domain name. Equivalent to
// is_valid_hostchar from the reference code.
func isValidHostCharacter(link string) bool {
c, _ := utf8.DecodeRuneInString(link)
if c == utf8.RuneError {
return false
}
return !unicode.IsSpace(c) && !unicode.IsPunct(c)
}
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
// Returns a new end position for the link. Equivalent to autolink_delim from the reference code.
func trimTrailingCharactersFromLink(markdown string, start int, end int) int {
runes := []rune(markdown[start:end])
linkEnd := len(runes)
// Cut off the link before an open angle bracket if it contains one
for i, c := range runes {
if c == '<' {
linkEnd = i
break
}
}
for linkEnd > 0 {
c := runes[linkEnd-1]
if !canEndAutolink(c) {
// Trim trailing quotes, periods, etc
linkEnd = linkEnd - 1
} else if c == ';' {
// Trim a trailing HTML entity
newEnd := linkEnd - 2
for newEnd > 0 && ((runes[newEnd] >= 'a' && runes[newEnd] <= 'z') || (runes[newEnd] >= 'A' && runes[newEnd] <= 'Z')) {
newEnd -= 1
}
if newEnd < linkEnd-2 && runes[newEnd] == '&' {
linkEnd = newEnd
} else {
// This isn't actually an HTML entity, so just trim the semicolon
linkEnd = linkEnd - 1
}
} else if c == ')' {
// Only allow an autolink ending with a bracket if that bracket is part of a matching pair of brackets.
// If there are more closing brackets than opening ones, remove the extra bracket
numClosing := 0
numOpening := 0
// Examples (input text => output linked portion):
//
// http://www.pokemon.com/Pikachu_(Electric)
// => http://www.pokemon.com/Pikachu_(Electric)
//
// http://www.pokemon.com/Pikachu_((Electric)
// => http://www.pokemon.com/Pikachu_((Electric)
//
// http://www.pokemon.com/Pikachu_(Electric))
// => http://www.pokemon.com/Pikachu_(Electric)
//
// http://www.pokemon.com/Pikachu_((Electric))
// => http://www.pokemon.com/Pikachu_((Electric))
for i := 0; i < linkEnd; i++ {
if runes[i] == '(' {
numOpening += 1
} else if runes[i] == ')' {
numClosing += 1
}
}
if numClosing <= numOpening {
// There's fewer or equal closing brackets, so we've found the end of the link
break
}
linkEnd -= 1
} else {
// There's no special characters at the end of the link, so we're at the end
break
}
}
return start + len(string(runes[:linkEnd]))
}
func canEndAutolink(c rune) bool {
switch c {
case '?', '!', '.', ',', ':', '*', '_', '~', '\'', '"':
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type BlockQuote struct {
blockBase
markdown string
Children []Block
}
func (b *BlockQuote) Continuation(indentation int, r Range) *continuation {
if indentation > 3 {
return nil
}
s := b.markdown[r.Position:r.End]
if s == "" || s[0] != '>' {
return nil
}
remaining := Range{r.Position + 1, r.End}
indentation, indentationBytes := countIndentation(b.markdown, remaining)
if indentation > 0 {
indentation--
}
return &continuation{
Indentation: indentation,
Remaining: Range{remaining.Position + indentationBytes, remaining.End},
}
}
func (b *BlockQuote) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
return openBlocks
}
func blockQuoteStart(markdown string, indent int, r Range) []Block {
if indent > 3 {
return nil
}
s := markdown[r.Position:r.End]
if s == "" || s[0] != '>' {
return nil
}
block := &BlockQuote{
markdown: markdown,
}
r.Position++
if len(s) > 1 && s[1] == ' ' {
r.Position++
}
indent, bytes := countIndentation(markdown, r)
ret := []Block{block}
if descendants := blockStartOrParagraph(markdown, indent, Range{r.Position + bytes, r.End}, nil, nil); descendants != nil {
block.Children = append(block.Children, descendants[0])
ret = append(ret, descendants...)
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type continuation struct {
Indentation int
Remaining Range
}
type Block interface {
Continuation(indentation int, r Range) *continuation
AddLine(indentation int, r Range) bool
Close()
AllowsBlockStarts() bool
HasTrailingBlankLine() bool
}
type blockBase struct{}
func (*blockBase) AddLine(indentation int, r Range) bool { return false }
func (*blockBase) Close() {}
func (*blockBase) AllowsBlockStarts() bool { return true }
func (*blockBase) HasTrailingBlankLine() bool { return false }
type ContainerBlock interface {
Block
AddChild(openBlocks []Block) []Block
}
type Range struct {
Position int
End int
}
func closeBlocks(blocks []Block, referenceDefinitions []*ReferenceDefinition) []*ReferenceDefinition {
for _, block := range blocks {
block.Close()
if p, ok := block.(*Paragraph); ok && len(p.ReferenceDefinitions) > 0 {
referenceDefinitions = append(referenceDefinitions, p.ReferenceDefinitions...)
}
}
return referenceDefinitions
}
func ParseBlocks(markdown string, lines []Line) (*Document, []*ReferenceDefinition) {
document := &Document{}
var referenceDefinitions []*ReferenceDefinition
openBlocks := []Block{document}
for _, line := range lines {
r := line.Range
lastMatchIndex := 0
indentation, indentationBytes := countIndentation(markdown, r)
r = Range{r.Position + indentationBytes, r.End}
for i, block := range openBlocks {
if continuation := block.Continuation(indentation, r); continuation != nil {
indentation = continuation.Indentation
r = continuation.Remaining
additionalIndentation, additionalIndentationBytes := countIndentation(markdown, r)
r = Range{r.Position + additionalIndentationBytes, r.End}
indentation += additionalIndentation
lastMatchIndex = i
} else {
break
}
}
if openBlocks[lastMatchIndex].AllowsBlockStarts() {
if newBlocks := blockStart(markdown, indentation, r, openBlocks[:lastMatchIndex+1], openBlocks[lastMatchIndex+1:]); newBlocks != nil {
didAdd := false
for i := lastMatchIndex; i >= 0; i-- {
if container, ok := openBlocks[i].(ContainerBlock); ok {
if addedBlocks := container.AddChild(newBlocks); addedBlocks != nil {
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
openBlocks = openBlocks[:i+1]
openBlocks = append(openBlocks, addedBlocks...)
didAdd = true
break
}
}
}
if didAdd {
continue
}
}
}
isBlank := strings.TrimSpace(markdown[r.Position:r.End]) == ""
if paragraph, ok := openBlocks[len(openBlocks)-1].(*Paragraph); ok && !isBlank {
paragraph.Text = append(paragraph.Text, r)
continue
}
referenceDefinitions = closeBlocks(openBlocks[lastMatchIndex+1:], referenceDefinitions)
openBlocks = openBlocks[:lastMatchIndex+1]
if openBlocks[lastMatchIndex].AddLine(indentation, r) {
continue
}
if paragraph := newParagraph(markdown, r); paragraph != nil {
for i := lastMatchIndex; i >= 0; i-- {
if container, ok := openBlocks[i].(ContainerBlock); ok {
if newBlocks := container.AddChild([]Block{paragraph}); newBlocks != nil {
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
openBlocks = openBlocks[:i+1]
openBlocks = append(openBlocks, newBlocks...)
break
}
}
}
}
}
referenceDefinitions = closeBlocks(openBlocks, referenceDefinitions)
return document, referenceDefinitions
}
func blockStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if r.Position >= r.End {
return nil
}
if start := blockQuoteStart(markdown, indentation, r); start != nil {
return start
} else if start := listStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
} else if start := indentedCodeStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
} else if start := fencedCodeStart(markdown, indentation, r); start != nil {
return start
}
return nil
}
func blockStartOrParagraph(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if start := blockStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
}
if paragraph := newParagraph(markdown, r); paragraph != nil {
return []Block{paragraph}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type Document struct {
blockBase
Children []Block
}
func (b *Document) Continuation(indentation int, r Range) *continuation {
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *Document) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
return openBlocks
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type FencedCodeLine struct {
Indentation int
Range Range
}
type FencedCode struct {
blockBase
markdown string
didSeeClosingFence bool
Indentation int
OpeningFence Range
RawInfo Range
RawCode []FencedCodeLine
}
func (b *FencedCode) Code() (result string) {
for _, code := range b.RawCode {
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
}
return
}
func (b *FencedCode) Info() string {
return Unescape(b.markdown[b.RawInfo.Position:b.RawInfo.End])
}
func (b *FencedCode) Continuation(indentation int, r Range) *continuation {
if b.didSeeClosingFence {
return nil
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *FencedCode) AddLine(indentation int, r Range) bool {
s := b.markdown[r.Position:r.End]
if indentation <= 3 && strings.HasPrefix(s, b.markdown[b.OpeningFence.Position:b.OpeningFence.End]) {
suffix := strings.TrimSpace(s[b.OpeningFence.End-b.OpeningFence.Position:])
isClosingFence := true
for _, c := range suffix {
if c != rune(s[0]) {
isClosingFence = false
break
}
}
if isClosingFence {
b.didSeeClosingFence = true
return true
}
}
if indentation >= b.Indentation {
indentation -= b.Indentation
} else {
indentation = 0
}
b.RawCode = append(b.RawCode, FencedCodeLine{
Indentation: indentation,
Range: r,
})
return true
}
func (b *FencedCode) AllowsBlockStarts() bool {
return false
}
func fencedCodeStart(markdown string, indentation int, r Range) []Block {
s := markdown[r.Position:r.End]
if !strings.HasPrefix(s, "```") && !strings.HasPrefix(s, "~~~") {
return nil
}
fenceCharacter := rune(s[0])
fenceLength := 3
for _, c := range s[3:] {
if c == fenceCharacter {
fenceLength++
} else {
break
}
}
for i := r.Position + fenceLength; i < r.End; i++ {
if markdown[i] == '`' {
return nil
}
}
return []Block{
&FencedCode{
markdown: markdown,
Indentation: indentation,
RawInfo: trimRightSpace(markdown, Range{r.Position + fenceLength, r.End}),
OpeningFence: Range{r.Position, r.Position + fenceLength},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"fmt"
"strings"
)
var htmlEscaper = strings.NewReplacer(
`&`, "&",
`<`, "<",
`>`, ">",
`"`, """,
)
// RenderHTML produces HTML with the same behavior as the example renderer used in the CommonMark
// reference materials except for one slight difference: for brevity, no unnecessary whitespace is
// inserted between elements. The output is not defined by the CommonMark spec, and it exists
// primarily as an aid in testing.
func RenderHTML(markdown string) string {
return RenderBlockHTML(Parse(markdown))
}
func RenderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition) (result string) {
return renderBlockHTML(block, referenceDefinitions, false)
}
func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) (result string) {
switch v := block.(type) {
case *Document:
for _, block := range v.Children {
result += RenderBlockHTML(block, referenceDefinitions)
}
case *Paragraph:
if len(v.Text) == 0 {
return
}
if !isTightList {
result += "<p>"
}
for _, inline := range v.ParseInlines(referenceDefinitions) {
result += RenderInlineHTML(inline)
}
if !isTightList {
result += "</p>"
}
case *List:
if v.IsOrdered {
if v.OrderedStart != 1 {
result += fmt.Sprintf(`<ol start="%v">`, v.OrderedStart)
} else {
result += "<ol>"
}
} else {
result += "<ul>"
}
for _, block := range v.Children {
result += renderBlockHTML(block, referenceDefinitions, !v.IsLoose)
}
if v.IsOrdered {
result += "</ol>"
} else {
result += "</ul>"
}
case *ListItem:
result += "<li>"
for _, block := range v.Children {
result += renderBlockHTML(block, referenceDefinitions, isTightList)
}
result += "</li>"
case *BlockQuote:
result += "<blockquote>"
for _, block := range v.Children {
result += RenderBlockHTML(block, referenceDefinitions)
}
result += "</blockquote>"
case *FencedCode:
if info := v.Info(); info != "" {
language := strings.Fields(info)[0]
result += `<pre><code class="language-` + htmlEscaper.Replace(language) + `">`
} else {
result += "<pre><code>"
}
result += htmlEscaper.Replace(v.Code()) + "</code></pre>"
case *IndentedCode:
result += "<pre><code>" + htmlEscaper.Replace(v.Code()) + "</code></pre>"
default:
panic(fmt.Sprintf("missing case for type %T", v))
}
return
}
func escapeURL(url string) (result string) {
for i := 0; i < len(url); {
switch b := url[i]; b {
case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#':
result += string(b)
i++
default:
if b == '%' && i+2 < len(url) && isHexByte(url[i+1]) && isHexByte(url[i+2]) {
result += url[i : i+3]
i += 3
} else if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') {
result += string(b)
i++
} else {
result += fmt.Sprintf("%%%0X", b)
i++
}
}
}
return
}
func RenderInlineHTML(inline Inline) (result string) {
switch v := inline.(type) {
case *Text:
return htmlEscaper.Replace(v.Text)
case *HardLineBreak:
return "<br />"
case *SoftLineBreak:
return "\n"
case *CodeSpan:
return "<code>" + htmlEscaper.Replace(v.Code) + "</code>"
case *InlineImage:
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += ` />`
case *ReferenceImage:
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += ` />`
case *InlineLink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += `>`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
case *ReferenceLink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += `>`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
case *Autolink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `">`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
default:
panic(fmt.Sprintf("missing case for type %T", v))
}
return
}
func renderImageAltText(children []Inline) (result string) {
for _, inline := range children {
result += renderImageChildAltText(inline)
}
return
}
func renderImageChildAltText(inline Inline) (result string) {
switch v := inline.(type) {
case *Text:
return v.Text
case *InlineImage:
for _, inline := range v.Children {
result += renderImageChildAltText(inline)
}
case *InlineLink:
for _, inline := range v.Children {
result += renderImageChildAltText(inline)
}
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type IndentedCodeLine struct {
Indentation int
Range Range
}
type IndentedCode struct {
blockBase
markdown string
RawCode []IndentedCodeLine
}
func (b *IndentedCode) Code() (result string) {
for _, code := range b.RawCode {
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
}
return
}
func (b *IndentedCode) Continuation(indentation int, r Range) *continuation {
if indentation >= 4 {
return &continuation{
Indentation: indentation - 4,
Remaining: r,
}
}
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return &continuation{
Remaining: r,
}
}
return nil
}
func (b *IndentedCode) AddLine(indentation int, r Range) bool {
b.RawCode = append(b.RawCode, IndentedCodeLine{
Indentation: indentation,
Range: r,
})
return true
}
func (b *IndentedCode) Close() {
for {
last := b.RawCode[len(b.RawCode)-1]
s := b.markdown[last.Range.Position:last.Range.End]
if strings.TrimRight(s, "\r\n") == "" {
b.RawCode = b.RawCode[:len(b.RawCode)-1]
} else {
break
}
}
}
func (b *IndentedCode) AllowsBlockStarts() bool {
return false
}
func indentedCodeStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if len(unmatchedBlocks) > 0 {
if _, ok := unmatchedBlocks[len(unmatchedBlocks)-1].(*Paragraph); ok {
return nil
}
} else if len(matchedBlocks) > 0 {
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
return nil
}
}
if indentation < 4 {
return nil
}
s := markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return []Block{
&IndentedCode{
markdown: markdown,
RawCode: []IndentedCodeLine{{
Indentation: indentation - 4,
Range: r,
}},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"container/list"
"strings"
"unicode"
"unicode/utf8"
)
type Inline interface {
IsInline() bool
}
type inlineBase struct{}
func (inlineBase) IsInline() bool { return true }
type Text struct {
inlineBase
Text string
Range Range
}
type CodeSpan struct {
inlineBase
Code string
}
type HardLineBreak struct {
inlineBase
}
type SoftLineBreak struct {
inlineBase
}
type InlineLinkOrImage struct {
inlineBase
Children []Inline
RawDestination Range
markdown string
rawTitle string
}
func (i *InlineLinkOrImage) Destination() string {
return Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
}
func (i *InlineLinkOrImage) Title() string {
return Unescape(i.rawTitle)
}
type InlineLink struct {
InlineLinkOrImage
}
type InlineImage struct {
InlineLinkOrImage
}
type ReferenceLinkOrImage struct {
inlineBase
*ReferenceDefinition
Children []Inline
}
type ReferenceLink struct {
ReferenceLinkOrImage
}
type ReferenceImage struct {
ReferenceLinkOrImage
}
type Autolink struct {
inlineBase
Children []Inline
RawDestination Range
markdown string
}
func (i *Autolink) Destination() string {
destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
if strings.HasPrefix(destination, "www") {
destination = "http://" + destination
}
return destination
}
type delimiterType int
const (
linkOpeningDelimiter delimiterType = iota
imageOpeningDelimiter
)
type delimiter struct {
Type delimiterType
IsInactive bool
TextNode int
Range Range
}
type inlineParser struct {
markdown string
ranges []Range
referenceDefinitions []*ReferenceDefinition
raw string
position int
inlines []Inline
delimiterStack *list.List
}
func newInlineParser(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) *inlineParser {
return &inlineParser{
markdown: markdown,
ranges: ranges,
referenceDefinitions: referenceDefinitions,
delimiterStack: list.New(),
}
}
func (p *inlineParser) parseBackticks() {
count := 1
for i := p.position + 1; i < len(p.raw) && p.raw[i] == '`'; i++ {
count++
}
opening := p.raw[p.position : p.position+count]
search := p.position + count
for search < len(p.raw) {
end := strings.Index(p.raw[search:], opening)
if end == -1 {
break
}
if search+end+count < len(p.raw) && p.raw[search+end+count] == '`' {
search += end + count
for search < len(p.raw) && p.raw[search] == '`' {
search++
}
continue
}
code := strings.Join(strings.Fields(p.raw[p.position+count:search+end]), " ")
p.position = search + end + count
p.inlines = append(p.inlines, &CodeSpan{
Code: code,
})
return
}
p.position += len(opening)
absPos := relativeToAbsolutePosition(p.ranges, p.position-len(opening))
p.inlines = append(p.inlines, &Text{
Text: opening,
Range: Range{absPos, absPos + len(opening)},
})
}
func (p *inlineParser) parseLineEnding() {
if p.position >= 1 && p.raw[p.position-1] == '\t' {
p.inlines = append(p.inlines, &HardLineBreak{})
} else if p.position >= 2 && p.raw[p.position-1] == ' ' && (p.raw[p.position-2] == '\t' || p.raw[p.position-1] == ' ') {
p.inlines = append(p.inlines, &HardLineBreak{})
} else {
p.inlines = append(p.inlines, &SoftLineBreak{})
}
p.position++
if p.position < len(p.raw) && p.raw[p.position] == '\n' {
p.position++
}
}
func (p *inlineParser) parseEscapeCharacter() {
if p.position+1 < len(p.raw) && isEscapableByte(p.raw[p.position+1]) {
absPos := relativeToAbsolutePosition(p.ranges, p.position+1)
p.inlines = append(p.inlines, &Text{
Text: string(p.raw[p.position+1]),
Range: Range{absPos, absPos + len(string(p.raw[p.position+1]))},
})
p.position += 2
} else {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: `\`,
Range: Range{absPos, absPos + 1},
})
p.position++
}
}
func (p *inlineParser) parseText() {
if next := strings.IndexAny(p.raw[p.position:], "\r\n\\`&![]wW:"); next == -1 {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: strings.TrimRightFunc(p.raw[p.position:], isWhitespace),
Range: Range{absPos, absPos + len(p.raw[p.position:])},
})
p.position = len(p.raw)
} else {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
if p.raw[p.position+next] == '\r' || p.raw[p.position+next] == '\n' {
s := strings.TrimRightFunc(p.raw[p.position:p.position+next], isWhitespace)
p.inlines = append(p.inlines, &Text{
Text: s,
Range: Range{absPos, absPos + len(s)},
})
} else {
if next == 0 {
// Always read at least one character since 'w', 'W', and ':' may not actually match another
// type of node
next = 1
}
p.inlines = append(p.inlines, &Text{
Text: p.raw[p.position : p.position+next],
Range: Range{absPos, absPos + next},
})
}
p.position += next
}
}
func (p *inlineParser) parseLinkOrImageDelimiter() {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
if p.raw[p.position] == '[' {
p.inlines = append(p.inlines, &Text{
Text: "[",
Range: Range{absPos, absPos + 1},
})
p.delimiterStack.PushBack(&delimiter{
Type: linkOpeningDelimiter,
TextNode: len(p.inlines) - 1,
Range: Range{p.position, p.position + 1},
})
p.position++
} else if p.raw[p.position] == '!' && p.position+1 < len(p.raw) && p.raw[p.position+1] == '[' {
p.inlines = append(p.inlines, &Text{
Text: "![",
Range: Range{absPos, absPos + 2},
})
p.delimiterStack.PushBack(&delimiter{
Type: imageOpeningDelimiter,
TextNode: len(p.inlines) - 1,
Range: Range{p.position, p.position + 2},
})
p.position += 2
} else {
p.inlines = append(p.inlines, &Text{
Text: "!",
Range: Range{absPos, absPos + 1},
})
p.position++
}
}
func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) {
if position >= len(p.raw) || p.raw[position] != '(' {
return
}
position++
destinationStart := nextNonWhitespace(p.raw, position)
if destinationStart >= len(p.raw) {
return
} else if p.raw[destinationStart] == ')' {
return Range{destinationStart, destinationStart}, Range{destinationStart, destinationStart}, destinationStart + 1, true
}
destination, end, ok = parseLinkDestination(p.raw, destinationStart)
if !ok {
return
}
position = end
if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
dimensionsStart := nextNonWhitespace(p.raw, position)
if dimensionsStart >= len(p.raw) {
return
}
if p.raw[dimensionsStart] == '=' {
// Read optional image dimensions even if we don't use them
_, end, ok = parseImageDimensions(p.raw, dimensionsStart)
if !ok {
return
}
position = end
}
}
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
titleStart := nextNonWhitespace(p.raw, position)
if titleStart >= len(p.raw) {
return
} else if p.raw[titleStart] == ')' {
return destination, Range{titleStart, titleStart}, titleStart + 1, true
}
if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
title, end, ok = parseLinkTitle(p.raw, titleStart)
if !ok {
return
}
position = end
}
}
closingPosition := nextNonWhitespace(p.raw, position)
if closingPosition >= len(p.raw) || p.raw[closingPosition] != ')' {
return Range{}, Range{}, 0, false
}
return destination, title, closingPosition + 1, true
}
func (p *inlineParser) referenceDefinition(label string) *ReferenceDefinition {
clean := strings.Join(strings.Fields(label), " ")
for _, d := range p.referenceDefinitions {
if strings.EqualFold(clean, strings.Join(strings.Fields(d.Label()), " ")) {
return d
}
}
return nil
}
func (p *inlineParser) lookForLinkOrImage() {
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
d := element.Value.(*delimiter)
if d.Type != imageOpeningDelimiter && d.Type != linkOpeningDelimiter {
continue
}
if d.IsInactive {
p.delimiterStack.Remove(element)
break
}
isImage := d.Type == imageOpeningDelimiter
var inline Inline
if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok {
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
linkOrImage := InlineLinkOrImage{
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
RawDestination: Range{destinationMarkdownPosition, destinationMarkdownPosition + destination.End - destination.Position},
markdown: p.markdown,
rawTitle: p.raw[title.Position:title.End],
}
if d.Type == imageOpeningDelimiter {
inline = &InlineImage{linkOrImage}
} else {
inline = &InlineLink{linkOrImage}
}
p.position = next
} else {
referenceLabel := ""
label, next, hasLinkLabel := parseLinkLabel(p.raw, p.position+1)
if hasLinkLabel && label.End > label.Position {
referenceLabel = p.raw[label.Position:label.End]
} else {
referenceLabel = p.raw[d.Range.End:p.position]
if !hasLinkLabel {
next = p.position + 1
}
}
if referenceLabel != "" {
if reference := p.referenceDefinition(referenceLabel); reference != nil {
linkOrImage := ReferenceLinkOrImage{
ReferenceDefinition: reference,
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
}
if d.Type == imageOpeningDelimiter {
inline = &ReferenceImage{linkOrImage}
} else {
inline = &ReferenceLink{linkOrImage}
}
p.position = next
}
}
}
if inline != nil {
if d.Type == imageOpeningDelimiter {
p.inlines = append(p.inlines[:d.TextNode], inline)
} else {
p.inlines = append(p.inlines[:d.TextNode], inline)
for inlineElement := element.Prev(); inlineElement != nil; inlineElement = inlineElement.Prev() {
if d := inlineElement.Value.(*delimiter); d.Type == linkOpeningDelimiter {
d.IsInactive = true
}
}
}
p.delimiterStack.Remove(element)
return
}
p.delimiterStack.Remove(element)
break
}
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: "]",
Range: Range{absPos, absPos + 1},
})
p.position++
}
func CharacterReference(ref string) string {
if ref == "" {
return ""
}
if ref[0] == '#' {
if len(ref) < 2 {
return ""
}
n := 0
if ref[1] == 'X' || ref[1] == 'x' {
if len(ref) < 3 {
return ""
}
for i := 2; i < len(ref); i++ {
if i > 9 {
return ""
}
d := ref[i]
switch {
case d >= '0' && d <= '9':
n = n*16 + int(d-'0')
case d >= 'a' && d <= 'f':
n = n*16 + 10 + int(d-'a')
case d >= 'A' && d <= 'F':
n = n*16 + 10 + int(d-'A')
default:
return ""
}
}
} else {
for i := 1; i < len(ref); i++ {
if i > 8 || ref[i] < '0' || ref[i] > '9' {
return ""
}
n = n*10 + int(ref[i]-'0')
}
}
c := rune(n)
if c == '\u0000' || !utf8.ValidRune(c) {
return string(unicode.ReplacementChar)
}
return string(c)
}
if entity, ok := htmlEntities[ref]; ok {
return entity
}
return ""
}
func (p *inlineParser) parseCharacterReference() {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.position++
if semicolon := strings.IndexByte(p.raw[p.position:], ';'); semicolon == -1 {
p.inlines = append(p.inlines, &Text{
Text: "&",
Range: Range{absPos, absPos + 1},
})
} else if s := CharacterReference(p.raw[p.position : p.position+semicolon]); s != "" {
p.position += semicolon + 1
p.inlines = append(p.inlines, &Text{
Text: s,
Range: Range{absPos, absPos + len(s)},
})
} else {
p.inlines = append(p.inlines, &Text{
Text: "&",
Range: Range{absPos, absPos + 1},
})
}
}
func (p *inlineParser) parseAutolink(c rune) bool {
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
d := element.Value.(*delimiter)
if !d.IsInactive {
return false
}
}
var link Range
if c == ':' {
var ok bool
link, ok = parseURLAutolink(p.raw, p.position)
if !ok {
return false
}
// Since the current position is at the colon, we have to rewind the parsing slightly so that
// we don't duplicate the URL scheme
rewind := strings.Index(p.raw[link.Position:link.End], ":")
if rewind != -1 {
lastInline := p.inlines[len(p.inlines)-1]
lastText, ok := lastInline.(*Text)
if !ok {
// This should never occur since parseURLAutolink will only return a non-empty value
// when the previous text ends in a valid URL protocol which would mean that the previous
// node is a Text node
return false
}
p.inlines = p.inlines[0 : len(p.inlines)-1]
p.inlines = append(p.inlines, &Text{
Text: lastText.Text[:len(lastText.Text)-rewind],
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
})
p.position -= rewind
}
} else if c == 'w' || c == 'W' {
var ok bool
link, ok = parseWWWAutolink(p.raw, p.position)
if !ok {
return false
}
}
linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position)
linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position}
p.inlines = append(p.inlines, &Autolink{
Children: []Inline{
&Text{
Text: p.raw[link.Position:link.End],
Range: linkRange,
},
},
RawDestination: linkRange,
markdown: p.markdown,
})
p.position += (link.End - link.Position)
return true
}
func (p *inlineParser) Parse() []Inline {
for _, r := range p.ranges {
p.raw += p.markdown[r.Position:r.End]
}
for p.position < len(p.raw) {
c, _ := utf8.DecodeRuneInString(p.raw[p.position:])
switch c {
case '\r', '\n':
p.parseLineEnding()
case '\\':
p.parseEscapeCharacter()
case '`':
p.parseBackticks()
case '&':
p.parseCharacterReference()
case '!', '[':
p.parseLinkOrImageDelimiter()
case ']':
p.lookForLinkOrImage()
case 'w', 'W', ':':
matched := p.parseAutolink(c)
if !matched {
p.parseText()
}
default:
p.parseText()
}
}
return p.inlines
}
func ParseInlines(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) (inlines []Inline) {
return newInlineParser(markdown, ranges, referenceDefinitions).Parse()
}
func MergeInlineText(inlines []Inline) []Inline {
ret := inlines[:0]
for i, v := range inlines {
// always add first node
if i == 0 {
ret = append(ret, v)
continue
}
// not a text node? nothing to merge
text, ok := v.(*Text)
if !ok {
ret = append(ret, v)
continue
}
// previous node is not a text node? nothing to merge
prevText, ok := ret[len(ret)-1].(*Text)
if !ok {
ret = append(ret, v)
continue
}
// previous node is not right before this one
if prevText.Range.End != text.Range.Position {
ret = append(ret, v)
continue
}
// we have two consecutive text nodes
ret[len(ret)-1] = &Text{
Text: prevText.Text + text.Text,
Range: Range{prevText.Range.Position, text.Range.End},
}
}
return ret
}
func Unescape(markdown string) string {
ret := ""
position := 0
for position < len(markdown) {
c, cSize := utf8.DecodeRuneInString(markdown[position:])
switch c {
case '\\':
if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) {
ret += string(markdown[position+1])
position += 2
} else {
ret += `\`
position++
}
case '&':
position++
if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 {
ret += "&"
} else if s := CharacterReference(markdown[position : position+semicolon]); s != "" {
position += semicolon + 1
ret += s
} else {
ret += "&"
}
default:
ret += string(c)
position += cSize
}
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
// Inspect traverses the markdown tree in depth-first order. If f returns true, Inspect invokes f
// recursively for each child of the block or inline, followed by a call of f(nil).
func Inspect(markdown string, f func(any) bool) {
document, referenceDefinitions := Parse(markdown)
InspectBlock(document, func(block Block) bool {
if !f(block) {
return false
}
switch v := block.(type) {
case *Paragraph:
for _, inline := range MergeInlineText(v.ParseInlines(referenceDefinitions)) {
InspectInline(inline, func(inline Inline) bool {
return f(inline)
})
}
}
return true
})
}
// InspectBlock traverses the blocks in depth-first order, starting with block. If f returns true,
// InspectBlock invokes f recursively for each child of the block, followed by a call of f(nil).
func InspectBlock(block Block, f func(Block) bool) {
if !f(block) {
return
}
switch v := block.(type) {
case *Document:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *List:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *ListItem:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *BlockQuote:
for _, child := range v.Children {
InspectBlock(child, f)
}
}
f(nil)
}
// InspectInline traverses the blocks in depth-first order, starting with block. If f returns true,
// InspectInline invokes f recursively for each child of the block, followed by a call of f(nil).
func InspectInline(inline Inline, f func(Inline) bool) {
if !f(inline) {
return
}
switch v := inline.(type) {
case *InlineImage:
for _, child := range v.Children {
InspectInline(child, f)
}
case *InlineLink:
for _, child := range v.Children {
InspectInline(child, f)
}
case *ReferenceImage:
for _, child := range v.Children {
InspectInline(child, f)
}
case *ReferenceLink:
for _, child := range v.Children {
InspectInline(child, f)
}
}
f(nil)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type Line struct {
Range
}
func ParseLines(markdown string) []Line {
lineStartPosition := 0
isAfterCarriageReturn := false
lines := make([]Line, 0, strings.Count(markdown, "\n"))
for position, r := range markdown {
if r == '\n' {
lines = append(lines, Line{Range{lineStartPosition, position + 1}})
lineStartPosition = position + 1
} else if isAfterCarriageReturn {
lines = append(lines, Line{Range{lineStartPosition, position}})
lineStartPosition = position
}
isAfterCarriageReturn = r == '\r'
}
if lineStartPosition < len(markdown) {
lines = append(lines, Line{Range{lineStartPosition, len(markdown)}})
}
return lines
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"unicode/utf8"
)
func parseLinkDestination(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
if markdown[position] == '<' {
isEscaped := false
for offset, c := range []byte(markdown[position+1:]) {
if isEscaped {
isEscaped = false
if isEscapableByte(c) {
continue
}
}
if c == '\\' {
isEscaped = true
} else if c == '<' {
break
} else if c == '>' {
return Range{position + 1, position + 1 + offset}, position + 1 + offset + 1, true
} else if isWhitespaceByte(c) {
break
}
}
}
openCount := 0
isEscaped := false
for offset, c := range []byte(markdown[position:]) {
if isEscaped {
isEscaped = false
if isEscapableByte(c) {
continue
}
}
switch c {
case '\\':
isEscaped = true
case '(':
openCount++
case ')':
if openCount < 1 {
return Range{position, position + offset}, position + offset, true
}
openCount--
default:
if isWhitespaceByte(c) {
return Range{position, position + offset}, position + offset, true
}
}
}
return Range{position, len(markdown)}, len(markdown), true
}
func parseLinkTitle(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
originalPosition := position
var closer byte
switch markdown[position] {
case '"', '\'':
closer = markdown[position]
case '(':
closer = ')'
default:
return
}
position++
for position < len(markdown) {
switch markdown[position] {
case '\\':
position++
if position < len(markdown) && isEscapableByte(markdown[position]) {
position++
}
case closer:
return Range{originalPosition + 1, position}, position + 1, true
default:
position++
}
}
return
}
func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) || markdown[position] != '[' {
return
}
originalPosition := position
position++
for position < len(markdown) {
switch markdown[position] {
case '\\':
position++
if position < len(markdown) && isEscapableByte(markdown[position]) {
position++
}
case '[':
return
case ']':
if position-originalPosition >= 1000 && utf8.RuneCountInString(markdown[originalPosition:position]) >= 1000 {
return
}
return Range{originalPosition + 1, position}, position + 1, true
default:
position++
}
}
return
}
// As a non-standard feature, we allow image links to specify dimensions of the image by adding "=WIDTHxHEIGHT"
// after the image destination but before the image title like .
// Both width and height are optional, but at least one of them must be specified.
func parseImageDimensions(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
originalPosition := position
// Read =
position += 1
if position >= len(markdown) {
return
}
// Read width
hasWidth := false
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
hasWidth = true
position += 1
}
// Look for early end of dimensions
if isWhitespaceByte(markdown[position]) || markdown[position] == ')' {
return Range{originalPosition, position - 1}, position, true
}
// Read the x
if (markdown[position] != 'x' && markdown[position] != 'X') || position == len(markdown)-1 {
return
}
position += 1
// Read height
hasHeight := false
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
hasHeight = true
position += 1
}
// Make sure the there's no trailing characters
if !isWhitespaceByte(markdown[position]) && markdown[position] != ')' {
return
}
if !hasWidth && !hasHeight {
// At least one of width or height is required
return
}
return Range{originalPosition, position - 1}, position, true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type ListItem struct {
blockBase
markdown string
hasTrailingBlankLine bool
hasBlankLineBetweenChildren bool
Indentation int
Children []Block
}
func (b *ListItem) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
if b.Children == nil {
return nil
}
return &continuation{
Remaining: r,
}
}
if indentation < b.Indentation {
return nil
}
return &continuation{
Indentation: indentation - b.Indentation,
Remaining: r,
}
}
func (b *ListItem) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
if b.hasTrailingBlankLine {
b.hasBlankLineBetweenChildren = true
}
b.hasTrailingBlankLine = false
return openBlocks
}
func (b *ListItem) AddLine(indentation int, r Range) bool {
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
if isBlank {
b.hasTrailingBlankLine = true
}
return false
}
func (b *ListItem) HasTrailingBlankLine() bool {
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
}
func (b *ListItem) isLoose() bool {
if b.hasBlankLineBetweenChildren {
return true
}
for i, child := range b.Children {
if i < len(b.Children)-1 && child.HasTrailingBlankLine() {
return true
}
}
return false
}
type List struct {
blockBase
markdown string
hasTrailingBlankLine bool
hasBlankLineBetweenChildren bool
IsLoose bool
IsOrdered bool
OrderedStart int
BulletOrDelimiter byte
Children []*ListItem
}
func (b *List) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return &continuation{
Remaining: r,
}
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *List) AddChild(openBlocks []Block) []Block {
if item, ok := openBlocks[0].(*ListItem); ok {
b.Children = append(b.Children, item)
if b.hasTrailingBlankLine {
b.hasBlankLineBetweenChildren = true
}
b.hasTrailingBlankLine = false
return openBlocks
} else if list, ok := openBlocks[0].(*List); ok {
if len(list.Children) == 1 && list.IsOrdered == b.IsOrdered && list.BulletOrDelimiter == b.BulletOrDelimiter {
return b.AddChild(openBlocks[1:])
}
}
return nil
}
func (b *List) AddLine(indentation int, r Range) bool {
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
if isBlank {
b.hasTrailingBlankLine = true
}
return false
}
func (b *List) HasTrailingBlankLine() bool {
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
}
func (b *List) isLoose() bool {
if b.hasBlankLineBetweenChildren {
return true
}
for i, child := range b.Children {
if child.isLoose() || (i < len(b.Children)-1 && child.HasTrailingBlankLine()) {
return true
}
}
return false
}
func (b *List) Close() {
b.IsLoose = b.isLoose()
}
func parseListMarker(markdown string, r Range) (success, isOrdered bool, orderedStart int, bulletOrDelimiter byte, markerWidth int, remaining Range) {
digits := 0
n := 0
for i := r.Position; i < r.End && markdown[i] >= '0' && markdown[i] <= '9'; i++ {
digits++
n = n*10 + int(markdown[i]-'0')
}
if digits > 0 {
if digits > 9 || r.Position+digits >= r.End {
return
}
next := markdown[r.Position+digits]
if next != '.' && next != ')' {
return
}
return true, true, n, next, digits + 1, Range{r.Position + digits + 1, r.End}
}
if r.Position >= r.End {
return
}
next := markdown[r.Position]
if next != '-' && next != '+' && next != '*' {
return
}
return true, false, 0, next, 1, Range{r.Position + 1, r.End}
}
func listStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
afterList := false
if len(matchedBlocks) > 0 {
_, afterList = matchedBlocks[len(matchedBlocks)-1].(*List)
}
if !afterList && indent > 3 {
return nil
}
success, isOrdered, orderedStart, bulletOrDelimiter, markerWidth, remaining := parseListMarker(markdown, r)
if !success {
return nil
}
isBlank := strings.TrimSpace(markdown[remaining.Position:remaining.End]) == ""
if len(matchedBlocks) > 0 && len(unmatchedBlocks) == 0 {
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
if isBlank || (isOrdered && orderedStart != 1) {
return nil
}
}
}
indentAfterMarker, indentBytesAfterMarker := countIndentation(markdown, remaining)
if !isBlank && indentAfterMarker < 1 {
return nil
}
remaining = Range{remaining.Position + indentBytesAfterMarker, remaining.End}
consumedIndentAfterMarker := indentAfterMarker
if isBlank || indentAfterMarker >= 5 {
consumedIndentAfterMarker = 1
}
listItem := &ListItem{
markdown: markdown,
Indentation: indent + markerWidth + consumedIndentAfterMarker,
}
list := &List{
markdown: markdown,
IsOrdered: isOrdered,
OrderedStart: orderedStart,
BulletOrDelimiter: bulletOrDelimiter,
Children: []*ListItem{listItem},
}
ret := []Block{list, listItem}
if descendants := blockStartOrParagraph(markdown, indentAfterMarker-consumedIndentAfterMarker, remaining, nil, nil); descendants != nil {
listItem.Children = append(listItem.Children, descendants[0])
ret = append(ret, descendants...)
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This package implements a parser for the subset of the CommonMark spec necessary for us to do
// server-side processing. It is not a full implementation and lacks many features. But it is
// complete enough to efficiently and accurately allow us to do what we need to like rewrite image
// URLs for proxying.
package markdown
import (
"strings"
)
func isEscapable(c rune) bool {
return c > ' ' && (c < '0' || (c > '9' && (c < 'A' || (c > 'Z' && (c < 'a' || (c > 'z' && c <= '~'))))))
}
func isEscapableByte(c byte) bool {
return isEscapable(rune(c))
}
func isWhitespace(c rune) bool {
switch c {
case ' ', '\t', '\n', '\u000b', '\u000c', '\r':
return true
}
return false
}
func isWhitespaceByte(c byte) bool {
return isWhitespace(rune(c))
}
func isNumeric(c rune) bool {
return c >= '0' && c <= '9'
}
func isNumericByte(c byte) bool {
return isNumeric(rune(c))
}
func isHex(c rune) bool {
return isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func isHexByte(c byte) bool {
return isHex(rune(c))
}
func isAlphanumeric(c rune) bool {
return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func isAlphanumericByte(c byte) bool {
return isAlphanumeric(rune(c))
}
func nextNonWhitespace(markdown string, position int) int {
for offset, c := range []byte(markdown[position:]) {
if !isWhitespaceByte(c) {
return position + offset
}
}
return len(markdown)
}
func nextLine(markdown string, position int) (linePosition int, skippedNonWhitespace bool) {
for i := position; i < len(markdown); i++ {
c := markdown[i]
if c == '\r' {
if i+1 < len(markdown) && markdown[i+1] == '\n' {
return i + 2, skippedNonWhitespace
}
return i + 1, skippedNonWhitespace
} else if c == '\n' {
return i + 1, skippedNonWhitespace
} else if !isWhitespaceByte(c) {
skippedNonWhitespace = true
}
}
return len(markdown), skippedNonWhitespace
}
func countIndentation(markdown string, r Range) (spaces, bytes int) {
for i := r.Position; i < r.End; i++ {
if markdown[i] == ' ' {
spaces++
bytes++
} else if markdown[i] == '\t' {
spaces += 4
bytes++
} else {
break
}
}
return
}
func trimLeftSpace(markdown string, r Range) Range {
s := markdown[r.Position:r.End]
trimmed := strings.TrimLeftFunc(s, isWhitespace)
return Range{r.Position, r.End - (len(s) - len(trimmed))}
}
func trimRightSpace(markdown string, r Range) Range {
s := markdown[r.Position:r.End]
trimmed := strings.TrimRightFunc(s, isWhitespace)
return Range{r.Position, r.End - (len(s) - len(trimmed))}
}
func relativeToAbsolutePosition(ranges []Range, position int) int {
rem := position
for _, r := range ranges {
l := r.End - r.Position
if rem < l {
return r.Position + rem
}
rem -= l
}
if len(ranges) == 0 {
return 0
}
return ranges[len(ranges)-1].End
}
func trimBytesFromRanges(ranges []Range, bytes int) (result []Range) {
rem := bytes
for _, r := range ranges {
if rem == 0 {
result = append(result, r)
continue
}
l := r.End - r.Position
if rem < l {
result = append(result, Range{r.Position + rem, r.End})
rem = 0
continue
}
rem -= l
}
return
}
func Parse(markdown string) (*Document, []*ReferenceDefinition) {
lines := ParseLines(markdown)
return ParseBlocks(markdown, lines)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type Paragraph struct {
blockBase
markdown string
Text []Range
ReferenceDefinitions []*ReferenceDefinition
}
func (b *Paragraph) ParseInlines(referenceDefinitions []*ReferenceDefinition) []Inline {
return ParseInlines(b.markdown, b.Text, referenceDefinitions)
}
func (b *Paragraph) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *Paragraph) Close() {
for {
for i := 0; i < len(b.Text); i++ {
b.Text[i] = trimLeftSpace(b.markdown, b.Text[i])
if b.Text[i].Position < b.Text[i].End {
break
}
}
if len(b.Text) == 0 || b.Text[0].Position < b.Text[0].End && b.markdown[b.Text[0].Position] != '[' {
break
}
definition, remaining := parseReferenceDefinition(b.markdown, b.Text)
if definition == nil {
break
}
b.ReferenceDefinitions = append(b.ReferenceDefinitions, definition)
b.Text = remaining
}
for i := len(b.Text) - 1; i >= 0; i-- {
b.Text[i] = trimRightSpace(b.markdown, b.Text[i])
if b.Text[i].Position < b.Text[i].End {
break
}
}
}
func newParagraph(markdown string, r Range) *Paragraph {
s := markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return &Paragraph{
markdown: markdown,
Text: []Range{r},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type ReferenceDefinition struct {
RawDestination Range
markdown string
rawLabel string
rawTitle string
}
func (d *ReferenceDefinition) Destination() string {
return Unescape(d.markdown[d.RawDestination.Position:d.RawDestination.End])
}
func (d *ReferenceDefinition) Label() string {
return d.rawLabel
}
func (d *ReferenceDefinition) Title() string {
return Unescape(d.rawTitle)
}
func parseReferenceDefinition(markdown string, ranges []Range) (*ReferenceDefinition, []Range) {
raw := ""
for _, r := range ranges {
raw += markdown[r.Position:r.End]
}
label, next, ok := parseLinkLabel(raw, 0)
if !ok {
return nil, nil
}
position := next
if position >= len(raw) || raw[position] != ':' {
return nil, nil
}
position++
destination, next, ok := parseLinkDestination(raw, nextNonWhitespace(raw, position))
if !ok {
return nil, nil
}
position = next
absoluteDestination := relativeToAbsolutePosition(ranges, destination.Position)
ret := &ReferenceDefinition{
RawDestination: Range{absoluteDestination, absoluteDestination + destination.End - destination.Position},
markdown: markdown,
rawLabel: raw[label.Position:label.End],
}
if position < len(raw) && isWhitespaceByte(raw[position]) {
title, next, ok := parseLinkTitle(raw, nextNonWhitespace(raw, position))
if !ok {
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
return ret, trimBytesFromRanges(ranges, nextLine)
}
return nil, nil
}
if nextLine, skippedNonWhitespace := nextLine(raw, next); !skippedNonWhitespace {
ret.rawTitle = raw[title.Position:title.End]
return ret, trimBytesFromRanges(ranges, nextLine)
}
}
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
return ret, trimBytesFromRanges(ranges, nextLine)
}
return nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mfa
import (
"crypto/rand"
"encoding/base32"
"fmt"
"net/url"
"strings"
"github.com/dgryski/dgoogauth"
"github.com/mattermost/rsc/qr"
"github.com/pkg/errors"
)
// InvalidToken indicates the case where the token validation has failed.
var InvalidToken = errors.New("invalid mfa token")
const (
// This will result in 160 bits of entropy (base32 encoded), as recommended by rfc4226.
mfaSecretSize = 20
)
type Store interface {
UpdateMfaActive(userId string, active bool) error
UpdateMfaSecret(userId, secret string) error
}
type MFA struct {
store Store
}
func New(store Store) *MFA {
return &MFA{store}
}
// newRandomBase32String returns a base32 encoded string of a random slice
// of bytes of the given size. The resulting entropy will be (8 * size) bits.
func newRandomBase32String(size int) string {
data := make([]byte, size)
rand.Read(data)
return base32.StdEncoding.EncodeToString(data)
}
func getIssuerFromURL(uri string) string {
issuer := "Mattermost"
siteURL := strings.TrimSpace(uri)
if siteURL != "" {
siteURL = strings.TrimPrefix(siteURL, "https://")
siteURL = strings.TrimPrefix(siteURL, "http://")
issuer = strings.TrimPrefix(siteURL, "www.")
}
return url.QueryEscape(issuer)
}
// GenerateSecret generates a new user mfa secret and store it with the StoreSecret function provided
func (m *MFA) GenerateSecret(siteURL, userEmail, userID string) (string, []byte, error) {
issuer := getIssuerFromURL(siteURL)
secret := newRandomBase32String(mfaSecretSize)
authLink := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, userEmail, secret, issuer)
code, err := qr.Encode(authLink, qr.H)
if err != nil {
return "", nil, errors.Wrap(err, "unable to generate qr code")
}
img := code.PNG()
if err := m.store.UpdateMfaSecret(userID, secret); err != nil {
return "", nil, errors.Wrap(err, "unable to store mfa secret")
}
return secret, img, nil
}
// Activate set the mfa as active and store it with the StoreActive function provided
func (m *MFA) Activate(userMfaSecret, userID string, token string) error {
otpConfig := &dgoogauth.OTPConfig{
Secret: userMfaSecret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return errors.Wrap(err, "unable to parse the token")
}
if !ok {
return InvalidToken
}
if err := m.store.UpdateMfaActive(userID, true); err != nil {
return errors.Wrap(err, "unable to store mfa active")
}
return nil
}
// Deactivate set the mfa as deactivated, remove the mfa secret, store it with the StoreActive and StoreSecret functions provided
func (m *MFA) Deactivate(userId string) error {
if err := m.store.UpdateMfaActive(userId, false); err != nil {
return errors.Wrap(err, "unable to store mfa active")
}
if err := m.store.UpdateMfaSecret(userId, ""); err != nil {
return errors.Wrap(err, "unable to store mfa secret")
}
return nil
}
// Validate the provide token using the secret provided
func (m *MFA) ValidateToken(secret, token string) (bool, error) {
otpConfig := &dgoogauth.OTPConfig{
Secret: secret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return false, errors.Wrap(err, "unable to parse the token")
}
return ok, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"bytes"
"encoding/json"
"fmt"
"os"
)
// defaultLog manually encodes the log to STDERR, providing a basic, default logging implementation
// before mlog is fully configured.
func defaultLog(level Level, msg string, fields ...Field) {
mFields := make(map[string]string)
buf := &bytes.Buffer{}
for _, fld := range fields {
buf.Reset()
fld.ValueString(buf, shouldQuote)
mFields[fld.Key] = buf.String()
}
log := struct {
Level string `json:"level"`
Message string `json:"msg"`
Fields map[string]string `json:"fields,omitempty"`
}{
level.Name,
msg,
mFields,
}
if b, err := json.Marshal(log); err != nil {
fmt.Fprintf(os.Stderr, `{"level":"error","msg":"failed to encode log message"}%s`, "\n")
} else {
fmt.Fprintf(os.Stderr, "%s\n", b)
}
}
func defaultIsLevelEnabled(level Level) bool {
return true
}
func defaultCustomMultiLog(lvl []Level, msg string, fields ...Field) {
for _, level := range lvl {
defaultLog(level, msg, fields...)
}
}
// shouldQuote returns true if val contains any characters that require quotations.
func shouldQuote(val string) bool {
for _, c := range val {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"sync"
)
var (
globalLogger *Logger
muxGlobalLogger sync.RWMutex
)
func InitGlobalLogger(logger *Logger) {
muxGlobalLogger.Lock()
defer muxGlobalLogger.Unlock()
globalLogger = logger
}
func getGlobalLogger() *Logger {
muxGlobalLogger.RLock()
defer muxGlobalLogger.RUnlock()
return globalLogger
}
// IsLevelEnabled returns true only if at least one log target is
// configured to emit the specified log level. Use this check when
// gathering the log info may be expensive.
//
// Note, transformations and serializations done via fields are already
// lazily evaluated and don't require this check beforehand.
func IsLevelEnabled(level Level) bool {
logger := getGlobalLogger()
if logger == nil {
return defaultIsLevelEnabled(level)
}
return logger.IsLevelEnabled(level)
}
// Log emits the log record for any targets configured for the specified level.
func Log(level Level, msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(level, msg, fields...)
return
}
logger.Log(level, msg, fields...)
}
// LogM emits the log record for any targets configured for the specified levels.
// Equivalent to calling `Log` once for each level.
func LogM(levels []Level, msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultCustomMultiLog(levels, msg, fields...)
return
}
logger.LogM(levels, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Trace` level.
func Trace(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlTrace, msg, fields...)
return
}
logger.Trace(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Debug` level.
func Debug(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlDebug, msg, fields...)
return
}
logger.Debug(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Info` level.
func Info(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlInfo, msg, fields...)
return
}
logger.Info(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Warn` level.
func Warn(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlWarn, msg, fields...)
return
}
logger.Warn(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Error` level.
func Error(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlError, msg, fields...)
return
}
logger.Error(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Critical` level.
// DEPRECATED: Either use Error or Fatal.
// Critical level isn't added in mlog/levels.go:StdAll so calling this doesn't
// really work. For now we just call Fatal to atleast print something.
func Critical(msg string, fields ...Field) {
Fatal(msg, fields...)
}
func Fatal(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlFatal, msg, fields...)
return
}
logger.Fatal(msg, fields...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"context"
)
// GraphQLLogger is used to log panics that occur during query execution.
type GraphQLLogger struct {
logger *Logger
}
func NewGraphQLLogger(logger *Logger) *GraphQLLogger {
return &GraphQLLogger{logger: logger}
}
// LogPanic satisfies the graphql/log.Logger interface.
// It converts the panic into an error.
func (l *GraphQLLogger) LogPanic(_ context.Context, value any) {
l.logger.Error("Error while executing GraphQL query", Any("error", value))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Package mlog provides a simple wrapper around Logr.
package mlog
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"sync/atomic"
"time"
"github.com/mattermost/logr/v2"
logrcfg "github.com/mattermost/logr/v2/config"
)
const (
ShutdownTimeout = time.Second * 15
FlushTimeout = time.Second * 15
DefaultMaxQueueSize = 1000
DefaultMetricsUpdateFreqMillis = 15000
)
type LoggerIFace interface {
IsLevelEnabled(Level) bool
Trace(string, ...Field)
Debug(string, ...Field)
Info(string, ...Field)
Warn(string, ...Field)
Error(string, ...Field)
Critical(string, ...Field)
Fatal(string, ...Field)
Log(Level, string, ...Field)
LogM([]Level, string, ...Field)
With(fields ...Field) *Logger
Flush() error
StdLogger(level Level) *log.Logger
}
// Type and function aliases from Logr to limit the spread of dependencies.
type Field = logr.Field
type Level = logr.Level
type Option = logr.Option
type Target = logr.Target
type TargetInfo = logr.TargetInfo
type LogRec = logr.LogRec
type LogCloner = logr.LogCloner
type MetricsCollector = logr.MetricsCollector
type TargetCfg = logrcfg.TargetCfg
type TargetFactory = logrcfg.TargetFactory
type FormatterFactory = logrcfg.FormatterFactory
type Factories = logrcfg.Factories
type Sugar = logr.Sugar
// LoggerConfiguration is a map of LogTarget configurations.
type LoggerConfiguration map[string]TargetCfg
func (lc LoggerConfiguration) Append(cfg LoggerConfiguration) {
for k, v := range cfg {
lc[k] = v
}
}
func (lc LoggerConfiguration) toTargetCfg() map[string]logrcfg.TargetCfg {
tcfg := make(map[string]logrcfg.TargetCfg)
for k, v := range lc {
tcfg[k] = v
}
return tcfg
}
// Any picks the best supported field type based on type of val.
// For best performance when passing a struct (or struct pointer),
// implement `logr.LogWriter` on the struct, otherwise reflection
// will be used to generate a string representation.
var Any = logr.Any
// Int64 constructs a field containing a key and Int64 value.
var Int64 = logr.Int64
// Int32 constructs a field containing a key and Int32 value.
var Int32 = logr.Int32
// Int constructs a field containing a key and Int value.
var Int = logr.Int
// Uint64 constructs a field containing a key and Uint64 value.
var Uint64 = logr.Uint64
// Uint32 constructs a field containing a key and Uint32 value.
var Uint32 = logr.Uint32
// Uint constructs a field containing a key and Uint value.
var Uint = logr.Uint
// Float64 constructs a field containing a key and Float64 value.
var Float64 = logr.Float64
// Float32 constructs a field containing a key and Float32 value.
var Float32 = logr.Float32
// String constructs a field containing a key and String value.
var String = logr.String
// Stringer constructs a field containing a key and a fmt.Stringer value.
// The fmt.Stringer's `String` method is called lazily.
var Stringer = func(key string, s fmt.Stringer) logr.Field {
if s == nil {
return Field{Key: key, Type: logr.StringType, String: ""}
}
return Field{Key: key, Type: logr.StringType, String: s.String()}
}
// Err constructs a field containing a default key ("error") and error value.
var Err = func(err error) logr.Field {
return NamedErr("error", err)
}
// NamedErr constructs a field containing a key and error value.
var NamedErr = func(key string, err error) logr.Field {
if err == nil {
return Field{Key: key, Type: logr.StringType, String: ""}
}
return Field{Key: key, Type: logr.StringType, String: err.Error()}
}
// Bool constructs a field containing a key and bool value.
var Bool = logr.Bool
// Time constructs a field containing a key and time.Time value.
var Time = logr.Time
// Duration constructs a field containing a key and time.Duration value.
var Duration = logr.Duration
// Millis constructs a field containing a key and timestamp value.
// The timestamp is expected to be milliseconds since Jan 1, 1970 UTC.
var Millis = logr.Millis
// Array constructs a field containing a key and array value.
var Array = logr.Array
// Map constructs a field containing a key and map value.
var Map = logr.Map
// Logger provides a thin wrapper around a Logr instance. This is a struct instead of an interface
// so that there are no allocations on the heap each interface method invocation. Normally not
// something to be concerned about, but logging calls for disabled levels should have as little CPU
// and memory impact as possible. Most of these wrapper calls will be inlined as well.
type Logger struct {
log *logr.Logger
lockConfig *int32
}
// NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure`.
// Some options with invalid values can cause an error to be returned, however `NewLogger()`
// using just defaults never errors.
func NewLogger(options ...Option) (*Logger, error) {
options = append(options, logr.StackFilter(logr.GetPackageName("NewLogger")))
lgr, err := logr.New(options...)
if err != nil {
return nil, err
}
log := lgr.NewLogger()
var lockConfig int32
return &Logger{
log: &log,
lockConfig: &lockConfig,
}, nil
}
// Configure provides a new configuration for this logger.
// Zero or more sources of config can be provided:
//
// cfgFile - path to file containing JSON
// cfgEscaped - JSON string probably from ENV var
//
// For each case JSON containing log targets is provided. Target name collisions are resolved
// using the following precedence:
//
// cfgFile > cfgEscaped
//
// An optional set of factories can be provided which will be called to create any target
// types or formatters not built-in.
func (l *Logger) Configure(cfgFile string, cfgEscaped string, factories *Factories) error {
if atomic.LoadInt32(l.lockConfig) != 0 {
return ErrConfigurationLock
}
cfgMap := make(LoggerConfiguration)
// Add config from file
if cfgFile != "" {
b, err := os.ReadFile(cfgFile)
if err != nil {
return fmt.Errorf("error reading logger config file %s: %w", cfgFile, err)
}
var mapCfgFile LoggerConfiguration
if err := json.Unmarshal(b, &mapCfgFile); err != nil {
return fmt.Errorf("error decoding logger config file %s: %w", cfgFile, err)
}
cfgMap.Append(mapCfgFile)
}
// Add config from escaped json string
if cfgEscaped != "" {
var mapCfgEscaped LoggerConfiguration
if err := json.Unmarshal([]byte(cfgEscaped), &mapCfgEscaped); err != nil {
return fmt.Errorf("error decoding logger config as escaped json: %w", err)
}
cfgMap.Append(mapCfgEscaped)
}
if len(cfgMap) == 0 {
return nil
}
return logrcfg.ConfigureTargets(l.log.Logr(), cfgMap.toTargetCfg(), factories)
}
// ConfigureTargets provides a new configuration for this logger via a `LoggerConfig` map.
// Typically `mlog.Configure` is used instead which accepts JSON formatted configuration.
// An optional set of factories can be provided which will be called to create any target
// types or formatters not built-in.
func (l *Logger) ConfigureTargets(cfg LoggerConfiguration, factories *Factories) error {
if atomic.LoadInt32(l.lockConfig) != 0 {
return ErrConfigurationLock
}
return logrcfg.ConfigureTargets(l.log.Logr(), cfg.toTargetCfg(), factories)
}
// LockConfiguration disallows further configuration changes until `UnlockConfiguration`
// is called. The previous locked stated is returned.
func (l *Logger) LockConfiguration() bool {
old := atomic.SwapInt32(l.lockConfig, 1)
return old != 0
}
// UnlockConfiguration allows configuration changes. The previous locked stated is returned.
func (l *Logger) UnlockConfiguration() bool {
old := atomic.SwapInt32(l.lockConfig, 0)
return old != 0
}
// IsConfigurationLocked returns the current state of the configuration lock.
func (l *Logger) IsConfigurationLocked() bool {
return atomic.LoadInt32(l.lockConfig) != 0
}
// With creates a new Logger with the specified fields. This is a light-weight
// operation and can be called on demand.
func (l *Logger) With(fields ...Field) *Logger {
logWith := l.log.With(fields...)
return &Logger{
log: &logWith,
lockConfig: l.lockConfig,
}
}
// IsLevelEnabled returns true only if at least one log target is
// configured to emit the specified log level. Use this check when
// gathering the log info may be expensive.
//
// Note, transformations and serializations done via fields are already
// lazily evaluated and don't require this check beforehand.
func (l *Logger) IsLevelEnabled(level Level) bool {
return l.log.IsLevelEnabled(level)
}
// Log emits the log record for any targets configured for the specified level.
func (l *Logger) Log(level Level, msg string, fields ...Field) {
l.log.Log(level, msg, fields...)
}
// LogM emits the log record for any targets configured for the specified levels.
// Equivalent to calling `Log` once for each level.
func (l *Logger) LogM(levels []Level, msg string, fields ...Field) {
l.log.LogM(levels, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Trace` level.
func (l *Logger) Trace(msg string, fields ...Field) {
l.log.Trace(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Debug` level.
func (l *Logger) Debug(msg string, fields ...Field) {
l.log.Debug(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Info` level.
func (l *Logger) Info(msg string, fields ...Field) {
l.log.Info(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Warn` level.
func (l *Logger) Warn(msg string, fields ...Field) {
l.log.Warn(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Error` level.
func (l *Logger) Error(msg string, fields ...Field) {
l.log.Error(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Critical` level.
func (l *Logger) Critical(msg string, fields ...Field) {
l.log.Log(LvlCritical, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Fatal` level,
// followed by `os.Exit(1)`.
func (l *Logger) Fatal(msg string, fields ...Field) {
l.log.Log(logr.Fatal, msg, fields...)
_ = l.Shutdown()
os.Exit(1)
}
// HasTargets returns true if at least one log target has been added.
func (l *Logger) HasTargets() bool {
return l.log.Logr().HasTargets()
}
// StdLogger creates a standard logger backed by this logger.
// All log records are output with the specified level.
func (l *Logger) StdLogger(level Level) *log.Logger {
return l.log.StdLogger(level)
}
// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger
// anything written will be interpreted as log entries and passed to this logger.
func (l *Logger) StdLogWriter() io.Writer {
return &logWriter{
logger: l,
}
}
// RedirectStdLog redirects output from the standard library's package-global logger
// to this logger at the specified level and with zero or more Field's. Since this logger already
// handles caller annotations, timestamps, etc., it automatically disables the standard
// library's annotations and prefixing.
// A function is returned that restores the original prefix and flags and resets the standard
// library's output to os.Stdout.
func (l *Logger) RedirectStdLog(level Level, fields ...Field) func() {
return l.log.Logr().RedirectStdLog(level, fields...)
}
// RemoveTargets safely removes one or more targets based on the filtering method.
// `f` should return true to delete the target, false to keep it.
// When removing a target, best effort is made to write any queued log records before
// closing, with ctx determining how much time can be spent in total.
// Note, keep the timeout short since this method blocks certain logging operations.
func (l *Logger) RemoveTargets(ctx context.Context, f func(ti TargetInfo) bool) error {
return l.log.Logr().RemoveTargets(ctx, f)
}
// SetMetricsCollector sets (or resets) the metrics collector to be used for gathering
// metrics for all targets. Only targets added after this call will use the collector.
//
// To ensure all targets use a collector, use the `SetMetricsCollector` option when
// creating the Logger instead, or configure/reconfigure the Logger after calling this method.
func (l *Logger) SetMetricsCollector(collector MetricsCollector, updateFrequencyMillis int64) {
l.log.Logr().SetMetricsCollector(collector, updateFrequencyMillis)
}
// Sugar creates a new `Logger` with a less structured API. Any fields are preserved.
func (l *Logger) Sugar(fields ...Field) Sugar {
return l.log.Sugar(fields...)
}
// Flush forces all targets to write out any queued log records with a default timeout.
func (l *Logger) Flush() error {
ctx, cancel := context.WithTimeout(context.Background(), FlushTimeout)
defer cancel()
return l.log.Logr().FlushWithTimeout(ctx)
}
// Flush forces all targets to write out any queued log records with the specified timeout.
func (l *Logger) FlushWithTimeout(ctx context.Context) error {
return l.log.Logr().FlushWithTimeout(ctx)
}
// Shutdown shuts down the logger after making best efforts to flush any
// remaining records.
func (l *Logger) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
defer cancel()
return l.log.Logr().ShutdownWithTimeout(ctx)
}
// Shutdown shuts down the logger after making best efforts to flush any
// remaining records.
func (l *Logger) ShutdownWithTimeout(ctx context.Context) error {
return l.log.Logr().ShutdownWithTimeout(ctx)
}
// GetPackageName reduces a fully qualified function name to the package name
// By sirupsen: https://github.com/sirupsen/logrus/blob/master/entry.go
func GetPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}
return f
}
// ShouldQuote returns true if val contains any characters that might be unsafe
// when injecting log output into an aggregator, viewer or report.
// Returning true means that val should be surrounded by quotation marks before being
// output into logs.
func ShouldQuote(val string) bool {
for _, c := range val {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
return true
}
}
return false
}
type logWriter struct {
logger *Logger
}
func (lw *logWriter) Write(p []byte) (int, error) {
lw.logger.Info(string(p))
return len(p), nil
}
// ErrConfigurationLock is returned when one of a logger's configuration APIs is called
// while the configuration is locked.
var ErrConfigurationLock = errors.New("configuration is locked")
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import "github.com/mattermost/logr/v2"
// MaxQueueSize is the maximum number of log records that can be queued.
// If exceeded, `OnQueueFull` is called which determines if the log
// record will be dropped or block until add is successful.
// Defaults to DefaultMaxQueueSize.
func MaxQueueSize(size int) Option {
return logr.MaxQueueSize(size)
}
// OnLoggerError, when not nil, is called any time an internal
// logging error occurs. For example, this can happen when a
// target cannot connect to its data sink.
func OnLoggerError(f func(error)) Option {
return logr.OnLoggerError(f)
}
// OnQueueFull, when not nil, is called on an attempt to add
// a log record to a full Logr queue.
// `MaxQueueSize` can be used to modify the maximum queue size.
// This function should return quickly, with a bool indicating whether
// the log record should be dropped (true) or block until the log record
// is successfully added (false). If nil then blocking (false) is assumed.
func OnQueueFull(f func(rec *LogRec, maxQueueSize int) bool) Option {
return logr.OnQueueFull(f)
}
// OnTargetQueueFull, when not nil, is called on an attempt to add
// a log record to a full target queue provided the target supports reporting
// this condition.
// This function should return quickly, with a bool indicating whether
// the log record should be dropped (true) or block until the log record
// is successfully added (false). If nil then blocking (false) is assumed.
func OnTargetQueueFull(f func(target Target, rec *LogRec, maxQueueSize int) bool) Option {
return logr.OnTargetQueueFull(f)
}
// SetMetricsCollector enables metrics collection by supplying a MetricsCollector.
// The MetricsCollector provides counters and gauges that are updated by log targets.
// `updateFreqMillis` determines how often polled metrics are updated. Defaults to 15000 (15 seconds)
// and must be at least 250 so we don't peg the CPU.
func SetMetricsCollector(collector MetricsCollector, updateFreqMillis int64) Option {
return logr.SetMetricsCollector(collector, updateFreqMillis)
}
// StackFilter provides a list of package names to exclude from the top of
// stack traces. The Logr packages are automatically filtered.
func StackFilter(pkg ...string) Option {
return logr.StackFilter(pkg...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"bytes"
"io"
"os"
"sync"
"github.com/mattermost/logr/v2"
"github.com/mattermost/logr/v2/formatters"
"github.com/mattermost/logr/v2/targets"
)
// AddWriterTarget adds a simple io.Writer target to an existing Logger.
// The `io.Writer` can be a buffer which is useful for testing.
// When adding a buffer to collect logs make sure to use `mlog.Buffer` which is
// a thread safe version of `bytes.Buffer`.
func AddWriterTarget(logger *Logger, w io.Writer, useJSON bool, levels ...Level) error {
filter := logr.NewCustomFilter(levels...)
var formatter logr.Formatter
if useJSON {
formatter = &formatters.JSON{EnableCaller: true}
} else {
formatter = &formatters.Plain{EnableCaller: true}
}
target := targets.NewWriterTarget(w)
return logger.log.Logr().AddTarget(target, "_testWriter", filter, formatter, 1000)
}
// CreateConsoleTestLogger creates a logger for unit tests. Log records are output to `os.Stdout`.
// Logs can also be mirrored to the optional `io.Writer`.
func CreateConsoleTestLogger(useJSON bool, level Level) *Logger {
logger, _ := NewLogger()
filter := logr.StdFilter{
Lvl: level,
Stacktrace: LvlPanic,
}
var formatter logr.Formatter
if useJSON {
formatter = &formatters.JSON{EnableCaller: true}
} else {
formatter = &formatters.Plain{EnableCaller: true}
}
target := targets.NewWriterTarget(os.Stdout)
if err := logger.log.Logr().AddTarget(target, "_testcon", filter, formatter, 1000); err != nil {
panic(err)
}
return logger
}
// Buffer provides a thread-safe buffer useful for logging to memory in unit tests.
type Buffer struct {
buf bytes.Buffer
mux sync.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.Write(p)
}
func (b *Buffer) String() string {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.String()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package templates
import (
"bytes"
"html/template"
"io"
"os"
"path/filepath"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
// Container represents a set of templates that can be render
type Container struct {
templates *template.Template
mutex sync.RWMutex
stop chan struct{}
stopped chan struct{}
watch bool
}
// Data contains the data used to populate the template variables, it has Props
// that can be of any type and HTML that only can be `template.HTML` types.
type Data struct {
Props map[string]any
HTML map[string]template.HTML
}
func GetTemplateDirectory() (string, bool) {
templatesDir := "templates"
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
templatesDir = filepath.Join(mattermostPath, templatesDir)
}
return fileutils.FindDir(templatesDir)
}
// NewFromTemplates creates a new templates container using a
// `template.Template` object
func NewFromTemplate(templates *template.Template) *Container {
return &Container{templates: templates}
}
// New creates a new templates container scanning a directory.
func New(directory string) (*Container, error) {
c := &Container{}
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
if err != nil {
return nil, err
}
c.templates = htmlTemplates
return c, nil
}
// NewWithWatcher creates a new templates container scanning a directory and
// watch the directory filesystem changes to apply them to the loaded
// templates. This function returns the container and an errors channel to pass
// all errors that can happen during the watch process, or an regular error if
// we fail to create the templates or the watcher. The caller must consume the
// returned errors channel to ensure not blocking the watch process.
func NewWithWatcher(directory string) (*Container, <-chan error, error) {
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
if err != nil {
return nil, nil, err
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, nil, err
}
err = watcher.Add(directory)
if err != nil {
watcher.Close()
return nil, nil, err
}
c := &Container{
templates: htmlTemplates,
watch: true,
stop: make(chan struct{}),
stopped: make(chan struct{}),
}
errors := make(chan error)
go func() {
defer close(errors)
defer close(c.stopped)
defer watcher.Close()
for {
select {
case <-c.stop:
return
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
if htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")); err != nil {
errors <- err
} else {
c.mutex.Lock()
c.templates = htmlTemplates
c.mutex.Unlock()
}
}
case err := <-watcher.Errors:
errors <- err
}
}
}()
return c, errors, nil
}
// Close stops the templates watcher of the container in case you have created
// it with watch parameter set to true
func (c *Container) Close() {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.watch {
close(c.stop)
<-c.stopped
}
}
// RenderToString renders the template referenced with the template name using
// the data provided and return a string with the result
func (c *Container) RenderToString(templateName string, data Data) (string, error) {
var text bytes.Buffer
if err := c.Render(&text, templateName, data); err != nil {
return "", err
}
return text.String(), nil
}
// RenderToString renders the template referenced with the template name using
// the data provided and write it to the writer provided
func (c *Container) Render(w io.Writer, templateName string, data Data) error {
c.mutex.RLock()
htmlTemplates := c.templates
c.mutex.RUnlock()
if err := htmlTemplates.ExecuteTemplate(w, templateName, data); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
var UnsafeContentTypes = [...]string{
"application/javascript",
"application/ecmascript",
"text/javascript",
"text/ecmascript",
"application/x-javascript",
"text/html",
}
var MediaContentTypes = [...]string{
"image/jpeg",
"image/png",
"image/bmp",
"image/gif",
"image/tiff",
"video/avi",
"video/mpeg",
"video/mp4",
"audio/mpeg",
"audio/wav",
}
func WriteFileResponse(filename string, contentType string, contentSize int64, lastModification time.Time, webserverMode string, fileReader io.ReadSeeker, forceDownload bool, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "private, no-cache")
w.Header().Set("X-Content-Type-Options", "nosniff")
if contentSize > 0 {
contentSizeStr := strconv.Itoa(int(contentSize))
if webserverMode == "gzip" {
w.Header().Set("X-Uncompressed-Content-Length", contentSizeStr)
} else {
w.Header().Set("Content-Length", contentSizeStr)
}
}
if contentType == "" {
contentType = "application/octet-stream"
} else {
for _, unsafeContentType := range UnsafeContentTypes {
if strings.HasPrefix(contentType, unsafeContentType) {
contentType = "text/plain"
break
}
}
}
w.Header().Set("Content-Type", contentType)
var toDownload bool
if forceDownload {
toDownload = true
} else {
isMediaType := false
for _, mediaContentType := range MediaContentTypes {
if strings.HasPrefix(contentType, mediaContentType) {
isMediaType = true
break
}
}
toDownload = !isMediaType
}
filename = url.PathEscape(filename)
if toDownload {
w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
} else {
w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
}
// prevent file links from being embedded in iframes
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")
http.ServeContent(w, r, filename, lastModification, fileReader)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
// ActionsService handles communication with the actions related
// methods of the Playbook API.
type ActionsService struct {
client *Client
}
// Create an action. Returns the id of the newly created action.
func (s *ActionsService) Create(ctx context.Context, channelID string, opts ChannelActionCreateOptions) (string, error) {
actionURL := fmt.Sprintf("actions/channels/%s", channelID)
req, err := s.client.newRequest(http.MethodPost, actionURL, opts)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
// List the actions in a channel.
func (s *ActionsService) List(ctx context.Context, channelID string, opts ChannelActionListOptions) ([]GenericChannelAction, error) {
actionURL, err := addOptions(fmt.Sprintf("actions/channels/%s", channelID), opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newRequest(http.MethodGet, actionURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := []GenericChannelAction{}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Update an existing action.
func (s *ActionsService) Update(ctx context.Context, action GenericChannelAction) error {
updateURL := fmt.Sprintf("actions/channels/%s/%s", action.ChannelID, action.ID)
req, err := s.client.newRequest(http.MethodPut, updateURL, action)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"github.com/google/go-querystring/query"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
const (
apiVersion = "v0"
manifestID = "playbooks"
userAgent = "go-client/" + apiVersion
)
// Client manages communication with the Playbooks API.
type Client struct {
// client is the underlying HTTP client used to make API requests.
client *http.Client
// BaseURL is the base HTTP endpoint for the Playbooks plugin.
BaseURL *url.URL
// User agent used when communicating with the Playbooks API.
UserAgent string
// PlaybookRuns is a collection of methods used to interact with playbook runs.
PlaybookRuns *PlaybookRunService
// Playbooks is a collection of methods used to interact with playbooks.
Playbooks *PlaybooksService
// Settings is a collection of methods used to interact with settings.
Settings *SettingsService
// Actions is a collection of methods used to interact with actions.
Actions *ActionsService
// Stats is a collection of methods used to interact with stats.
Stats *StatsService
// Reminders is a collection of methods used to interact with reminders.
Reminders *RemindersService
// Telemetry is a collection of methods used to interact with telemetry.
Telemetry *TelemetryService
}
// New creates a new instance of Client using the configuration from the given Mattermost Client.
func New(client4 *model.Client4) (*Client, error) {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: client4.AuthToken},
)
return newClient(client4.URL, oauth2.NewClient(ctx, ts))
}
// newClient creates a new instance of Client from the given URL and http.Client.
func newClient(mattermostSiteURL string, httpClient *http.Client) (*Client, error) {
siteURL, err := url.Parse(mattermostSiteURL)
if err != nil {
return nil, err
}
c := &Client{client: httpClient, BaseURL: siteURL, UserAgent: userAgent}
c.PlaybookRuns = &PlaybookRunService{c}
c.Playbooks = &PlaybooksService{c}
c.Settings = &SettingsService{c}
c.Actions = &ActionsService{c}
c.Stats = &StatsService{c}
c.Reminders = &RemindersService{c}
c.Telemetry = &TelemetryService{c}
return c, nil
}
// newRequest creates an API request, JSON-encoding any given body parameter.
func (c *Client) newRequest(method, endpoint string, body interface{}) (*http.Request, error) {
u, err := c.BaseURL.Parse(buildAPIURL(endpoint))
if err != nil {
return nil, errors.Wrapf(err, "invalid endpoint %s", endpoint)
}
var buf io.ReadWriter
if body != nil {
buf = &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
err = enc.Encode(body)
if err != nil {
return nil, errors.Wrapf(err, "failed to encode body %s", body)
}
}
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, errors.Wrapf(err, "failed to create http request for url %s", u)
}
if buf != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return req, nil
}
// buildAPIURL constructs the path to the given endpoint.
func buildAPIURL(endpoint string) string {
return fmt.Sprintf("plugins/%s/api/%s/%s", manifestID, apiVersion, endpoint)
}
// do sends an API request and returns the API response.
//
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred. If v implements the io.Writer
// interface, the raw response body will be written to v, without attempting to
// first decode it.
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
if ctx == nil {
return nil, errors.New("context must be non-nil")
}
req = req.WithContext(ctx)
resp, err := c.client.Do(req)
if err != nil {
select {
case <-ctx.Done():
return nil, errors.Wrapf(ctx.Err(), "client err=%s", err.Error())
default:
}
return nil, err
}
defer resp.Body.Close()
err = checkResponse(resp)
if err != nil {
return resp, err
}
if v != nil {
if w, ok := v.(io.Writer); ok {
if _, err = io.Copy(w, resp.Body); err != nil {
return nil, err
}
} else {
body, _ := ioutil.ReadAll(resp.Body)
decErr := json.NewDecoder(bytes.NewReader(body)).Decode(v)
if decErr == io.EOF {
// TODO: Confirm if this happens only on empty bodies. If so, check that first before decoding.
decErr = nil // ignore EOF errors caused by empty response body
}
if decErr != nil {
err = decErr
}
}
}
return resp, err
}
type GraphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
func (c *Client) DoGraphql(ctx context.Context, input *GraphQLInput, v interface{}) error {
url := "query"
req, err := c.newRequest(http.MethodPost, url, input)
if err != nil {
return err
}
_, err = c.do(ctx, req, v)
if err != nil {
return err
}
return nil
}
// checkResponse checks the API response for an error.
//
// Any response with a status code outside 2xx is considered an error, and its body inspected for
// an optional `Error` property in a JSON struct.
func checkResponse(r *http.Response) error {
if c := r.StatusCode; http.StatusOK <= c && c <= 299 {
return nil
}
errorResponse := &ErrorResponse{
StatusCode: r.StatusCode,
Method: r.Request.Method,
URL: r.Request.URL.String(),
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
errorResponse.Err = fmt.Errorf("failed to read response body: %w", err)
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
if data != nil {
_ = json.Unmarshal(data, errorResponse)
}
return errorResponse
}
// addOption adds the given parameter as an URL query parameters to s.
func addOption(s string, name, value string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qa := u.Query()
qa.Add(name, value)
u.RawQuery = qa.Encode()
return u.String(), nil
}
// addOptions adds the parameters in opts as URL query parameters to s. opts
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opts interface{}) (string, error) {
v := reflect.ValueOf(opts)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qs, err := query.Values(opts)
if err != nil {
return s, errors.Wrapf(err, "failed to opts %+v", opts)
}
// Append to the existing query parameters.
qa := u.Query()
for key, values := range qs {
for _, value := range values {
qa.Add(key, value)
}
}
u.RawQuery = qa.Encode()
return u.String(), nil
}
// addPaginationOptions adds the given pagination parameters as URL query parameters to s.
func addPaginationOptions(s string, page, perPage int) (string, error) {
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qa := u.Query()
qa.Add("page", strconv.Itoa(page))
qa.Add("per_page", strconv.Itoa(perPage))
u.RawQuery = qa.Encode()
return u.String(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"encoding/json"
"errors"
"fmt"
)
// ErrorResponse is an error from an API request.
type ErrorResponse struct {
// Method is the HTTP verb used in the API request.
Method string
// URL is the HTTP endpoint used in the API request.
URL string
// StatusCode is the HTTP status code returned by the API.
StatusCode int
// Err is the error parsed from the API response.
Err error `json:"error"`
}
func (e *ErrorResponse) UnmarshalJSON(data []byte) error {
type Alias ErrorResponse
temp := &struct {
Err string `json:"error"`
*Alias
}{
Alias: (*Alias)(e),
}
// Try to extract a structured error from the body, otherwise fall back to using
// the whole body as the error message.
if err := json.Unmarshal(data, &temp); err != nil || temp.Err == "" {
e.Err = errors.New(string(data))
} else {
e.Err = errors.New(temp.Err)
}
return nil
}
// Unwrap exposes the underlying error of an ErrorResponse.
func (e *ErrorResponse) Unwrap() error {
return e.Err
}
// Error describes the error from the API request.
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("%s %s [%d]: %v", e.Method, e.URL, e.StatusCode, e.Err)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"fmt"
"gopkg.in/guregu/null.v4"
)
// Playbook represents the planning before a playbook run is initiated.
type Playbook struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Public bool `json:"public"`
TeamID string `json:"team_id"`
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
NumStages int64 `json:"num_stages"`
NumSteps int64 `json:"num_steps"`
Checklists []Checklist `json:"checklists"`
Members []PlaybookMember `json:"members"`
ReminderMessageTemplate string `json:"reminder_message_template"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
InvitedUserIDs []string `json:"invited_user_ids"`
InvitedGroupIDs []string `json:"invited_group_ids"`
InviteUsersEnabled bool `json:"invite_users_enabled"`
DefaultOwnerID string `json:"default_owner_id"`
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
BroadcastEnabled bool `json:"broadcast_enabled"`
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled"`
Metrics []PlaybookMetricConfig `json:"metrics"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
ChannelID string `json:"channel_id" export:"channel_id"`
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
}
type PlaybookMember struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
SchemeRoles []string `json:"scheme_roles"`
}
const (
MetricTypeDuration = "metric_duration"
MetricTypeCurrency = "metric_currency"
MetricTypeInteger = "metric_integer"
)
// Checklist represents a checklist in a playbook
type Checklist struct {
ID string `json:"id"`
Title string `json:"title"`
Items []ChecklistItem `json:"items"`
}
// ChecklistItem represents an item in a checklist
type ChecklistItem struct {
ID string `json:"id"`
Title string `json:"title"`
State string `json:"state"`
StateModified int64 `json:"state_modified"`
AssigneeID string `json:"assignee_id"`
AssigneeModified int64 `json:"assignee_modified"`
Command string `json:"command"`
CommandLastRun int64 `json:"command_last_run"`
Description string `json:"description"`
LastSkipped int64 `json:"delete_at"`
DueDate int64 `json:"due_date"`
TaskActions []TaskAction `json:"task_actions"`
}
// TaskAction represents a task action in an item
type TaskAction struct {
Trigger TriggerAction `json:"trigger"`
Actions []TriggerAction `json:"actions"`
}
// TriggerAction represents a trigger or action in a Task Action
type TriggerAction struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
// PlaybookCreateOptions specifies the parameters for PlaybooksService.Create method.
type PlaybookCreateOptions struct {
Title string `json:"title"`
Description string `json:"description"`
TeamID string `json:"team_id"`
Public bool `json:"public"`
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
Checklists []Checklist `json:"checklists"`
Members []PlaybookMember `json:"members"`
BroadcastChannelID string `json:"broadcast_channel_id"`
ReminderMessageTemplate string `json:"reminder_message_template"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
InvitedUserIDs []string `json:"invited_user_ids"`
InvitedGroupIDs []string `json:"invited_group_ids"`
InviteUsersEnabled bool `json:"invite_users_enabled"`
DefaultOwnerID string `json:"default_owner_id"`
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
BroadcastEnabled bool `json:"broadcast_enabled"`
Metrics []PlaybookMetricConfig `json:"metrics"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
ChannelID string `json:"channel_id" export:"channel_id"`
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
}
type PlaybookMetricConfig struct {
ID string `json:"id"`
PlaybookID string `json:"playbook_id"`
Title string `json:"title"`
Description string `json:"description"`
Type string `json:"type"`
Target null.Int `json:"target"`
}
// PlaybookListOptions specifies the optional parameters to the
// PlaybooksService.List method.
type PlaybookListOptions struct {
Sort Sort `url:"sort,omitempty"`
Direction SortDirection `url:"direction,omitempty"`
SearchTeam string `url:"search_term,omitempty"`
WithArchived bool `url:"with_archived,omitempty"`
}
type GetPlaybooksResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []Playbook `json:"items"`
}
type PlaybookStats struct {
RunsInProgress int `json:"runs_in_progress"`
ParticipantsActive int `json:"participants_active"`
RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"`
RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"`
RunsStartedPerWeek []int `json:"runs_started_per_week"`
RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"`
ActiveRunsPerDay []int `json:"active_runs_per_day"`
ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"`
ActiveParticipantsPerDay []int `json:"active_participants_per_day"`
ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"`
MetricOverallAverage []null.Int `json:"metric_overall_average"`
MetricRollingAverage []null.Int `json:"metric_rolling_average"`
MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"`
MetricValueRange [][]int64 `json:"metric_value_range"`
MetricRollingValues [][]int64 `json:"metric_rolling_values"`
LastXRunNames []string `json:"last_x_run_names"`
}
type ChannelPlaybookMode int
const (
PlaybookRunCreateNewChannel ChannelPlaybookMode = iota
PlaybookRunLinkExistingChannel
)
var channelPlaybookTypes = [...]string{
PlaybookRunCreateNewChannel: "create_new_channel",
PlaybookRunLinkExistingChannel: "link_existing_channel",
}
// String creates the string version of the TelemetryTrack
func (cpm ChannelPlaybookMode) String() string {
return channelPlaybookTypes[cpm]
}
// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON)
func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) {
return []byte(channelPlaybookTypes[cpm]), nil
}
// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON)
func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error {
for i, st := range channelPlaybookTypes {
if st == string(text) {
*cpm = ChannelPlaybookMode(i)
return nil
}
}
return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
"time"
)
// PlaybookRunService handles communication with the playbook run related
// methods of the Playbooks API.
type PlaybookRunService struct {
client *Client
}
// Get a playbook run.
func (s *PlaybookRunService) Get(ctx context.Context, playbookRunID string) (*PlaybookRun, error) {
playbookRunURL := fmt.Sprintf("runs/%s", playbookRunID)
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// GetByChannelID gets a playbook run by ChannelID.
func (s *PlaybookRunService) GetByChannelID(ctx context.Context, channelID string) (*PlaybookRun, error) {
channelURL := fmt.Sprintf("runs/channel/%s", channelID)
req, err := s.client.newRequest(http.MethodGet, channelURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// Get a playbook run's metadata.
func (s *PlaybookRunService) GetMetadata(ctx context.Context, playbookRunID string) (*Metadata, error) {
playbookRunURL := fmt.Sprintf("runs/%s/metadata", playbookRunID)
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(Metadata)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// Get all playbook status updates.
func (s *PlaybookRunService) GetStatusUpdates(ctx context.Context, playbookRunID string) ([]StatusPostComplete, error) {
playbookRunURL := fmt.Sprintf("runs/%s/status-updates", playbookRunID)
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
var statusUpdates []StatusPostComplete
resp, err := s.client.do(ctx, req, &statusUpdates)
if err != nil {
return nil, err
}
resp.Body.Close()
return statusUpdates, nil
}
// List the playbook runs.
func (s *PlaybookRunService) List(ctx context.Context, page, perPage int, opts PlaybookRunListOptions) (*GetPlaybookRunsResults, error) {
playbookRunURL := "runs"
playbookRunURL, err := addOptions(playbookRunURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
playbookRunURL, err = addPaginationOptions(playbookRunURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetPlaybookRunsResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Create a playbook run.
func (s *PlaybookRunService) Create(ctx context.Context, opts PlaybookRunCreateOptions) (*PlaybookRun, error) {
playbookRunURL := "runs"
req, err := s.client.newRequest(http.MethodPost, playbookRunURL, opts)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("expected status code %d", http.StatusCreated)
}
return playbookRun, nil
}
func (s *PlaybookRunService) UpdateStatus(ctx context.Context, playbookRunID string, message string, reminderInSeconds int64) error {
updateURL := fmt.Sprintf("runs/%s/status", playbookRunID)
opts := StatusUpdateOptions{
Message: message,
Reminder: time.Duration(reminderInSeconds),
}
req, err := s.client.newRequest(http.MethodPost, updateURL, opts)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return nil
}
func (s *PlaybookRunService) RequestUpdate(ctx context.Context, playbookRunID, userID string) error {
requestURL := fmt.Sprintf("runs/%s/request-update", playbookRunID)
req, err := s.client.newRequest(http.MethodPost, requestURL, nil)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
func (s *PlaybookRunService) Finish(ctx context.Context, playbookRunID string) error {
finishURL := fmt.Sprintf("runs/%s/finish", playbookRunID)
req, err := s.client.newRequest(http.MethodPut, finishURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybookRunService) CreateChecklist(ctx context.Context, playbookRunID string, checklist Checklist) error {
createURL := fmt.Sprintf("runs/%s/checklists", playbookRunID)
req, err := s.client.newRequest(http.MethodPost, createURL, checklist)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RemoveChecklist(ctx context.Context, playbookRunID string, checklistNumber int) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d", playbookRunID, checklistNumber)
req, err := s.client.newRequest(http.MethodDelete, createURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RenameChecklist(ctx context.Context, playbookRunID string, checklistNumber int, newTitle string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/rename", playbookRunID, checklistNumber)
req, err := s.client.newRequest(http.MethodPut, createURL, struct{ Title string }{newTitle})
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) AddChecklistItem(ctx context.Context, playbookRunID string, checklistNumber int, checklistItem ChecklistItem) error {
addURL := fmt.Sprintf("runs/%s/checklists/%d/add", playbookRunID, checklistNumber)
req, err := s.client.newRequest(http.MethodPost, addURL, checklistItem)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) MoveChecklist(ctx context.Context, playbookRunID string, sourceChecklistIdx, destChecklistIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/move", playbookRunID)
body := struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
}{sourceChecklistIdx, destChecklistIdx}
req, err := s.client.newRequest(http.MethodPost, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) MoveChecklistItem(ctx context.Context, playbookRunID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/move-item", playbookRunID)
body := struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
SourceItemIdx int `json:"source_item_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
DestItemIdx int `json:"dest_item_idx"`
}{sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx}
req, err := s.client.newRequest(http.MethodPost, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
// UpdateRetrospective updates the run's retrospective info
func (s *PlaybookRunService) UpdateRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
createURL := fmt.Sprintf("runs/%s/retrospective", playbookRunID)
req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
// PublishRetrospective publishes the run's retrospective
func (s *PlaybookRunService) PublishRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
createURL := fmt.Sprintf("runs/%s/retrospective/publish", playbookRunID)
req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
func (s *PlaybookRunService) SetItemAssignee(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, assigneeID string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/assignee", playbookRunID, checklistIdx, itemIdx)
body := struct {
AssigneeID string `json:"assignee_id"`
}{assigneeID}
req, err := s.client.newRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) SetItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, newCommand string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/command", playbookRunID, checklistIdx, itemIdx)
body := struct {
Command string `json:"command"`
}{newCommand}
req, err := s.client.newRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RunItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/run", playbookRunID, checklistIdx, itemIdx)
req, err := s.client.newRequest(http.MethodPost, createURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) SetItemDueDate(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, duedate int64) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/duedate", playbookRunID, checklistIdx, itemIdx)
body := struct {
DueDate int64 `json:"due_date"`
}{duedate}
req, err := s.client.newRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
// Get a playbook run.
func (s *PlaybookRunService) GetOwners(ctx context.Context) ([]OwnerInfo, error) {
req, err := s.client.newRequest(http.MethodGet, "runs/owners", nil)
if err != nil {
return nil, err
}
owners := make([]OwnerInfo, 0)
resp, err := s.client.do(ctx, req, &owners)
if err != nil {
return nil, err
}
resp.Body.Close()
return owners, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"github.com/pkg/errors"
)
// PlaybooksService handles communication with the playbook related
// methods of the Playbook API.
type PlaybooksService struct {
client *Client
}
// Get a playbook.
func (s *PlaybooksService) Get(ctx context.Context, playbookID string) (*Playbook, error) {
playbookURL := fmt.Sprintf("playbooks/%s", playbookID)
req, err := s.client.newRequest(http.MethodGet, playbookURL, nil)
if err != nil {
return nil, err
}
playbook := new(Playbook)
resp, err := s.client.do(ctx, req, playbook)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbook, nil
}
// List the playbooks.
func (s *PlaybooksService) List(ctx context.Context, teamId string, page, perPage int, opts PlaybookListOptions) (*GetPlaybooksResults, error) {
playbookURL := "playbooks"
playbookURL, err := addOption(playbookURL, "team_id", teamId)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
playbookURL, err = addPaginationOptions(playbookURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
playbookURL, err = addOptions(playbookURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newRequest(http.MethodGet, playbookURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetPlaybooksResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Create a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Create(ctx context.Context, opts PlaybookCreateOptions) (string, error) {
// For ease of use set the default if not specificed so it doesn't just error
if opts.ReminderTimerDefaultSeconds == 0 {
opts.ReminderTimerDefaultSeconds = 86400
}
playbookURL := "playbooks"
req, err := s.client.newRequest(http.MethodPost, playbookURL, opts)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
func (s *PlaybooksService) Update(ctx context.Context, playbook Playbook) error {
updateURL := fmt.Sprintf("playbooks/%s", playbook.ID)
req, err := s.client.newRequest(http.MethodPut, updateURL, playbook)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) Archive(ctx context.Context, playbookID string) error {
updateURL := fmt.Sprintf("playbooks/%s", playbookID)
req, err := s.client.newRequest(http.MethodDelete, updateURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) Export(ctx context.Context, playbookID string) ([]byte, error) {
url := fmt.Sprintf("playbooks/%s/export", playbookID)
req, err := s.client.newRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := s.client.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected status code %d", http.StatusOK)
}
return result, nil
}
// Duplicate a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Duplicate(ctx context.Context, playbookID string) (string, error) {
url := fmt.Sprintf("playbooks/%s/duplicate", playbookID)
req, err := s.client.newRequest(http.MethodPost, url, nil)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
// Imports a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Import(ctx context.Context, toImport []byte, team string) (string, error) {
url := "playbooks/import?team_id=" + team
u, err := s.client.BaseURL.Parse(buildAPIURL(url))
if err != nil {
return "", errors.Wrapf(err, "invalid endpoint %s", url)
}
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(toImport))
if err != nil {
return "", errors.Wrapf(err, "failed to create http request for import")
}
req.Header.Set("Content-Type", "application/json")
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
func (s *PlaybooksService) Stats(ctx context.Context, playbookID string) (*PlaybookStats, error) {
playbookStatsURL := fmt.Sprintf("stats/playbook?playbook_id=%s", playbookID)
req, err := s.client.newRequest(http.MethodGet, playbookStatsURL, nil)
if err != nil {
return nil, err
}
stats := new(PlaybookStats)
resp, err := s.client.do(ctx, req, stats)
if err != nil {
return nil, err
}
resp.Body.Close()
return stats, nil
}
func (s *PlaybooksService) AutoFollow(ctx context.Context, playbookID string, userID string) error {
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
req, err := s.client.newRequest(http.MethodPut, followsURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) AutoUnfollow(ctx context.Context, playbookID string, userID string) error {
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
req, err := s.client.newRequest(http.MethodDelete, followsURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) GetAutoFollows(ctx context.Context, playbookID string) ([]string, error) {
autofollowsURL := fmt.Sprintf("playbooks/%s/autofollows", playbookID)
req, err := s.client.newRequest(http.MethodGet, autofollowsURL, nil)
if err != nil {
return nil, err
}
var followers []string
resp, err := s.client.do(ctx, req, &followers)
if err != nil {
return nil, err
}
resp.Body.Close()
return followers, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"io/ioutil"
"net/http"
)
type RemindersService struct {
client *Client
}
func (s *RemindersService) Reset(ctx context.Context, playbookRunID string, payload ReminderResetPayload) error {
resetURL := fmt.Sprintf("runs/%s/reminder", playbookRunID)
req, err := s.client.newRequest(http.MethodPost, resetURL, payload)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, ioutil.Discard)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("expected status code %d", http.StatusNoContent)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"net/http"
)
type GlobalSettings struct {
// EnableExperimentalFeatures is a read-only field set to true when experimental features
// are enabled. Changing this field requires access to the system console plugin
// configuration.
EnableExperimentalFeatures bool `json:"enable_experimental_features"`
}
// SettingsService handles communication with the settings related methods.
type SettingsService struct {
client *Client
}
// Get the configured settings.
func (s *SettingsService) Get(ctx context.Context) (*GlobalSettings, error) {
settingsURL := "settings"
req, err := s.client.newRequest(http.MethodGet, settingsURL, nil)
if err != nil {
return nil, err
}
settings := new(GlobalSettings)
resp, err := s.client.do(ctx, req, settings)
if err != nil {
return nil, err
}
resp.Body.Close()
return settings, nil
}
// Update the configured settings.
func (s *SettingsService) Update(ctx context.Context, settings GlobalSettings) error {
settingsURL := "settings"
req, err := s.client.newRequest(http.MethodPut, settingsURL, settings)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"net/http"
)
// StatsService handles communication with the stats related methods.
type StatsService struct {
client *Client
}
// PlaybookSiteStats holds the data that we want to expose in system console
type PlaybookSiteStats struct {
TotalPlaybooks int `json:"total_playbooks"`
TotalPlaybookRuns int `json:"total_playbook_runs"`
}
// Get the stats that should be displayed in system console.
func (s *StatsService) GetSiteStats(ctx context.Context) (*PlaybookSiteStats, error) {
statsURL := "stats/site"
req, err := s.client.newRequest(http.MethodGet, statsURL, nil)
if err != nil {
return nil, err
}
stats := new(PlaybookSiteStats)
resp, err := s.client.do(ctx, req, stats)
if err != nil {
return nil, err
}
resp.Body.Close()
return stats, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"io/ioutil"
"net/http"
)
type TelemetryService struct {
client *Client
}
func (s *TelemetryService) CreateEvent(ctx context.Context, name string, eventType string, properties map[string]interface{}) error {
payload := struct {
Type string
Name string
Properties map[string]interface{}
}{
Type: eventType,
Name: name,
Properties: properties,
}
req, err := s.client.newRequest(http.MethodPost, "telemetry", payload)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("expected status code %d, got %d: %s", http.StatusNoContent, resp.StatusCode, body)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/mattermost/mattermost-server/v6/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// normalizeAppError returns a truly nil error if appErr is nil
// See https://golang.org/doc/faq#nil_error for more details.
func normalizeAppErr(appErr *mm_model.AppError) error {
if appErr == nil {
return nil
}
if appErr.StatusCode == http.StatusNotFound {
return app.ErrNotFound
}
return appErr
}
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
// be used as per the Plugin API.
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
// can be modified to use the services in modular fashion.
type serviceAPIAdapter struct {
api *playbooksProduct
ctx *request.Context
}
func newServiceAPIAdapter(api *playbooksProduct) *serviceAPIAdapter {
return &serviceAPIAdapter{
api: api,
ctx: request.EmptyContext(api.logger),
}
}
//
// Channels service.
//
func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetChannelByID(channelID)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) {
member, appErr := a.api.channelService.GetChannelMember(channelID, userID)
return member, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) {
opts := &mm_model.ChannelSearchOpts{
IncludeDeleted: includeDeleted,
}
channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts)
return channels, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelSidebarCategories(userID, teamID string) (*mm_model.OrderedSidebarCategories, error) {
categories, appErr := a.api.channelService.GetChannelSidebarCategories(userID, teamID)
return categories, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetChannelMembers(channelID string, page, perPage int) (mm_model.ChannelMembers, error) {
channelMembers, appErr := a.api.channelService.GetChannelMembers(channelID, page, perPage)
return channelMembers, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
channels, appErr := a.api.channelService.CreateChannelSidebarCategory(userID, teamID, newCategory)
return channels, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) {
channels, appErr := a.api.channelService.UpdateChannelSidebarCategories(userID, teamID, categories)
return channels, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) CreateChannel(channel *mm_model.Channel) error {
_, appErr := a.api.channelService.CreateChannel(channel)
return normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) AddMemberToChannel(channelID, userID string) (*mm_model.ChannelMember, error) {
channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID)
return channelMember, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) AddUserToChannel(channelID, userID, asUserID string) (*mm_model.ChannelMember, error) {
channel, appErr := a.api.channelService.AddUserToChannel(channelID, userID, asUserID)
return channel, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*mm_model.ChannelMember, error) {
channelMember, appErr := a.api.channelService.UpdateChannelMemberRoles(channelID, userID, newRoles)
return channelMember, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) DeleteChannelMember(channelID, userID string) error {
appErr := a.api.channelService.DeleteChannelMember(channelID, userID)
return normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) AddChannelMember(channelID, userID string) (*mm_model.ChannelMember, error) {
channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID)
return channelMember, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2)
return channel, normalizeAppErr(appErr)
}
//
// Post service.
//
func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
createdPost, appErr := a.api.postService.CreatePost(a.ctx, post)
if appErr != nil {
return nil, normalizeAppErr(appErr)
}
err := createdPost.ShallowCopy(post)
if err != nil {
return nil, err
}
return post, nil
}
func (a *serviceAPIAdapter) GetPostsByIds(postIDs []string) ([]*mm_model.Post, error) {
post, _, appErr := a.api.postService.GetPostsByIds(postIDs)
return post, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) SendEphemeralPost(userID string, post *mm_model.Post) {
*post = *a.api.postService.SendEphemeralPost(a.ctx, userID, post)
}
func (a *serviceAPIAdapter) GetPost(postID string) (*mm_model.Post, error) {
post, appErr := a.api.postService.GetPost(postID)
return post, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) DeletePost(postID string) (*mm_model.Post, error) {
post, appErr := a.api.postService.DeletePost(a.ctx, postID, playbooksProductID)
return post, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdatePost(post *mm_model.Post) (*mm_model.Post, error) {
post, appErr := a.api.postService.UpdatePost(a.ctx, post, false)
return post, normalizeAppErr(appErr)
}
//
// User service.
//
func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUser(userID)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUserByUsername(name)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) {
user, appErr := a.api.userService.GetUserByEmail(email)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) {
user, appErr := a.api.userService.UpdateUser(a.ctx, user, true)
return user, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) {
user, appErr := a.api.userService.GetUsersFromProfiles(options)
return user, normalizeAppErr(appErr)
}
//
// Team service.
//
func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) {
member, appErr := a.api.teamService.GetMember(teamID, userID)
return member, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) {
member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID)
return member, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetGroup(groupID string) (*model.Group, error) {
group, appErr := a.api.teamService.GetGroup(groupID)
return group, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetTeam(teamID string) (*mm_model.Team, error) {
team, appErr := a.api.teamService.GetTeam(teamID)
return team, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) GetGroupMemberUsers(groupID string, page, perPage int) ([]*mm_model.User, error) {
users, appErr := a.api.teamService.GetGroupMemberUsers(groupID, page, perPage)
return users, normalizeAppErr(appErr)
}
//
// Permissions service.
//
func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionTo(userID, permission)
}
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
}
func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool {
return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission)
}
func (a *serviceAPIAdapter) RolesGrantPermission(roleNames []string, permissionID string) bool {
return a.api.permissionsService.RolesGrantPermission(roleNames, permissionID)
}
//
// Bot service.
//
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
return a.api.botService.EnsureBot(a.ctx, playbooksProductID, bot)
}
//
// License service.
//
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
return a.api.licenseService.GetLicense()
}
func (a *serviceAPIAdapter) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) error {
return normalizeAppErr(a.api.licenseService.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted))
}
//
// FileInfoStore service.
//
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
return fi, normalizeAppErr(appErr)
}
//
// Cluster store.
//
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
a.api.clusterService.PublishWebSocketEvent(playbooksProductID, event, payload, broadcast)
}
func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error {
return a.api.clusterService.PublishPluginClusterEvent(playbooksProductID, ev, opts)
}
//
// Cloud service.
//
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
return a.api.cloudService.GetCloudLimits()
}
//
// Config service.
//
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
cfg := a.api.configService.Config().Clone()
cfg.Sanitize()
return cfg
}
func (a *serviceAPIAdapter) LoadPluginConfiguration(dest any) error {
finalConfig := make(map[string]any)
// If we have settings given we override the defaults with them
for setting, value := range a.api.configService.Config().PluginSettings.Plugins[playbooksProductID] {
finalConfig[strings.ToLower(setting)] = value
}
pluginSettingsJSONBytes, err := json.Marshal(finalConfig)
if err != nil {
logrus.WithError(err).Error("Error marshaling config for plugin")
return nil
}
err = json.Unmarshal(pluginSettingsJSONBytes, dest)
if err != nil {
logrus.WithError(err).Error("Error unmarshaling config for plugin")
}
return nil
}
func (a *serviceAPIAdapter) SavePluginConfig(pluginConfig map[string]any) error {
cfg := a.GetConfig()
cfg.PluginSettings.Plugins["playbooks"] = pluginConfig
_, _, err := a.api.configService.SaveConfig(cfg, true)
return normalizeAppErr(err)
}
//
// KVStore service.
//
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(playbooksProductID, key, value, options)
return b, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) KVGet(key string) ([]byte, error) {
data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key)
return data, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) KVDelete(key string) error {
appErr := a.api.kvStoreService.KVDelete(playbooksProductID, key)
return normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) KVList(page, perPage int) ([]string, error) {
data, appErr := a.api.kvStoreService.KVList(playbooksProductID, page, perPage)
return data, normalizeAppErr(appErr)
}
// Get gets the value for the given key into the given interface.
//
// An error is returned only if the value cannot be fetched. A non-existent key will return no
// error, with nothing written to the given interface.
//
// Minimum server version: 5.2
func (a *serviceAPIAdapter) Get(key string, o interface{}) error {
data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key)
if appErr != nil {
return normalizeAppErr(appErr)
}
if len(data) == 0 {
return nil
}
if bytesOut, ok := o.(*[]byte); ok {
*bytesOut = data
return nil
}
if err := json.Unmarshal(data, o); err != nil {
return errors.Wrapf(err, "failed to unmarshal value for key %s", key)
}
return nil
}
//
// Store service.
//
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
return a.api.storeService.GetMasterDB(), nil
}
// DriverName returns the driver name for the datasource.
func (a *serviceAPIAdapter) DriverName() string {
return *a.api.configService.Config().SqlSettings.DriverName
}
//
// System service.
//
func (a *serviceAPIAdapter) GetDiagnosticID() string {
return a.api.systemService.GetDiagnosticId()
}
func (a *serviceAPIAdapter) GetServerVersion() string {
return model.CurrentVersion
}
//
// Router service.
//
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
a.api.routerService.RegisterRouter(playbooksProductName, sub)
}
//
// Preferences service.
//
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
return p, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
//
// Session service.
//
func (a *serviceAPIAdapter) GetSession(sessionID string) (*mm_model.Session, error) {
session, appErr := a.api.sessionService.GetSessionById(sessionID)
return session, normalizeAppErr(appErr)
}
//
// Frontend service.
//
func (a *serviceAPIAdapter) OpenInteractiveDialog(dialog model.OpenDialogRequest) error {
return normalizeAppErr(a.api.frontendService.OpenInteractiveDialog(dialog))
}
//
// Command service.
//
func (a *serviceAPIAdapter) Execute(command *mm_model.CommandArgs) (*mm_model.CommandResponse, error) {
user, err := a.GetUserByID(command.UserId)
if err != nil {
return nil, err
}
command.T = i18n.GetUserTranslations(user.Locale)
command.SiteURL = *a.GetConfig().ServiceSettings.SiteURL
response, appErr := a.api.commandService.ExecuteCommand(a.ctx, command)
return response, normalizeAppErr(appErr)
}
func (a *serviceAPIAdapter) RegisterCommand(command *mm_model.Command) error {
return a.api.commandService.RegisterProductCommand(playbooksProductName, command)
}
func (a *serviceAPIAdapter) IsEnterpriseReady() bool {
result, _ := strconv.ParseBool(model.BuildEnterpriseReady)
return result
}
//
// Threads service
//
func (a *serviceAPIAdapter) RegisterCollectionAndTopic(collectionType, topicType string) error {
return a.api.threadsService.RegisterCollectionAndTopic(playbooksProductID, collectionType, topicType)
}
// Ensure the adapter implements ServicesAPI.
var _ playbooks.ServicesAPI = &serviceAPIAdapter{}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"fmt"
"io"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/sirupsen/logrus"
)
// LogrusHook is a logrus.Hook for emitting plugin logs through the RPC API for inclusion in the
// server logs.
//
// To configure the default Logrus logger for use with plugin logging, simply invoke:
//
// pluginapi.ConfigureLogrus(logrus.StandardLogger(), pluginAPIClient)
//
// Alternatively, construct your own logger to pass to pluginapi.ConfigureLogrus.
type LogrusHook struct {
log mlog.LoggerIFace
}
// NewLogrusHook creates a new instance of LogrusHook.
func NewLogrusHook(log mlog.LoggerIFace) *LogrusHook {
return &LogrusHook{
log: log,
}
}
// Levels allows LogrusHook to process any log level.
func (lh *LogrusHook) Levels() []logrus.Level {
return logrus.AllLevels
}
// Fire proxies logrus entries through the plugin API at the appropriate level.
func (lh *LogrusHook) Fire(entry *logrus.Entry) error {
fields := []logr.Field{}
for key, value := range entry.Data {
field := logr.Field{
Key: key,
Interface: value,
}
if key == "error" {
field.Type = logr.ErrorType
}
fields = append(fields, field)
}
if entry.Caller != nil {
fields = append(fields,
logr.Field{
Key: "plugin_caller",
String: fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line),
})
}
switch entry.Level {
case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel:
lh.log.Error(entry.Message, fields...)
case logrus.WarnLevel:
lh.log.Warn(entry.Message, fields...)
case logrus.InfoLevel:
lh.log.Info(entry.Message, fields...)
case logrus.DebugLevel, logrus.TraceLevel:
lh.log.Debug(entry.Message, fields...)
}
return nil
}
// ConfigureLogrus configures the given logrus logger with a hook to proxy through the RPC API,
// discarding the default output to avoid duplicating the events across the standard STDOUT proxy.
func ConfigureLogrus(logger *logrus.Logger, log mlog.LoggerIFace) {
hook := NewLogrusHook(log)
logger.Hooks.Add(hook)
logger.SetOutput(io.Discard)
logrus.SetReportCaller(true)
// By default, log everything to the server, and let it decide what gets through.
logrus.SetLevel(logrus.TraceLevel)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
mmapp "github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/playbooks/product/pluginapi/cluster"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/api"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/command"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/enterprise"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/metrics"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/scheduler"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/telemetry"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
playbooksProductName = "playbooks"
playbooksProductID = "playbooks"
)
const (
updateMetricsTaskFrequency = 15 * time.Minute
metricsExposePort = ":9093"
// Topic represents a start of a thread. In playbooks we support 2 types of topics:
// status topic - indicating the start of the thread below status update and
// task topic - indicating the start of the thread below task(checklist item)
TopicTypeStatus = "status"
TopicTypeTask = "task"
// Collection is a group of topics and their corresponding threads.
// In Playbooks we support a single type of collection - a run
CollectionTypeRun = "run"
)
const ServerKey product.ServiceKey = "server"
// These credentials for Rudder need to be replaced at build-time.
const (
rudderDataplaneURL = "placeholder_rudder_dataplane_url"
rudderWriteKey = "placeholder_playbooks_rudder_key"
)
var errServiceTypeAssert = errors.New("type assertion failed")
type TelemetryClient interface {
app.PlaybookRunTelemetry
app.PlaybookTelemetry
app.GenericTelemetry
bot.Telemetry
app.UserInfoTelemetry
app.ChannelActionTelemetry
app.CategoryTelemetry
Enable() error
Disable() error
}
func init() {
product.RegisterProduct(playbooksProductName, product.Manifest{
Initializer: newPlaybooksProduct,
Dependencies: map[product.ServiceKey]struct{}{
product.TeamKey: {},
product.ChannelKey: {},
product.UserKey: {},
product.PostKey: {},
product.BotKey: {},
product.ClusterKey: {},
product.ConfigKey: {},
product.LogKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
product.FileInfoStoreKey: {},
product.RouterKey: {},
product.CloudKey: {},
product.KVStoreKey: {},
product.StoreKey: {},
product.SystemKey: {},
product.PreferencesKey: {},
product.SessionKey: {},
product.FrontendKey: {},
product.CommandKey: {},
product.ThreadsKey: {},
},
})
}
type playbooksProduct struct {
server *mmapp.Server
teamService product.TeamService
channelService product.ChannelService
userService product.UserService
postService product.PostService
permissionsService product.PermissionService
botService product.BotService
clusterService product.ClusterService
configService product.ConfigService
logger mlog.LoggerIFace
licenseService product.LicenseService
filestoreService product.FilestoreService
fileInfoStoreService product.FileInfoStoreService
routerService product.RouterService
cloudService product.CloudService
kvStoreService product.KVStoreService
storeService product.StoreService
systemService product.SystemService
preferencesService product.PreferencesService
hooksService product.HooksService
sessionService product.SessionService
frontendService product.FrontendService
commandService product.CommandService
threadsService product.ThreadsService
handler *api.Handler
config *config.ServiceImpl
playbookRunService app.PlaybookRunService
playbookService app.PlaybookService
permissions *app.PermissionsService
channelActionService app.ChannelActionService
categoryService app.CategoryService
bot *bot.Bot
userInfoStore app.UserInfoStore
telemetryClient TelemetryClient
licenseChecker app.LicenseChecker
metricsService *metrics.Metrics
playbookStore app.PlaybookStore
playbookRunStore app.PlaybookRunStore
metricsServer *metrics.Service
metricsUpdaterTask *scheduler.ScheduledTask
serviceAdapter playbooks.ServicesAPI
}
func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.Product, error) {
playbooks := &playbooksProduct{}
err := playbooks.setProductServices(services)
if err != nil {
return nil, err
}
playbooks.server = services[ServerKey].(*mmapp.Server)
playbooks.serviceAdapter = newServiceAPIAdapter(playbooks)
return playbooks, nil
}
func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]interface{}) error {
for key, service := range services {
switch key {
case product.TeamKey:
teamService, ok := service.(product.TeamService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.teamService = teamService
case product.ChannelKey:
channelService, ok := service.(product.ChannelService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.channelService = channelService
case product.UserKey:
userService, ok := service.(product.UserService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.userService = userService
case product.PostKey:
postService, ok := service.(product.PostService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.postService = postService
case product.PermissionsKey:
permissionsService, ok := service.(product.PermissionService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.permissionsService = permissionsService
case product.BotKey:
botService, ok := service.(product.BotService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.botService = botService
case product.ClusterKey:
clusterService, ok := service.(product.ClusterService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.clusterService = clusterService
case product.ConfigKey:
configService, ok := service.(product.ConfigService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.configService = configService
case product.LogKey:
logger, ok := service.(mlog.LoggerIFace)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.logger = logger.With(mlog.String("product", playbooksProductName))
case product.LicenseKey:
licenseService, ok := service.(product.LicenseService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.licenseService = licenseService
case product.FilestoreKey:
filestoreService, ok := service.(product.FilestoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.filestoreService = filestoreService
case product.FileInfoStoreKey:
fileInfoStoreService, ok := service.(product.FileInfoStoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.fileInfoStoreService = fileInfoStoreService
case product.RouterKey:
routerService, ok := service.(product.RouterService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.routerService = routerService
case product.CloudKey:
cloudService, ok := service.(product.CloudService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.cloudService = cloudService
case product.KVStoreKey:
kvStoreService, ok := service.(product.KVStoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.kvStoreService = kvStoreService
case product.StoreKey:
storeService, ok := service.(product.StoreService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.storeService = storeService
case product.SystemKey:
systemService, ok := service.(product.SystemService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.systemService = systemService
case product.PreferencesKey:
preferencesService, ok := service.(product.PreferencesService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.preferencesService = preferencesService
case product.HooksKey:
hooksService, ok := service.(product.HooksService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.hooksService = hooksService
case product.SessionKey:
sessionService, ok := service.(product.SessionService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.sessionService = sessionService
case product.FrontendKey:
frontendService, ok := service.(product.FrontendService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.frontendService = frontendService
case product.CommandKey:
commandService, ok := service.(product.CommandService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.commandService = commandService
case product.ThreadsKey:
threadsService, ok := service.(product.ThreadsService)
if !ok {
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
pp.threadsService = threadsService
}
}
return nil
}
func (pp *playbooksProduct) Start() error {
logger := logrus.StandardLogger()
ConfigureLogrus(logger, pp.logger)
botID, err := pp.serviceAdapter.EnsureBot(&model.Bot{
Username: "playbooks",
DisplayName: "Playbooks",
Description: "Playbooks bot.",
OwnerId: "playbooks",
})
if err != nil {
return errors.Wrapf(err, "failed to ensure bot")
}
pp.config = config.NewConfigService(pp.serviceAdapter)
err = pp.config.UpdateConfiguration(func(c *config.Configuration) {
c.BotUserID = botID
c.AdminLogLevel = "debug"
})
if err != nil {
return errors.Wrapf(err, "failed save bot to config")
}
pp.handler = api.NewHandler(pp.config)
if strings.HasPrefix(rudderWriteKey, "placeholder_") {
logrus.Warn("Rudder credentials are not set. Disabling analytics.")
pp.telemetryClient = &telemetry.NoopTelemetry{}
} else {
logrus.Info("Rudder credentials are set. Enabling analytics.")
diagnosticID := pp.serviceAdapter.GetDiagnosticID()
serverVersion := pp.serviceAdapter.GetServerVersion()
pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion)
if err != nil {
return errors.Wrapf(err, "failed init telemetry client")
}
}
toggleTelemetry := func() {
diagnosticsFlag := pp.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics
telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag
if telemetryEnabled {
if err = pp.telemetryClient.Enable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be enabled")
}
return
}
if err = pp.telemetryClient.Disable(); err != nil {
logrus.WithError(err).Error("Telemetry could not be disabled")
}
}
toggleTelemetry()
pp.config.RegisterConfigChangeListener(toggleTelemetry)
apiClient := sqlstore.NewClient(pp.serviceAdapter)
pp.bot = bot.New(pp.serviceAdapter, pp.config.GetConfiguration().BotUserID, pp.config, pp.telemetryClient)
scheduler := cluster.GetJobOnceScheduler(pp.serviceAdapter)
sqlStore, err := sqlstore.New(apiClient, scheduler)
if err != nil {
return errors.Wrapf(err, "failed creating the SQL store")
}
pp.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore)
pp.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore)
statsStore := sqlstore.NewStatsStore(apiClient, sqlStore)
pp.userInfoStore = sqlstore.NewUserInfoStore(sqlStore)
channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore)
categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore)
pp.handler = api.NewHandler(pp.config)
pp.playbookService = app.NewPlaybookService(pp.playbookStore, pp.bot, pp.telemetryClient, pp.serviceAdapter, pp.metricsService)
keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer()
pp.channelActionService = app.NewChannelActionsService(pp.serviceAdapter, pp.bot, pp.config, channelActionStore, pp.playbookService, keywordsThreadIgnorer, pp.telemetryClient)
pp.categoryService = app.NewCategoryService(categoryStore, pp.serviceAdapter, pp.telemetryClient)
pp.licenseChecker = enterprise.NewLicenseChecker(pp.serviceAdapter)
pp.playbookRunService = app.NewPlaybookRunService(
pp.playbookRunStore,
pp.bot,
pp.config,
scheduler,
pp.telemetryClient,
pp.telemetryClient,
pp.serviceAdapter,
pp.playbookService,
pp.channelActionService,
pp.licenseChecker,
pp.metricsService,
)
if err = scheduler.SetCallback(pp.playbookRunService.HandleReminder); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder")
}
if err = scheduler.Start(); err != nil {
logrus.WithError(err).Error("JobOnceScheduler could not start")
}
// Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started
mutex, err := cluster.NewMutex(pp.serviceAdapter, "IR_dbMutex")
if err != nil {
return errors.Wrapf(err, "failed creating cluster mutex")
}
mutex.Lock()
if err = sqlStore.RunMigrations(); err != nil {
mutex.Unlock()
return errors.Wrapf(err, "failed to run migrations")
}
mutex.Unlock()
pp.permissions = app.NewPermissionsService(
pp.playbookService,
pp.playbookRunService,
pp.serviceAdapter,
pp.config,
pp.licenseChecker,
)
// register collections and topics.
// TODO bump the minimum server version
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic")
}
if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil {
logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic")
}
api.NewGraphQLHandler(
pp.handler.APIRouter,
pp.playbookService,
pp.playbookRunService,
pp.categoryService,
pp.serviceAdapter,
pp.config,
pp.permissions,
pp.playbookStore,
pp.licenseChecker,
)
api.NewPlaybookHandler(
pp.handler.APIRouter,
pp.playbookService,
pp.serviceAdapter,
pp.config,
pp.permissions,
)
api.NewPlaybookRunHandler(
pp.handler.APIRouter,
pp.playbookRunService,
pp.playbookService,
pp.permissions,
pp.licenseChecker,
pp.serviceAdapter,
pp.bot,
pp.config,
)
api.NewStatsHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
statsStore,
pp.playbookService,
pp.permissions,
pp.licenseChecker,
)
api.NewBotHandler(
pp.handler.APIRouter,
pp.serviceAdapter, pp.bot,
pp.config,
pp.playbookRunService,
pp.userInfoStore,
)
api.NewTelemetryHandler(
pp.handler.APIRouter,
pp.playbookRunService,
pp.serviceAdapter,
pp.telemetryClient,
pp.playbookService,
pp.telemetryClient,
pp.telemetryClient,
pp.telemetryClient,
pp.permissions,
)
api.NewSignalHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.playbookRunService,
pp.playbookService,
keywordsThreadIgnorer,
)
api.NewSettingsHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.config,
)
api.NewActionsHandler(
pp.handler.APIRouter,
pp.channelActionService,
pp.serviceAdapter,
pp.permissions,
)
api.NewCategoryHandler(
pp.handler.APIRouter,
pp.serviceAdapter,
pp.categoryService,
pp.playbookService,
pp.playbookRunService,
)
isTestingEnabled := false
flag := pp.serviceAdapter.GetConfig().ServiceSettings.EnableTesting
if flag != nil {
isTestingEnabled = *flag
}
if err = command.RegisterCommands(pp.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil {
return errors.Wrapf(err, "failed register commands")
}
if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil {
return fmt.Errorf("failed to register hooks: %w", err)
}
enableMetrics := pp.configService.Config().MetricsSettings.Enable
if enableMetrics != nil && *enableMetrics {
pp.metricsService = newMetricsInstance()
// run metrics server to expose data
pp.runMetricsServer()
// run metrics updater recurring task
pp.runMetricsUpdaterTask(pp.playbookStore, pp.playbookRunStore, updateMetricsTaskFrequency)
// set error counter middleware handler
pp.handler.APIRouter.Use(pp.getErrorCounterHandler())
}
pp.routerService.RegisterRouter(playbooksProductName, pp.handler.APIRouter)
logrus.Debug("Playbooks product successfully started.")
return nil
}
func (pp *playbooksProduct) Stop() error {
if pp.metricsServer != nil {
err := pp.metricsServer.Shutdown()
if err != nil {
logrus.WithError(err).Warn("unable to shut down metric server")
}
}
if pp.metricsUpdaterTask != nil {
pp.metricsUpdaterTask.Cancel()
}
return nil
}
func newMetricsInstance() *metrics.Metrics {
// Init metrics
instanceInfo := metrics.InstanceInfo{
Version: model.BuildHashPlaybooks,
InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"),
}
return metrics.NewMetrics(instanceInfo)
}
func (pp *playbooksProduct) runMetricsServer() {
logrus.WithField("port", metricsExposePort).Info("Starting Playbooks metrics server")
pp.metricsServer = metrics.NewMetricsServer(metricsExposePort, pp.metricsService)
// Run server to expose metrics
go func() {
err := pp.metricsServer.Run()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
logrus.WithError(err).Error("Metrics server could not be started")
}
}()
}
func (pp *playbooksProduct) runMetricsUpdaterTask(playbookStore app.PlaybookStore, playbookRunStore app.PlaybookRunStore, updateMetricsTaskFrequency time.Duration) {
metricsUpdater := func() {
if playbooksActiveTotal, err := playbookStore.GetPlaybooksActiveTotal(); err == nil {
pp.metricsService.ObservePlaybooksActiveTotal(playbooksActiveTotal)
} else {
logrus.WithError(err).Error("error updating metrics, playbooks_active_total")
}
if runsActiveTotal, err := playbookRunStore.GetRunsActiveTotal(); err == nil {
pp.metricsService.ObserveRunsActiveTotal(runsActiveTotal)
} else {
logrus.WithError(err).Error("error updating metrics, runs_active_total")
}
if remindersOverdueTotal, err := playbookRunStore.GetOverdueUpdateRunsTotal(); err == nil {
pp.metricsService.ObserveRemindersOutstandingTotal(remindersOverdueTotal)
} else {
logrus.WithError(err).Error("error updating metrics, reminders_outstanding_total")
}
if retrosOverdueTotal, err := playbookRunStore.GetOverdueRetroRunsTotal(); err == nil {
pp.metricsService.ObserveRetrosOutstandingTotal(retrosOverdueTotal)
} else {
logrus.WithError(err).Error("error updating metrics, retros_outstanding_total")
}
if followersActiveTotal, err := playbookRunStore.GetFollowersActiveTotal(); err == nil {
pp.metricsService.ObserveFollowersActiveTotal(followersActiveTotal)
} else {
logrus.WithError(err).Error("error updating metrics, followers_active_total")
}
if participantsActiveTotal, err := playbookRunStore.GetParticipantsActiveTotal(); err == nil {
pp.metricsService.ObserveParticipantsActiveTotal(participantsActiveTotal)
} else {
logrus.WithError(err).Error("error updating metrics, participants_active_total")
}
}
pp.metricsUpdaterTask = scheduler.CreateRecurringTask("metricsUpdater", metricsUpdater, updateMetricsTaskFrequency)
}
func (pp *playbooksProduct) getErrorCounterHandler() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := &StatusRecorder{
ResponseWriter: w,
Status: 200,
}
next.ServeHTTP(recorder, r)
if recorder.Status < 200 || recorder.Status > 299 {
pp.metricsService.IncrementErrorsCount(1)
}
})
}
}
type StatusRecorder struct {
http.ResponseWriter
Status int
}
func (r *StatusRecorder) WriteHeader(status int) {
r.Status = status
r.ResponseWriter.WriteHeader(status)
}
// ServeHTTP routes incoming HTTP requests to the plugin's REST API.
func (pp *playbooksProduct) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
pp.handler.ServeHTTP(w, r)
}
//
// These callbacks are called by the suite automatically
//
func (pp *playbooksProduct) OnConfigurationChange() error {
if pp.config == nil {
return nil
}
return pp.config.OnConfigurationChange()
}
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand.
func (pp *playbooksProduct) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
runner := command.NewCommandRunner(c, args, pp.serviceAdapter, pp.bot,
pp.playbookRunService, pp.playbookService, pp.config, pp.userInfoStore, pp.telemetryClient, pp.permissions)
if err := runner.Execute(); err != nil {
return nil, model.NewAppError("Playbooks.ExecuteCommand", "app.command.execute.error", nil, err.Error(), http.StatusInternalServerError)
}
return &model.CommandResponse{}, nil
}
func (pp *playbooksProduct) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
actorID := ""
if actor != nil && actor.Id != channelMember.UserId {
actorID = actor.Id
}
pp.channelActionService.UserHasJoinedChannel(channelMember.UserId, channelMember.ChannelId, actorID)
}
func (pp *playbooksProduct) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
pp.channelActionService.MessageHasBeenPosted(post)
pp.playbookRunService.MessageHasBeenPosted(post)
}
func (pp *playbooksProduct) UserHasPermissionToCollection(c *plugin.Context, userID string, collectionType, collectionID string, permission *model.Permission) (bool, error) {
if collectionType != CollectionTypeRun {
return false, errors.Errorf("collection %s is not registered by playbooks", collectionType)
}
run, err := pp.playbookRunService.GetPlaybookRun(collectionID)
if err != nil {
return false, errors.Wrapf(err, "No run with id - %s", collectionID)
}
return pp.permissions.HasPermissionsToRun(userID, run, permission), nil
}
func (pp *playbooksProduct) GetAllCollectionIDsForUser(c *plugin.Context, userID, collectionType string) ([]string, error) {
if collectionType != CollectionTypeRun {
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
}
ids, err := pp.playbookRunService.GetPlaybookRunIDsForUser(userID)
if err != nil {
return nil, err
}
return ids, nil
}
func (pp *playbooksProduct) GetAllUserIdsForCollection(c *plugin.Context, collectionType, collectionID string) ([]string, error) {
if collectionType != CollectionTypeRun {
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
}
run, err := pp.playbookRunService.GetPlaybookRun(collectionID)
if err != nil {
return nil, errors.Wrapf(err, "No run with id - %s", collectionID)
}
followers, err := pp.playbookRunService.GetFollowers(collectionID)
if err != nil {
return nil, errors.Wrapf(err, "can't get followers for run - %s", collectionID)
}
return mergeSlice(run.ParticipantIDs, followers), nil
}
func (pp *playbooksProduct) GetCollectionMetadataByIds(c *plugin.Context, collectionType string, collectionIDs []string) (map[string]*model.CollectionMetadata, error) {
if collectionType != CollectionTypeRun {
return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType)
}
runsMetadata := map[string]*model.CollectionMetadata{}
runs, err := pp.playbookRunService.GetRunMetadataByIDs(collectionIDs)
if err != nil {
return nil, errors.Wrap(err, "can't get playbook run metadata by ids")
}
for _, run := range runs {
runsMetadata[run.ID] = &model.CollectionMetadata{
Id: run.ID,
CollectionType: CollectionTypeRun,
TeamId: run.TeamID,
Name: run.Name,
RelativeURL: app.GetRunDetailsRelativeURL(run.ID),
}
}
return runsMetadata, nil
}
func (pp *playbooksProduct) GetTopicMetadataByIds(c *plugin.Context, topicType string, topicIDs []string) (map[string]*model.TopicMetadata, error) {
topicsMetadata := map[string]*model.TopicMetadata{}
var getTopicMetadataByIDs func(topicIDs []string) ([]app.TopicMetadata, error)
switch topicType {
case TopicTypeStatus:
getTopicMetadataByIDs = pp.playbookRunService.GetStatusMetadataByIDs
case TopicTypeTask:
getTopicMetadataByIDs = pp.playbookRunService.GetTaskMetadataByIDs
default:
return map[string]*model.TopicMetadata{}, errors.Errorf("topic type %s is not registered by playbooks", topicType)
}
topics, err := getTopicMetadataByIDs(topicIDs)
if err != nil {
return nil, errors.Wrap(err, "can't get metadata by topic ids")
}
for _, topic := range topics {
topicsMetadata[topic.ID] = &model.TopicMetadata{
Id: topic.ID,
TopicType: topicType,
CollectionType: CollectionTypeRun,
TeamId: topic.TeamID,
CollectionId: topic.RunID,
}
}
return topicsMetadata, nil
}
func mergeSlice(a, b []string) []string {
m := make(map[string]struct{}, len(a)+len(b))
for _, elem := range a {
m[elem] = struct{}{}
}
for _, elem := range b {
m[elem] = struct{}{}
}
merged := make([]string, 0, len(m))
for key := range m {
merged = append(merged, key)
}
return merged
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cluster
import (
"encoding/json"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// cronPrefix is used to namespace key values created for a job from other key values
// created by a plugin.
cronPrefix = "cron_"
)
// JobPluginAPI is the plugin API interface required to schedule jobs.
type JobPluginAPI interface {
MutexPluginAPI
KVGet(key string) ([]byte, error)
KVDelete(key string) error
KVList(page, count int) ([]string, error)
}
// JobConfig defines the configuration of a scheduled job.
type JobConfig struct {
// Interval is the period of execution for the job.
Interval time.Duration
}
// NextWaitInterval is a callback computing the next wait interval for a job.
type NextWaitInterval func(now time.Time, metadata JobMetadata) time.Duration
// MakeWaitForInterval creates a function to scheduling a job to run on the given interval relative
// to the last finished timestamp.
//
// For example, if the job first starts at 12:01 PM, and is configured with interval 5 minutes,
// it will next run at:
//
// 12:06, 12:11, 12:16, ...
//
// If the job has not previously started, it will run immediately.
func MakeWaitForInterval(interval time.Duration) NextWaitInterval {
if interval == 0 {
panic("must specify non-zero ready interval")
}
return func(now time.Time, metadata JobMetadata) time.Duration {
sinceLastFinished := now.Sub(metadata.LastFinished)
if sinceLastFinished < interval {
return interval - sinceLastFinished
}
return 0
}
}
// MakeWaitForRoundedInterval creates a function, scheduling a job to run on the nearest rounded
// interval relative to the last finished timestamp.
//
// For example, if the job first starts at 12:04 PM, and is configured with interval 5 minutes,
// and is configured to round to 5 minute intervals, it will next run at:
//
// 12:05 PM, 12:10 PM, 12:15 PM, ...
//
// If the job has not previously started, it will run immediately. Note that this wait interval
// strategy does not guarantee a minimum interval between runs, only that subsequent runs will be
// scheduled on the rounded interval.
func MakeWaitForRoundedInterval(interval time.Duration) NextWaitInterval {
if interval == 0 {
panic("must specify non-zero ready interval")
}
return func(now time.Time, metadata JobMetadata) time.Duration {
if metadata.LastFinished.IsZero() {
return 0
}
target := metadata.LastFinished.Add(interval).Truncate(interval)
untilTarget := target.Sub(now)
if untilTarget > 0 {
return untilTarget
}
return 0
}
}
// Job is a scheduled job whose callback function is executed on a configured interval by at most
// one plugin instance at a time.
//
// Use scheduled jobs to perform background activity on a regular interval without having to
// explicitly coordinate with other instances of the same plugin that might repeat that effort.
type Job struct {
pluginAPI JobPluginAPI
key string
mutex *Mutex
nextWaitInterval NextWaitInterval
callback func()
stopOnce sync.Once
stop chan bool
done chan bool
}
// JobMetadata persists metadata about job execution.
type JobMetadata struct {
// LastFinished is the last time the job finished anywhere in the cluster.
LastFinished time.Time
}
// Schedule creates a scheduled job.
func Schedule(pluginAPI JobPluginAPI, key string, nextWaitInterval NextWaitInterval, callback func()) (*Job, error) {
key = cronPrefix + key
mutex, err := NewMutex(pluginAPI, key)
if err != nil {
return nil, errors.Wrap(err, "failed to create job mutex")
}
job := &Job{
pluginAPI: pluginAPI,
key: key,
mutex: mutex,
nextWaitInterval: nextWaitInterval,
callback: callback,
stop: make(chan bool),
done: make(chan bool),
}
go job.run()
return job, nil
}
// readMetadata reads the job execution metadata from the kv store.
func (j *Job) readMetadata() (JobMetadata, error) {
data, appErr := j.pluginAPI.KVGet(j.key)
if appErr != nil {
return JobMetadata{}, errors.Wrap(appErr, "failed to read data")
}
if data == nil {
return JobMetadata{}, nil
}
var metadata JobMetadata
err := json.Unmarshal(data, &metadata)
if err != nil {
return JobMetadata{}, errors.Wrap(err, "failed to decode data")
}
return metadata, nil
}
// saveMetadata writes updated job execution metadata from the kv store.
//
// It is assumed that the job mutex is held, negating the need to require an atomic write.
func (j *Job) saveMetadata(metadata JobMetadata) error {
data, err := json.Marshal(metadata)
if err != nil {
return errors.Wrap(err, "failed to marshal data")
}
ok, appErr := j.pluginAPI.KVSetWithOptions(j.key, data, model.PluginKVSetOptions{})
if appErr != nil || !ok {
return errors.Wrap(appErr, "failed to set data")
}
return nil
}
// run attempts to run the scheduled job, guaranteeing only one instance is executing concurrently.
func (j *Job) run() {
defer close(j.done)
var waitInterval time.Duration
for {
select {
case <-j.stop:
return
case <-time.After(waitInterval):
}
func() {
// Acquire the corresponding job lock and hold it throughout execution.
j.mutex.Lock()
defer j.mutex.Unlock()
metadata, err := j.readMetadata()
if err != nil {
logrus.WithError(err).WithField("key", j.key).Error("failed to read job metadata")
waitInterval = nextWaitInterval(waitInterval, err)
return
}
// Is it time to run the job?
waitInterval = j.nextWaitInterval(time.Now(), metadata)
if waitInterval > 0 {
return
}
// Run the job
j.callback()
metadata.LastFinished = time.Now()
err = j.saveMetadata(metadata)
if err != nil {
logrus.WithError(err).WithField("key", j.key).Error("failed to write job data")
}
waitInterval = j.nextWaitInterval(time.Now(), metadata)
}()
}
}
// Close terminates a scheduled job, preventing it from being scheduled on this plugin instance.
func (j *Job) Close() error {
j.stopOnce.Do(func() {
close(j.stop)
})
<-j.done
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cluster
import (
"encoding/json"
"math/rand"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
)
const (
// oncePrefix is used to namespace key values created for a scheduleOnce job
oncePrefix = "once_"
// keysPerPage is the maximum number of keys to retrieve from the db per call
keysPerPage = 1000
// maxNumFails is the maximum number of KVStore read fails or failed attempts to run the
// callback until the scheduler cancels a job.
maxNumFails = 3
// waitAfterFail is the amount of time to wait after a failure
waitAfterFail = 1 * time.Second
// pollNewJobsInterval is the amount of time to wait between polling the db for new scheduled jobs
pollNewJobsInterval = 5 * time.Minute
// scheduleOnceJitter is the range of jitter to add to intervals to avoid contention issues
scheduleOnceJitter = 100 * time.Millisecond
)
type JobOnceMetadata struct {
Key string
RunAt time.Time
}
type JobOnce struct {
pluginAPI JobPluginAPI
clusterMutex *Mutex
// key is the original key. It is prefixed with oncePrefix when used as a key in the KVStore
key string
runAt time.Time
numFails int
// done signals the job.run go routine to exit
done chan bool
doneOnce sync.Once
// join is a join point for the job.run() goroutine to join the calling goroutine (in this case,
// the one calling job.Cancel)
join chan bool
joinOnce sync.Once
storedCallback *syncedCallback
activeJobs *syncedJobs
}
// Cancel terminates a scheduled job, preventing it from being scheduled on this plugin instance.
// It also removes the job from the db, preventing it from being run in the future.
func (j *JobOnce) Cancel() {
j.clusterMutex.Lock()
defer j.clusterMutex.Unlock()
j.cancelWhileHoldingMutex()
// join the running goroutine
j.joinOnce.Do(func() {
<-j.join
})
}
func newJobOnce(pluginAPI JobPluginAPI, key string, runAt time.Time, callback *syncedCallback, jobs *syncedJobs) (*JobOnce, error) {
mutex, err := NewMutex(pluginAPI, key)
if err != nil {
return nil, errors.Wrap(err, "failed to create job mutex")
}
return &JobOnce{
pluginAPI: pluginAPI,
clusterMutex: mutex,
key: key,
runAt: runAt,
done: make(chan bool),
join: make(chan bool),
storedCallback: callback,
activeJobs: jobs,
}, nil
}
func (j *JobOnce) run() {
defer close(j.join)
wait := time.Until(j.runAt)
for {
select {
case <-j.done:
return
case <-time.After(wait + addJitter()):
}
func() {
// Acquire the cluster mutex while we're trying to do the job
j.clusterMutex.Lock()
defer j.clusterMutex.Unlock()
// Check that the job has not been completed
metadata, err := readMetadata(j.pluginAPI, j.key)
if err != nil {
j.numFails++
if j.numFails > maxNumFails {
j.cancelWhileHoldingMutex()
return
}
// wait a bit of time and try again
wait = waitAfterFail
return
}
// If key doesn't exist, or if the runAt has changed, the original job has been completed already
if metadata == nil || !j.runAt.Equal(metadata.RunAt) {
j.cancelWhileHoldingMutex()
return
}
j.executeJob()
j.cancelWhileHoldingMutex()
}()
}
}
func (j *JobOnce) executeJob() {
j.storedCallback.mu.Lock()
defer j.storedCallback.mu.Unlock()
j.storedCallback.callback(j.key)
}
// readMetadata reads the job's stored metadata. If the caller wishes to make an atomic
// read/write, the cluster mutex for job's key should be held.
func readMetadata(pluginAPI JobPluginAPI, key string) (*JobOnceMetadata, error) {
data, err := pluginAPI.KVGet(oncePrefix + key)
if err != nil {
return nil, errors.Wrap(err, "failed to read data")
}
if data == nil {
return nil, nil
}
var metadata JobOnceMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, errors.Wrap(err, "failed to decode data")
}
return &metadata, nil
}
// saveMetadata writes the job's metadata to the kvstore. saveMetadata acquires the job's cluster lock.
// saveMetadata will not overwrite an existing key.
func (j *JobOnce) saveMetadata() error {
j.clusterMutex.Lock()
defer j.clusterMutex.Unlock()
metadata := JobOnceMetadata{
Key: j.key,
RunAt: j.runAt,
}
data, err := json.Marshal(metadata)
if err != nil {
return errors.Wrap(err, "failed to marshal data")
}
ok, err := j.pluginAPI.KVSetWithOptions(oncePrefix+j.key, data, model.PluginKVSetOptions{
Atomic: true,
OldValue: nil,
})
if err != nil {
return err
}
if !ok {
return errors.New("failed to set data")
}
return nil
}
// cancelWhileHoldingMutex assumes the caller holds the job's mutex.
func (j *JobOnce) cancelWhileHoldingMutex() {
// remove the job from the kv store, if it exists
_ = j.pluginAPI.KVDelete(oncePrefix + j.key)
j.activeJobs.mu.Lock()
defer j.activeJobs.mu.Unlock()
delete(j.activeJobs.jobs, j.key)
j.doneOnce.Do(func() {
close(j.done)
})
}
func addJitter() time.Duration {
return time.Duration(rand.Int63n(int64(scheduleOnceJitter)))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cluster
import (
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// syncedCallback uses the mutex to make things predictable for the client: the callback will be
// called once at a time (the client does not need to worry about concurrency within the callback)
type syncedCallback struct {
mu sync.Mutex
callback func(string)
}
type syncedJobs struct {
mu sync.RWMutex
jobs map[string]*JobOnce
}
type JobOnceScheduler struct {
pluginAPI JobPluginAPI
startedMu sync.RWMutex
started bool
activeJobs *syncedJobs
storedCallback *syncedCallback
}
// GetJobOnceScheduler returns a scheduler which is ready to have its callback set. Repeated
// calls will return the same scheduler.
func GetJobOnceScheduler(pluginAPI JobPluginAPI) *JobOnceScheduler {
return &JobOnceScheduler{
pluginAPI: pluginAPI,
activeJobs: &syncedJobs{
jobs: make(map[string]*JobOnce),
},
storedCallback: &syncedCallback{},
}
}
// Start starts the Scheduler. It finds all previous ScheduleOnce jobs and starts them running, and
// fires any jobs that have reached or exceeded their runAt time. Thus, even if a cluster goes down
// and is restarted, Start will restart previously scheduled jobs.
func (s *JobOnceScheduler) Start() error {
s.startedMu.Lock()
defer s.startedMu.Unlock()
if s.started {
return errors.New("scheduler has already been started")
}
if err := s.verifyCallbackExists(); err != nil {
return errors.Wrap(err, "callback not found; cannot start scheduler")
}
if err := s.scheduleNewJobsFromDB(); err != nil {
return errors.Wrap(err, "could not start JobOnceScheduler due to error")
}
go s.pollForNewScheduledJobs()
s.started = true
return nil
}
// SetCallback sets the scheduler's callback. When a job fires, the callback will be called with
// the job's id.
func (s *JobOnceScheduler) SetCallback(callback func(string)) error {
if callback == nil {
return errors.New("callback cannot be nil")
}
s.storedCallback.mu.Lock()
defer s.storedCallback.mu.Unlock()
s.storedCallback.callback = callback
return nil
}
// ListScheduledJobs returns a list of the jobs in the db that have been scheduled. There is no
// guarantee that list is accurate by the time the caller reads the list. E.g., the jobs in the list
// may have been run, canceled, or new jobs may have scheduled.
func (s *JobOnceScheduler) ListScheduledJobs() ([]JobOnceMetadata, error) {
var ret []JobOnceMetadata
for i := 0; ; i++ {
keys, err := s.pluginAPI.KVList(i, keysPerPage)
if err != nil {
return nil, errors.Wrap(err, "error getting KVList")
}
for _, k := range keys {
if strings.HasPrefix(k, oncePrefix) {
metadata, err := readMetadata(s.pluginAPI, k[len(oncePrefix):])
if err != nil {
logrus.WithError(err).WithField("key", k).Error("could not retrieve data from plugin kvstore")
continue
}
if metadata == nil {
continue
}
ret = append(ret, *metadata)
}
}
if len(keys) < keysPerPage {
break
}
}
return ret, nil
}
// ScheduleOnce creates a scheduled job that will run once. When the clock reaches runAt, the
// callback will be called with key as the argument.
//
// If the job key already exists in the db, this will return an error. To reschedule a job, first
// cancel the original then schedule it again.
func (s *JobOnceScheduler) ScheduleOnce(key string, runAt time.Time) (*JobOnce, error) {
s.startedMu.RLock()
defer s.startedMu.RUnlock()
if !s.started {
return nil, errors.New("start the scheduler before adding jobs")
}
job, err := newJobOnce(s.pluginAPI, key, runAt, s.storedCallback, s.activeJobs)
if err != nil {
return nil, errors.Wrap(err, "could not create new job")
}
if err = job.saveMetadata(); err != nil {
return nil, errors.Wrap(err, "could not save job metadata")
}
s.runAndTrack(job)
return job, nil
}
// Cancel cancels a job by its key. This is useful if the plugin lost the original *JobOnce, or
// is stopping a job found in ListScheduledJobs().
func (s *JobOnceScheduler) Cancel(key string) {
// using an anonymous function because job.Close() below needs access to the activeJobs mutex
job := func() *JobOnce {
s.activeJobs.mu.RLock()
defer s.activeJobs.mu.RUnlock()
j, ok := s.activeJobs.jobs[key]
if ok {
return j
}
// Job wasn't active, so no need to call CancelWhileHoldingMutex (which shuts down the
// goroutine). There's a condition where another server in the cluster started the job, and
// the current server hasn't polled for it yet. To solve that case, delete it from the db.
mutex, err := NewMutex(s.pluginAPI, key)
if err != nil {
logrus.WithError(err).WithField("key", key).Error("failed to create job mutex in Cancel")
}
mutex.Lock()
defer mutex.Unlock()
_ = s.pluginAPI.KVDelete(oncePrefix + key)
return nil
}()
if job != nil {
job.Cancel()
}
}
func (s *JobOnceScheduler) scheduleNewJobsFromDB() error {
scheduled, err := s.ListScheduledJobs()
if err != nil {
return errors.Wrap(err, "could not read scheduled jobs from db")
}
for _, m := range scheduled {
job, err := newJobOnce(s.pluginAPI, m.Key, m.RunAt, s.storedCallback, s.activeJobs)
if err != nil {
logrus.WithError(err).WithField("key", m.Key).Error("could not create new job")
continue
}
s.runAndTrack(job)
}
return nil
}
func (s *JobOnceScheduler) runAndTrack(job *JobOnce) {
s.activeJobs.mu.Lock()
defer s.activeJobs.mu.Unlock()
// has this been scheduled already on this server?
if _, ok := s.activeJobs.jobs[job.key]; ok {
return
}
go job.run()
s.activeJobs.jobs[job.key] = job
}
// pollForNewScheduledJobs will only be started once per plugin. It doesn't need to be stopped.
func (s *JobOnceScheduler) pollForNewScheduledJobs() {
for {
<-time.After(pollNewJobsInterval + addJitter())
if err := s.scheduleNewJobsFromDB(); err != nil {
logrus.WithError(err).Error("scheduleOnce poller encountered an error but is still polling")
}
}
}
func (s *JobOnceScheduler) verifyCallbackExists() error {
s.storedCallback.mu.Lock()
defer s.storedCallback.mu.Unlock()
if s.storedCallback.callback == nil {
return errors.New("set callback before starting the scheduler")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cluster
import (
"context"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// mutexPrefix is used to namespace key values created for a mutex from other key values
// created by a plugin.
mutexPrefix = "mutex_"
)
const (
// ttl is the interval after which a locked mutex will expire unless refreshed
ttl = time.Second * 15
// refreshInterval is the interval on which the mutex will be refreshed when locked
refreshInterval = ttl / 2
)
// MutexPluginAPI is the plugin API interface required to manage mutexes.
type MutexPluginAPI interface {
KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, error)
}
// Mutex is similar to sync.Mutex, except usable by multiple plugin instances across a cluster.
//
// Internally, a mutex relies on an atomic key-value set operation as exposed by the Mattermost
// plugin API.
//
// Mutexes with different names are unrelated. Mutexes with the same name from different plugins
// are unrelated. Pick a unique name for each mutex your plugin requires.
//
// A Mutex must not be copied after first use.
type Mutex struct {
pluginAPI MutexPluginAPI
key string
// lock guards the variables used to manage the refresh task, and is not itself related to
// the cluster-wide lock.
lock sync.Mutex
stopRefresh chan bool
refreshDone chan bool
}
// NewMutex creates a mutex with the given key name.
//
// Panics if key is empty.
func NewMutex(pluginAPI MutexPluginAPI, key string) (*Mutex, error) {
key, err := makeLockKey(key)
if err != nil {
return nil, err
}
return &Mutex{
pluginAPI: pluginAPI,
key: key,
}, nil
}
// makeLockKey returns the prefixed key used to namespace mutex keys.
func makeLockKey(key string) (string, error) {
if key == "" {
return "", errors.New("must specify valid mutex key")
}
return mutexPrefix + key, nil
}
// lock makes a single attempt to atomically lock the mutex, returning true only if successful.
func (m *Mutex) tryLock() (bool, error) {
ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{
Atomic: true,
OldValue: nil, // No existing key value.
ExpireInSeconds: int64(ttl / time.Second),
})
if err != nil {
return false, errors.Wrap(err, "failed to set mutex kv")
}
return ok, nil
}
// refreshLock rewrites the lock key value with a new expiry, returning true only if successful.
func (m *Mutex) refreshLock() error {
ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{
Atomic: true,
OldValue: []byte{1},
ExpireInSeconds: int64(ttl / time.Second),
})
if err != nil {
return errors.Wrap(err, "failed to refresh mutex kv")
} else if !ok {
return errors.New("unexpectedly failed to refresh mutex kv")
}
return nil
}
// Lock locks m. If the mutex is already locked by any plugin instance, including the current one,
// the calling goroutine blocks until the mutex can be locked.
func (m *Mutex) Lock() {
_ = m.LockWithContext(context.Background())
}
// LockWithContext locks m unless the context is canceled. If the mutex is already locked by any plugin
// instance, including the current one, the calling goroutine blocks until the mutex can be locked,
// or the context is canceled.
//
// The mutex is locked only if a nil error is returned.
func (m *Mutex) LockWithContext(ctx context.Context) error {
var waitInterval time.Duration
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(waitInterval):
}
locked, err := m.tryLock()
if err != nil {
logrus.WithError(err).WithField("lock_key", m.key).Error("failed to lock mutex")
waitInterval = nextWaitInterval(waitInterval, err)
continue
} else if !locked {
waitInterval = nextWaitInterval(waitInterval, err)
continue
}
stop := make(chan bool)
done := make(chan bool)
go func() {
defer close(done)
t := time.NewTicker(refreshInterval)
for {
select {
case <-t.C:
err := m.refreshLock()
if err != nil {
logrus.WithError(err).WithField("lock_key", m.key).Error("failed to refresh mutex")
return
}
case <-stop:
return
}
}
}()
m.lock.Lock()
m.stopRefresh = stop
m.refreshDone = done
m.lock.Unlock()
return nil
}
}
// Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock.
//
// Just like sync.Mutex, a locked Lock is not associated with a particular goroutine or plugin
// instance. It is allowed for one goroutine or plugin instance to lock a Lock and then arrange
// for another goroutine or plugin instance to unlock it. In practice, ownership of the lock should
// remain within a single plugin instance.
func (m *Mutex) Unlock() {
m.lock.Lock()
if m.stopRefresh == nil {
m.lock.Unlock()
panic("mutex has not been acquired")
}
close(m.stopRefresh)
m.stopRefresh = nil
<-m.refreshDone
m.lock.Unlock()
// If an error occurs deleting, the mutex kv will still expire, allowing later retry.
_, _ = m.pluginAPI.KVSetWithOptions(m.key, nil, model.PluginKVSetOptions{})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cluster
import (
"math/rand"
"time"
)
const (
// minWaitInterval is the minimum amount of time to wait between locking attempts
minWaitInterval = 1 * time.Second
// maxWaitInterval is the maximum amount of time to wait between locking attempts
maxWaitInterval = 5 * time.Minute
// pollWaitInterval is the usual time to wait between unsuccessful locking attempts
pollWaitInterval = 1 * time.Second
// jitterWaitInterval is the amount of jitter to add when waiting to avoid thundering herds
jitterWaitInterval = minWaitInterval / 2
)
// nextWaitInterval determines how long to wait until the next lock retry.
func nextWaitInterval(lastWaitInterval time.Duration, err error) time.Duration {
nextWaitInterval := lastWaitInterval
if nextWaitInterval <= 0 {
nextWaitInterval = minWaitInterval
}
if err != nil {
nextWaitInterval *= 2
if nextWaitInterval > maxWaitInterval {
nextWaitInterval = maxWaitInterval
}
} else {
nextWaitInterval = pollWaitInterval
}
// Add some jitter to avoid unnecessary collision between competing plugin instances.
nextWaitInterval += time.Duration(rand.Int63n(int64(jitterWaitInterval)) - int64(jitterWaitInterval)/2)
return nextWaitInterval
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package pluginapi
import (
"github.com/mattermost/mattermost-server/v6/model"
)
const (
e10 = "E10"
e20 = "E20"
professional = "professional"
enterprise = "enterprise"
)
// IsEnterpriseLicensedOrDevelopment returns true when the server is licensed with any Mattermost
// Enterprise License, or has `EnableDeveloper` and `EnableTesting` configuration settings
// enabled signaling a non-production, developer mode.
func IsEnterpriseLicensedOrDevelopment(config *model.Config, license *model.License) bool {
if license != nil {
return true
}
return IsConfiguredForDevelopment(config)
}
// isValidSkuShortName returns whether the SKU short name is one of the known strings;
// namely: E10 or professional, or E20 or enterprise
func isValidSkuShortName(license *model.License) bool {
if license == nil {
return false
}
switch license.SkuShortName {
case e10, e20, professional, enterprise:
return true
default:
return false
}
}
// IsE10LicensedOrDevelopment returns true when the server is at least licensed with a legacy Mattermost
// Enterprise E10 License or a Mattermost Professional License, or has `EnableDeveloper` and
// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode.
func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bool {
if license != nil &&
(license.SkuShortName == e10 || license.SkuShortName == professional ||
license.SkuShortName == e20 || license.SkuShortName == enterprise) {
return true
}
if !isValidSkuShortName(license) {
// As a fallback for licenses whose SKU short name is unknown, make a best effort to try
// and use the presence of a known E10/Professional feature as a check to determine licensing.
if license != nil &&
license.Features != nil &&
license.Features.LDAP != nil &&
*license.Features.LDAP {
return true
}
}
return IsConfiguredForDevelopment(config)
}
// IsE20LicensedOrDevelopment returns true when the server is licensed with a legacy Mattermost
// Enterprise E20 License or a Mattermost Enterprise License, or has `EnableDeveloper` and
// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode.
func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bool {
if license != nil && (license.SkuShortName == e20 || license.SkuShortName == enterprise) {
return true
}
if !isValidSkuShortName(license) {
// As a fallback for licenses whose SKU short name is unknown, make a best effort to try
// and use the presence of a known E20/Enterprise feature as a check to determine licensing.
if license != nil &&
license.Features != nil &&
license.Features.FutureFeatures != nil &&
*license.Features.FutureFeatures {
return true
}
}
return IsConfiguredForDevelopment(config)
}
// IsConfiguredForDevelopment returns true when the server has `EnableDeveloper` and `EnableTesting`
// configuration settings enabled, signaling a non-production, developer mode.
func IsConfiguredForDevelopment(config *model.Config) bool {
if config != nil &&
config.ServiceSettings.EnableTesting != nil &&
*config.ServiceSettings.EnableTesting &&
config.ServiceSettings.EnableDeveloper != nil &&
*config.ServiceSettings.EnableDeveloper {
return true
}
return false
}
// IsCloud returns true when the server is on cloud, and false otherwise.
func IsCloud(license *model.License) bool {
if license == nil || license.Features == nil || license.Features.Cloud == nil {
return false
}
return *license.Features.Cloud
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
"github.com/gorilla/mux"
)
type ActionsHandler struct {
*ErrorHandler
channelActionsService app.ChannelActionService
api playbooks.ServicesAPI
permissions *app.PermissionsService
}
func NewActionsHandler(router *mux.Router, channelActionsService app.ChannelActionService, api playbooks.ServicesAPI, permissions *app.PermissionsService) *ActionsHandler {
handler := &ActionsHandler{
ErrorHandler: &ErrorHandler{},
channelActionsService: channelActionsService,
api: api,
permissions: permissions,
}
actionsRouter := router.PathPrefix("/actions").Subrouter()
channelsActionsRouter := actionsRouter.PathPrefix("/channels").Subrouter()
channelActionsRouter := channelsActionsRouter.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
channelActionsRouter.HandleFunc("", withContext(handler.createChannelAction)).Methods(http.MethodPost)
channelActionsRouter.HandleFunc("", withContext(handler.getChannelActions)).Methods(http.MethodGet)
channelActionsRouter.HandleFunc("/check-and-send-message-on-join", withContext(handler.checkAndSendMessageOnJoin)).Methods(http.MethodGet)
channelActionRouter := channelActionsRouter.PathPrefix("/{action_id:[A-Za-z0-9]+}").Subrouter()
channelActionRouter.HandleFunc("", withContext(handler.updateChannelAction)).Methods(http.MethodPut)
return handler
}
func (a *ActionsHandler) createChannelAction(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
vars := mux.Vars(r)
channelID := vars["channel_id"]
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionCreate(userID, channelID)) {
return
}
var channelAction app.GenericChannelAction
if err := json.NewDecoder(r.Body).Decode(&channelAction); err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err)
return
}
// Ensure that the channel ID in both the URL and the body of the request are the same;
// otherwise the permission check done above no longer makes sense
if channelAction.ChannelID != channelID {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil)
return
}
// Validate the action type and payload
if err := a.channelActionsService.Validate(channelAction); err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err)
return
}
id, err := a.channelActionsService.Create(channelAction)
if err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to create action", err)
return
}
result := struct {
ID string `json:"id"`
}{
ID: id,
}
w.Header().Add("Location", makeAPIURL(a.api, "actions/channel/%s/%s", channelAction.ChannelID, id))
ReturnJSON(w, &result, http.StatusCreated)
}
func isValidTrigger(trigger string) bool {
if trigger == "" {
return true
}
for _, elem := range app.ValidTriggerTypes {
if trigger == string(elem) {
return true
}
}
return false
}
func isValidAction(action string) bool {
if action == "" {
return true
}
for _, elem := range app.ValidActionTypes {
if action == string(elem) {
return true
}
}
return false
}
func parseGetChannelActionsOptions(query url.Values) (*app.GetChannelActionOptions, error) {
actionTypeStr := query.Get("action_type")
triggerTypeStr := query.Get("trigger_type")
if !isValidAction(actionTypeStr) {
return nil, fmt.Errorf("action_type %q not recognized; valid values are %v", actionTypeStr, app.ValidActionTypes)
}
if !isValidTrigger(triggerTypeStr) {
return nil, fmt.Errorf("trigger_type %q not recognized; valid values are %v", triggerTypeStr, app.ValidTriggerTypes)
}
return &app.GetChannelActionOptions{
ActionType: app.ActionType(actionTypeStr),
TriggerType: app.TriggerType(triggerTypeStr),
}, nil
}
func (a *ActionsHandler) getChannelActions(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
vars := mux.Vars(r)
channelID := vars["channel_id"]
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) {
return
}
options, err := parseGetChannelActionsOptions(r.URL.Query())
if err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, errors.Wrapf(err, "bad options").Error(), err)
return
}
actions, err := a.channelActionsService.GetChannelActions(channelID, *options)
if err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to retrieve actions for channel %s", channelID), err)
return
}
ReturnJSON(w, &actions, http.StatusOK)
}
// checkAndSendMessageOnJoin handles the GET /actions/channels/{channel_id}/check_and_send_message_on_join endpoint.
func (a *ActionsHandler) checkAndSendMessageOnJoin(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
channelID := vars["channel_id"]
userID := r.Header.Get("Mattermost-User-ID")
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) {
return
}
hasViewed := a.channelActionsService.CheckAndSendMessageOnJoin(userID, channelID)
ReturnJSON(w, map[string]interface{}{"viewed": hasViewed}, http.StatusOK)
}
func (a *ActionsHandler) updateChannelAction(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
vars := mux.Vars(r)
channelID := vars["channel_id"]
if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionUpdate(userID, channelID)) {
return
}
var newChannelAction app.GenericChannelAction
if err := json.NewDecoder(r.Body).Decode(&newChannelAction); err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err)
return
}
// Ensure that the channel ID in both the URL and the body of the request are the same;
// otherwise the permission check done above no longer makes sense
if newChannelAction.ChannelID != channelID {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil)
return
}
// Validate the new action type and payload
if err := a.channelActionsService.Validate(newChannelAction); err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err)
return
}
err := a.channelActionsService.Update(newChannelAction, userID)
if err != nil {
a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to update action with ID %q", newChannelAction.ID), err)
return
}
w.WriteHeader(http.StatusOK)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/sirupsen/logrus"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
)
// MaxRequestSize is the size limit for any incoming request
// The default limit set by mattermost-server is the configured max file size, and
// it sometimes isn't small enough to prevent some scenarios.
//
// This is important to prevent huge payloads from being sent
// that could end in a bigger problem.
//
// If an endpoint needs a smaller limit than this one, it could be solved by adding their
// own limit BEFORE reading the request body `r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize)`
const MaxRequestSize = 5 * 1024 * 1024 // 5MB
// Handler Root API handler.
type Handler struct {
*ErrorHandler
APIRouter *mux.Router
root *mux.Router
config config.Service
}
// NewHandler constructs a new handler.
func NewHandler(config config.Service) *Handler {
handler := &Handler{
ErrorHandler: &ErrorHandler{},
config: config,
}
root := mux.NewRouter()
api := root.PathPrefix("/api/v0").Subrouter()
api.Use(LogRequest)
api.Use(MattermostAuthorizationRequired)
api.Handle("{anything:.*}", http.NotFoundHandler())
api.NotFoundHandler = http.NotFoundHandler()
handler.APIRouter = api
handler.root = root
handler.config = config
return handler
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize)
h.root.ServeHTTP(w, r)
}
// handleResponseWithCode logs the internal error and sends the public facing error
// message as JSON in a response with the provided code.
func handleResponseWithCode(w http.ResponseWriter, code int, publicMsg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
responseMsg, _ := json.Marshal(struct {
Error string `json:"error"` // A public facing message providing details about the error.
}{
Error: publicMsg,
})
_, _ = w.Write(responseMsg)
}
// HandleErrorWithCode logs the internal error and sends the public facing error
// message as JSON in a response with the provided code.
func HandleErrorWithCode(logger logrus.FieldLogger, w http.ResponseWriter, code int, publicErrorMsg string, internalErr error) {
if internalErr != nil {
logger = logger.WithError(internalErr)
}
if code >= http.StatusInternalServerError {
logger.Error(publicErrorMsg)
} else {
logger.Warn(publicErrorMsg)
}
handleResponseWithCode(w, code, publicErrorMsg)
}
// ReturnJSON writes the given pointerToObject as json with the provided httpStatus
func ReturnJSON(w http.ResponseWriter, pointerToObject interface{}, httpStatus int) {
jsonBytes, err := json.Marshal(pointerToObject)
if err != nil {
logrus.WithError(err).Error("Unable to marshal JSON")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
if _, err = w.Write(jsonBytes); err != nil {
logrus.WithError(err).Warn("Unable to write to http.ResponseWriter")
return
}
}
// MattermostAuthorizationRequired checks if request is authorized.
func MattermostAuthorizationRequired(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-Id")
if userID != "" {
next.ServeHTTP(w, r)
return
}
http.Error(w, "Not authorized", http.StatusUnauthorized)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
type BotHandler struct {
*ErrorHandler
api playbooks.ServicesAPI
poster bot.Poster
config config.Service
playbookRunService app.PlaybookRunService
userInfoStore app.UserInfoStore
}
func NewBotHandler(router *mux.Router, api playbooks.ServicesAPI, poster bot.Poster, config config.Service, playbookRunService app.PlaybookRunService, userInfoStore app.UserInfoStore) *BotHandler {
handler := &BotHandler{
ErrorHandler: &ErrorHandler{},
api: api,
poster: poster,
config: config,
playbookRunService: playbookRunService,
userInfoStore: userInfoStore,
}
botRouter := router.PathPrefix("/bot").Subrouter()
notifyAdminsRouter := botRouter.PathPrefix("/notify-admins").Subrouter()
notifyAdminsRouter.HandleFunc("", withContext(handler.notifyAdmins)).Methods(http.MethodPost)
notifyAdminsRouter.HandleFunc("/button-start-trial", withContext(handler.startTrial)).Methods(http.MethodPost)
botRouter.HandleFunc("/connect", withContext(handler.connect)).Methods(http.MethodGet)
return handler
}
type messagePayload struct {
MessageType string `json:"message_type"`
}
func (h *BotHandler) notifyAdmins(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var payload messagePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode message", err)
return
}
if err := h.poster.NotifyAdmins(payload.MessageType, userID, !h.api.IsEnterpriseReady()); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func CanStartTrialLicense(userID string, api playbooks.ServicesAPI) error {
if !api.HasPermissionTo(userID, model.PermissionManageLicenseInformation) {
return errors.Wrap(app.ErrNoPermissions, "no permission to manage license information")
}
return nil
}
func (h *BotHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if err := CanStartTrialLicense(userID, h.api); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "no permission to start a trial license", err)
return
}
var requestData *model.PostActionIntegrationRequest
err := json.NewDecoder(r.Body).Decode(&requestData)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse json", err)
return
}
if requestData == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "missing request data", nil)
return
}
users, ok := requestData.Context["users"].(float64)
if !ok {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: users is not a number", nil)
return
}
termsAccepted, ok := requestData.Context["termsAccepted"].(bool)
if !ok {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: termsAccepted is not a boolean", nil)
return
}
receiveEmailsAccepted, ok := requestData.Context["receiveEmailsAccepted"].(bool)
if !ok {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: receiveEmailsAccepted is not a boolean", nil)
return
}
originalPost, err := h.api.GetPost(requestData.PostId)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
// Modify the button text while the license is downloading
originalAttachments := originalPost.Attachments()
outer:
for _, attachment := range originalAttachments {
for _, action := range attachment.Actions {
if action.Id == "message" {
action.Name = "Requesting trial..."
break outer
}
}
}
model.ParseSlackAttachment(originalPost, originalAttachments)
_, _ = h.api.UpdatePost(originalPost)
post := &model.Post{
Id: requestData.PostId,
}
if err := h.api.RequestTrialLicense(requestData.UserId, int(users), termsAccepted, receiveEmailsAccepted); err != nil {
post.Message = "Trial license could not be retrieved. Visit [https://mattermost.com/trial/](https://mattermost.com/trial/) to request a license."
if _, postErr := h.api.UpdatePost(post); postErr != nil {
logrus.WithError(postErr).WithField("post_id", post.Id).Error("unable to edit the admin notification post")
}
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to request the trial license", err)
return
}
post.Message = "Thank you!"
attachments := []*model.SlackAttachment{
{
Title: "You’re currently on a free trial of Mattermost Enterprise.",
Text: "Your free trial will expire in **30 days**. Visit our Customer Portal to purchase a license to continue using commercial edition features after your trial ends.\n[Purchase a license](https://customers.mattermost.com/signup)\n[Contact sales](https://mattermost.com/contact-us/)",
},
}
model.ParseSlackAttachment(post, attachments)
if _, err := h.api.UpdatePost(post); err != nil {
logrus.WithError(err).WithField("post_id", post.Id).Error("unable to edit the admin notification post")
}
ReturnJSON(w, post, http.StatusOK)
}
type DigestSenderParams struct {
isWeekly bool
}
// connect handles the GET /bot/connect endpoint (a notification sent when the client wakes up or reconnects)
func (h *BotHandler) connect(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
info, err := h.userInfoStore.Get(userID)
if errors.Is(err, app.ErrNotFound) {
info = app.UserInfo{
ID: userID,
}
} else if err != nil {
h.HandleError(w, c.logger, err)
return
}
var timezone *time.Location
offset, _ := strconv.Atoi(r.Header.Get("X-Timezone-Offset"))
timezone = time.FixedZone("local", offset*60*60)
sendRegularDigest := h.createDigestSender(c, w, userID, &info)
// we want to first try a weekly digest
// if we have already sent it this week, try with a daily one
currentTime := time.UnixMilli(model.GetMillis()).In(timezone)
if app.ShouldSendWeeklyDigestMessage(info, timezone, currentTime) {
sendRegularDigest(DigestSenderParams{isWeekly: true})
} else if app.ShouldSendDailyDigestMessage(info, timezone, currentTime) {
sendRegularDigest(DigestSenderParams{isWeekly: false})
}
w.WriteHeader(http.StatusOK)
}
func (h *BotHandler) createDigestSender(c *Context, w http.ResponseWriter, userID string, userInfo *app.UserInfo) func(DigestSenderParams) {
return func(params DigestSenderParams) {
now := model.GetMillis()
// record that we're sending a DM now (this will prevent us trying over and over on every
// response if there's a failure later)
userInfo.LastDailyTodoDMAt = now
if err := h.userInfoStore.Upsert(*userInfo); err != nil {
h.HandleError(w, c.logger, err)
return
}
regulartity := "daily"
if params.isWeekly {
regulartity = "weekly"
}
if err := h.playbookRunService.DMTodoDigestToUser(userID, false, params.isWeekly); err != nil {
h.HandleError(w, c.logger, errors.Wrapf(err, "failed to send '%s' DMTodoDigest to userID '%s'", regulartity, userID))
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
)
const maxItemsInRunsAndPlaybooksCategory = 1000
type CategoryHandler struct {
*ErrorHandler
api playbooks.ServicesAPI
categoryService app.CategoryService
playbookService app.PlaybookService
playbookRunService app.PlaybookRunService
}
func NewCategoryHandler(router *mux.Router, api playbooks.ServicesAPI, categoryService app.CategoryService, playbookService app.PlaybookService, playbookRunService app.PlaybookRunService) *CategoryHandler {
handler := &CategoryHandler{
ErrorHandler: &ErrorHandler{},
api: api,
categoryService: categoryService,
playbookService: playbookService,
playbookRunService: playbookRunService,
}
categoriesRouter := router.PathPrefix("/my_categories").Subrouter()
categoriesRouter.HandleFunc("", withContext(handler.getMyCategories)).Methods(http.MethodGet)
categoriesRouter.HandleFunc("", withContext(handler.createMyCategory)).Methods(http.MethodPost)
categoriesRouter.HandleFunc("/favorites", withContext(handler.isFavorite)).Methods(http.MethodGet)
categoryRouter := categoriesRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter()
categoryRouter.HandleFunc("", withContext(handler.updateMyCategory)).Methods(http.MethodPut)
categoryRouter.HandleFunc("", withContext(handler.deleteMyCategory)).Methods(http.MethodDelete)
categoryRouter.HandleFunc("/collapse", withContext(handler.collapseMyCategory)).Methods(http.MethodPut)
return handler
}
func (h *CategoryHandler) getMyCategories(c *Context, w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
teamID := params.Get("team_id")
userID := r.Header.Get("Mattermost-User-ID")
customCategories, err := h.categoryService.GetCategories(teamID, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
filteredCustomCategories := filterEmptyCategories(customCategories)
runsCategory, err := h.getRunsCategory(teamID, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
filteredRuns := filterDuplicatesFromCategory(runsCategory, filteredCustomCategories)
allCategories := append([]app.Category{}, customCategories...)
allCategories = append(allCategories, filteredRuns)
playbooksCategory, err := h.getPlaybooksCategory(teamID, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
filteredPlaybooks := filterDuplicatesFromCategory(playbooksCategory, filteredCustomCategories)
allCategories = append(allCategories, filteredPlaybooks)
ReturnJSON(w, allCategories, http.StatusOK)
}
func (h *CategoryHandler) createMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var category app.Category
if err := json.NewDecoder(r.Body).Decode(&category); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err)
return
}
if category.ID != "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Category given already has ID", nil)
return
}
// user can only create category for themselves
if category.UserID != userID {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("userID %s and category userID %s mismatch", userID, category.UserID), nil)
return
}
createdCategory, err := h.categoryService.Create(category)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, createdCategory, http.StatusOK)
}
func (h *CategoryHandler) updateMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
categoryID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var category app.Category
if err := json.NewDecoder(r.Body).Decode(&category); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err)
return
}
if categoryID != category.ID {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "categoryID mismatch in patch and body", nil)
return
}
// user can only update category for themselves
if category.UserID != userID {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "user ID mismatch in session and category", nil)
return
}
// verify if category belongs to the user
existingCategory, err := h.categoryService.Get(category.ID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
return
}
if existingCategory.DeleteAt != 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
return
}
if existingCategory.UserID != category.UserID {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil)
return
}
if err := h.categoryService.Update(category); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *CategoryHandler) collapseMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
categoryID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var collapsed bool
if err := json.NewDecoder(r.Body).Decode(&collapsed); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode collapsed", err)
return
}
existingCategory, err := h.categoryService.Get(categoryID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
return
}
if existingCategory.DeleteAt != 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
return
}
// verify if category belongs to the user
if existingCategory.UserID != userID {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "UserID mismatch", nil)
return
}
if existingCategory.Collapsed == collapsed {
w.WriteHeader(http.StatusOK)
return
}
patchedCategory := existingCategory
patchedCategory.Collapsed = collapsed
patchedCategory.UpdateAt = model.GetMillis()
if err := h.categoryService.Update(patchedCategory); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *CategoryHandler) deleteMyCategory(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
categoryID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
existingCategory, err := h.categoryService.Get(categoryID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err)
return
}
// category is already deleted. This avoids
// overriding the original deleted at timestamp
if existingCategory.DeleteAt != 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil)
return
}
// verify if category belongs to the user
if existingCategory.UserID != userID {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil)
return
}
if err := h.categoryService.Delete(categoryID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *CategoryHandler) isFavorite(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
params := r.URL.Query()
teamID := params.Get("team_id")
itemID := params.Get("item_id")
itemType := params.Get("type")
convertedItemType, err := app.StringToItemType(itemType)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
isFavorite, err := h.categoryService.IsItemFavorite(app.CategoryItem{ItemID: itemID, Type: convertedItemType}, teamID, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, isFavorite, http.StatusOK)
}
func (h *CategoryHandler) getRunsCategory(teamID, userID string) (app.Category, error) {
runs, err := h.playbookRunService.GetPlaybookRuns(
app.RequesterInfo{
UserID: userID,
TeamID: teamID,
},
app.PlaybookRunFilterOptions{
TeamID: teamID,
ParticipantOrFollowerID: userID,
Statuses: []string{app.StatusInProgress},
Types: []string{app.RunTypePlaybook}, // only playbook runs can be viewed in Playbook product
Page: 0,
PerPage: maxItemsInRunsAndPlaybooksCategory,
},
)
if err != nil {
return app.Category{}, errors.Wrapf(err, "can't get playbook runs")
}
runCategoryItems := []app.CategoryItem{}
for _, run := range runs.Items {
runCategoryItems = append(runCategoryItems, app.CategoryItem{
ItemID: run.ID,
Type: app.RunItemType,
Name: run.Name,
})
}
runCategory := app.Category{
ID: "runsCategory",
Name: "Runs",
TeamID: teamID,
UserID: userID,
Collapsed: false,
Items: runCategoryItems,
}
return runCategory, nil
}
func (h *CategoryHandler) getPlaybooksCategory(teamID, userID string) (app.Category, error) {
playbooks, err := h.playbookService.GetPlaybooksForTeam(
app.RequesterInfo{
TeamID: teamID,
UserID: userID,
},
teamID,
app.PlaybookFilterOptions{
Page: 0,
PerPage: maxItemsInRunsAndPlaybooksCategory,
WithMembershipOnly: true,
},
)
if err != nil {
return app.Category{}, errors.Wrap(err, "can't get playbooks for team")
}
playbookCategoryItems := []app.CategoryItem{}
for _, playbook := range playbooks.Items {
playbookCategoryItems = append(playbookCategoryItems, app.CategoryItem{
ItemID: playbook.ID,
Type: app.PlaybookItemType,
Name: playbook.Title,
Public: playbook.Public,
})
}
playbookCategory := app.Category{
ID: "playbooksCategory",
Name: "Playbooks",
TeamID: teamID,
UserID: userID,
Collapsed: false,
Items: playbookCategoryItems,
}
return playbookCategory, nil
}
func categoriesContainItem(categories []app.Category, item app.CategoryItem) bool {
for _, category := range categories {
if category.ContainsItem(item) {
return true
}
}
return false
}
func filterDuplicatesFromCategory(category app.Category, categories []app.Category) app.Category {
newItems := []app.CategoryItem{}
for _, item := range category.Items {
if !categoriesContainItem(categories, item) {
newItems = append(newItems, item)
}
}
category.Items = newItems
return category
}
func filterEmptyCategories(categories []app.Category) []app.Category {
newCategories := []app.Category{}
for _, category := range categories {
if len(category.Items) > 0 {
newCategories = append(newCategories, category)
}
}
return newCategories
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/sirupsen/logrus"
)
// requestIDContextKeyType ensures requestIDContextKey can never collide with another context key
// having the same value.
type requestIDContextKeyType string
// requestIDContextKey is the key for the incoming requestID.
var requestIDContextKey = requestIDContextKeyType("requestID")
// getLogger builds a logger with the requestID attached to the given request.
func getLogger(r *http.Request) logrus.FieldLogger {
var logger logrus.FieldLogger = logrus.StandardLogger()
requestID, ok := r.Context().Value(requestIDContextKey).(string)
if ok {
logger = logger.WithField("request_id", requestID)
}
return logger
}
type Context struct {
logger logrus.FieldLogger
}
// withContext passes a logger to http handler functions.
func withContext(handler func(c *Context, w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
logger := getLogger(r)
handler(&Context{logger}, w, r)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/sirupsen/logrus"
)
type ErrorHandler struct {
}
// HandleError logs the internal error and sends a generic error as JSON in a 500 response.
func (h *ErrorHandler) HandleError(w http.ResponseWriter, logger logrus.FieldLogger, internalErr error) {
h.HandleErrorWithCode(w, logger, http.StatusInternalServerError, "An internal error has occurred. Check app server logs for details.", internalErr)
}
// HandleErrorWithCode logs the internal error and sends the public facing error
// message as JSON in a response with the provided code.
func (h *ErrorHandler) HandleErrorWithCode(w http.ResponseWriter, logger logrus.FieldLogger, code int, publicErrorMsg string, internalErr error) {
HandleErrorWithCode(logger, w, code, publicErrorMsg, internalErr)
}
// PermissionsCheck handles the output of a permission check
// Automatically does the proper error handling.
// Returns true if the check passed and false on failure. Correct use is: if !h.PermissionsCheck(w, check) { return }
func (h *ErrorHandler) PermissionsCheck(w http.ResponseWriter, logger logrus.FieldLogger, checkOutput error) bool {
if checkOutput != nil {
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", checkOutput)
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"github.com/graph-gophers/dataloader/v7"
)
const loaderBatchCapacity = 200
func populateResultWithError[K any](err error, result []*dataloader.Result[K]) []*dataloader.Result[K] {
for i := range result {
result[i] = &dataloader.Result[K]{Error: err}
}
return result
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/graph-gophers/dataloader/v7"
graphql "github.com/graph-gophers/graphql-go"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type GraphQLHandler struct {
*ErrorHandler
playbookService app.PlaybookService
playbookRunService app.PlaybookRunService
categoryService app.CategoryService
api playbooks.ServicesAPI
config config.Service
permissions *app.PermissionsService
playbookStore app.PlaybookStore
licenceChecker app.LicenseChecker
schema *graphql.Schema
}
//go:embed schema.graphqls
var SchemaFile string
func NewGraphQLHandler(
router *mux.Router,
playbookService app.PlaybookService,
playbookRunService app.PlaybookRunService,
categoryService app.CategoryService,
api playbooks.ServicesAPI,
configService config.Service,
permissions *app.PermissionsService,
playbookStore app.PlaybookStore,
licenceChecker app.LicenseChecker,
) *GraphQLHandler {
handler := &GraphQLHandler{
ErrorHandler: &ErrorHandler{},
playbookService: playbookService,
playbookRunService: playbookRunService,
categoryService: categoryService,
api: api,
config: configService,
permissions: permissions,
playbookStore: playbookStore,
licenceChecker: licenceChecker,
}
opts := []graphql.SchemaOpt{
graphql.UseFieldResolvers(),
graphql.MaxParallelism(5),
}
if !configService.IsConfiguredForDevelopmentAndTesting() {
opts = append(opts,
graphql.MaxDepth(8),
graphql.DisableIntrospection(),
)
}
root := &RootResolver{}
var err error
handler.schema, err = graphql.ParseSchema(SchemaFile, root, opts...)
if err != nil {
logrus.WithError(err).Error("unable to parse graphql schema")
return nil
}
router.HandleFunc("/query", withContext(graphiQL)).Methods("GET")
router.HandleFunc("/query", withContext(handler.graphQL)).Methods("POST")
return handler
}
type ctxKey struct{}
type GraphQLContext struct {
r *http.Request
playbookService app.PlaybookService
playbookRunService app.PlaybookRunService
playbookStore app.PlaybookStore
categoryService app.CategoryService
api playbooks.ServicesAPI
logger logrus.FieldLogger
config config.Service
permissions *app.PermissionsService
licenceChecker app.LicenseChecker
favoritesLoader *dataloader.Loader[favoriteInfo, bool]
playbooksLoader *dataloader.Loader[playbookInfo, *app.Playbook]
}
// When moving over to the multi-product architecture this should be handled by the server.
func (h *GraphQLHandler) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
// Limit bodies to 300KiB.
r.Body = http.MaxBytesReader(w, r.Body, 300*1024)
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
c.logger.WithError(err).Error("Unable to decode graphql query")
return
}
if !h.config.IsConfiguredForDevelopmentAndTesting() {
if params.OperationName == "" {
c.logger.Warn("Invalid blank operation name")
return
}
}
// dataloaders
favoritesLoader := dataloader.NewBatchedLoader(graphQLFavoritesLoader[bool], dataloader.WithBatchCapacity[favoriteInfo, bool](loaderBatchCapacity))
playbooksLoader := dataloader.NewBatchedLoader(graphQLPlaybooksLoader[*app.Playbook], dataloader.WithBatchCapacity[playbookInfo, *app.Playbook](loaderBatchCapacity))
graphQLContext := &GraphQLContext{
r: r,
playbookService: h.playbookService,
playbookRunService: h.playbookRunService,
categoryService: h.categoryService,
api: h.api,
logger: c.logger,
config: h.config,
permissions: h.permissions,
playbookStore: h.playbookStore,
licenceChecker: h.licenceChecker,
favoritesLoader: favoritesLoader,
playbooksLoader: playbooksLoader,
}
// Populate the context with required info.
reqCtx := r.Context()
reqCtx = context.WithValue(reqCtx, ctxKey{}, graphQLContext)
response := h.schema.Exec(reqCtx,
params.Query,
params.OperationName,
params.Variables,
)
r.Header.Set("X-GQL-Operation", params.OperationName)
for _, err := range response.Errors {
errLogger := c.logger.WithError(err).WithField("operation", params.OperationName)
if errors.Is(err, app.ErrNoPermissions) {
errLogger.Warn("Warning executing request")
} else if err.Rule == "FieldsOnCorrectType" {
errLogger.Warn("Query for non existent field")
} else {
errLogger.Error("Error executing request")
}
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.logger.WithError(err).Warn("Error while writing response")
}
}
func getContext(ctx context.Context) (*GraphQLContext, error) {
c, ok := ctx.Value(ctxKey{}).(*GraphQLContext)
if !ok {
return nil, errors.New("custom context not found in context")
}
return c, nil
}
// GraphiqlPage is the html base code for the graphiQL query runner
//
//go:embed graphqli.html
var GraphiqlPage []byte
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write(GraphiqlPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"github.com/graph-gophers/dataloader/v7"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
)
type favoriteInfo struct {
TeamID string
UserID string
ID string
Type app.CategoryItemType
}
func graphQLFavoritesLoader[V bool](ctx context.Context, keys []favoriteInfo) []*dataloader.Result[V] {
result := make([]*dataloader.Result[V], len(keys))
if len(keys) == 0 {
return result
}
c, err := getContext(ctx)
if err != nil {
for i := range keys {
result[i] = &dataloader.Result[V]{Error: err}
}
return result
}
// assume all keys are for the same team and user
teamID := keys[0].TeamID
userID := keys[0].UserID
categoryItems := make([]app.CategoryItem, len(keys))
for i, favorite := range keys {
categoryItems[i] = app.CategoryItem{
ItemID: favorite.ID,
Type: favorite.Type,
}
}
favorites, err := c.categoryService.AreItemsFavorites(categoryItems, teamID, userID)
if err != nil {
populateResultWithError(err, result)
}
for i, fav := range favorites {
result[i] = &dataloader.Result[V]{Data: V(fav)}
}
return result
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"github.com/graph-gophers/dataloader/v7"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
)
type playbookInfo struct {
UserID string
TeamID string
ID string
}
func graphQLPlaybooksLoader[V *app.Playbook](ctx context.Context, keys []playbookInfo) []*dataloader.Result[V] {
result := make([]*dataloader.Result[V], len(keys))
if len(keys) == 0 {
return result
}
uniquePlaybookIDs := getUniquePlaybookIDs(keys)
var teamID, userID string = keys[0].TeamID, keys[0].UserID
c, err := getContext(ctx)
if err != nil {
return populateResultWithError(err, result)
}
playbookResult, err := c.playbookService.GetPlaybooksForTeam(
app.RequesterInfo{
UserID: userID,
TeamID: teamID,
},
teamID,
app.PlaybookFilterOptions{
PlaybookIDs: uniquePlaybookIDs,
PerPage: loaderBatchCapacity,
},
)
if err != nil {
return populateResultWithError(err, result)
}
playbooksByID := make(map[string]*app.Playbook)
for i := range playbookResult.Items {
playbooksByID[playbookResult.Items[i].ID] = &playbookResult.Items[i]
}
for i, playbookInfo := range keys {
playbook, ok := playbooksByID[playbookInfo.ID]
if !ok {
result[i] = &dataloader.Result[V]{Data: nil}
continue
}
result[i] = &dataloader.Result[V]{
Data: V(playbook),
}
}
return result
}
func getUniquePlaybookIDs(playbooks []playbookInfo) []string {
playbookByID := make(map[string]bool)
for _, playbook := range playbooks {
playbookByID[playbook.ID] = true
}
result := make([]string, 0, len(playbookByID))
for playbookID := range playbookByID {
result = append(result, playbookID)
}
return result
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"fmt"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/sirupsen/logrus"
)
type PlaybookResolver struct {
app.Playbook
}
func (r *PlaybookResolver) ChannelMode(ctx context.Context) string {
return fmt.Sprint(r.Playbook.ChannelMode)
}
func (r *PlaybookResolver) IsFavorite(ctx context.Context) (bool, error) {
c, err := getContext(ctx)
if err != nil {
return false, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
thunk := c.favoritesLoader.Load(ctx, favoriteInfo{
TeamID: r.TeamID,
UserID: userID,
Type: app.PlaybookItemType,
ID: r.ID,
})
result, err := thunk()
if err != nil {
return false, err
}
return result, nil
}
func (r *PlaybookResolver) DeleteAt() float64 {
return float64(r.Playbook.DeleteAt)
}
func (r *PlaybookResolver) LastRunAt() float64 {
return float64(r.Playbook.LastRunAt)
}
func (r *PlaybookResolver) NumRuns() int32 {
return int32(r.Playbook.NumRuns)
}
func (r *PlaybookResolver) ActiveRuns() int32 {
return int32(r.Playbook.ActiveRuns)
}
func (r *PlaybookResolver) RetrospectiveReminderIntervalSeconds() float64 {
return float64(r.Playbook.RetrospectiveReminderIntervalSeconds)
}
func (r *PlaybookResolver) ReminderTimerDefaultSeconds() float64 {
return float64(r.Playbook.ReminderTimerDefaultSeconds)
}
func (r *PlaybookResolver) Metrics() []*MetricConfigResolver {
metricConfigResolvers := make([]*MetricConfigResolver, 0, len(r.Playbook.Metrics))
for _, metricConfig := range r.Playbook.Metrics {
metricConfigResolvers = append(metricConfigResolvers, &MetricConfigResolver{metricConfig})
}
return metricConfigResolvers
}
type MetricConfigResolver struct {
app.PlaybookMetricConfig
}
func (r *MetricConfigResolver) Target() *int32 {
if r.PlaybookMetricConfig.Target.Valid {
intvalue := int32(r.PlaybookMetricConfig.Target.ValueOrZero())
return &intvalue
}
return nil
}
func (r *PlaybookResolver) Checklists() []*ChecklistResolver {
checklistResolvers := make([]*ChecklistResolver, 0, len(r.Playbook.Checklists))
for _, checklist := range r.Playbook.Checklists {
checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist})
}
return checklistResolvers
}
type ChecklistResolver struct {
app.Checklist
}
func (r *ChecklistResolver) Items() []*ChecklistItemResolver {
checklistItemResolvers := make([]*ChecklistItemResolver, 0, len(r.Checklist.Items))
for _, items := range r.Checklist.Items {
checklistItemResolvers = append(checklistItemResolvers, &ChecklistItemResolver{items})
}
return checklistItemResolvers
}
type ChecklistItemResolver struct {
app.ChecklistItem
}
func (r *ChecklistItemResolver) StateModified() float64 {
return float64(r.ChecklistItem.StateModified)
}
func (r *ChecklistItemResolver) AssigneeModified() float64 {
return float64(r.ChecklistItem.AssigneeModified)
}
func (r *ChecklistItemResolver) CommandLastRun() float64 {
return float64(r.ChecklistItem.CommandLastRun)
}
func (r *ChecklistItemResolver) DueDate() float64 {
return float64(r.ChecklistItem.DueDate)
}
func (r *ChecklistItemResolver) TaskActions() []*TaskActionResolver {
taskActionsResolvers := make([]*TaskActionResolver, 0, len(r.ChecklistItem.TaskActions))
for _, taskAction := range r.ChecklistItem.TaskActions {
taskActionsResolvers = append(taskActionsResolvers, &TaskActionResolver{taskAction})
}
return taskActionsResolvers
}
type TaskActionResolver struct {
app.TaskAction
}
func (r *TaskActionResolver) Trigger() *TriggerResolver {
return &TriggerResolver{r.TaskAction.Trigger}
}
func (r *TaskActionResolver) Actions() []*ActionResolver {
actionsResolvers := make([]*ActionResolver, 0, len(r.TaskAction.Actions))
for _, action := range r.TaskAction.Actions {
actionsResolvers = append(actionsResolvers, &ActionResolver{action})
}
return actionsResolvers
}
type ActionResolver struct {
app.Action
}
func (r *ActionResolver) Type() string {
return string(r.Action.Type)
}
func (r *ActionResolver) Payload() string {
var payload string
switch r.Action.Type {
case app.MarkItemAsDoneActionType:
payload = r.Action.Payload
default:
logrus.WithField("task_action_type", r.Action.Type).Error("Unknown trigger type")
payload = ""
}
return payload
}
type TriggerResolver struct {
app.Trigger
}
func (r *TriggerResolver) Type() string {
return string(r.Trigger.Type)
}
func (r *TriggerResolver) Payload() string {
var payload string
switch r.Trigger.Type {
case app.KeywordsByUsersTriggerType:
payload = r.Trigger.Payload
default:
logrus.WithField("task_trigger_type", r.Trigger.Type).Error("Unknown trigger type")
payload = ""
}
return payload
}
type UpdateChecklist struct {
Title string `json:"title"`
Items []UpdateChecklistItem `json:"items"`
}
func (c UpdateChecklist) GetItems() []app.ChecklistItemCommon {
items := make([]app.ChecklistItemCommon, len(c.Items))
for i := range c.Items {
items[i] = &c.Items[i]
}
return items
}
type UpdateChecklistItem struct {
Title string `json:"title"`
State string `json:"state"`
StateModified float64 `json:"state_modified"`
AssigneeID string `json:"assignee_id"`
AssigneeModified float64 `json:"assignee_modified"`
Command string `json:"command"`
CommandLastRun float64 `json:"command_last_run"`
Description string `json:"description"`
LastSkipped float64 `json:"delete_at"`
DueDate float64 `json:"due_date"`
TaskActions *[]app.TaskAction `json:"task_actions"`
}
func (ci *UpdateChecklistItem) GetAssigneeID() string {
return ci.AssigneeID
}
func (ci *UpdateChecklistItem) SetAssigneeModified(modified int64) {
ci.AssigneeModified = float64(modified)
}
func (ci *UpdateChecklistItem) SetState(state string) {
ci.State = state
}
func (ci *UpdateChecklistItem) SetStateModified(modified int64) {
ci.StateModified = float64(modified)
}
func (ci *UpdateChecklistItem) SetCommandLastRun(lastRun int64) {
ci.CommandLastRun = float64(lastRun)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"strings"
)
type RootResolver struct {
RunRootResolver
PlaybookRootResolver
}
func addToSetmap[T any](setmap map[string]interface{}, name string, value *T) {
if value != nil {
setmap[name] = *value
}
}
func addConcatToSetmap(setmap map[string]interface{}, name string, value *[]string) {
if value != nil {
setmap[name] = strings.Join(*value, ",")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
"gopkg.in/guregu/null.v4"
)
// RunMutationCollection hold all mutation functions for a playbookRun
type PlaybookRootResolver struct {
}
func getGraphqlPlaybook(ctx context.Context, playbookID string) (*PlaybookResolver, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err = c.permissions.PlaybookView(userID, playbookID); err != nil {
return nil, err
}
playbook, err := c.playbookService.Get(playbookID)
if err != nil {
return nil, err
}
return &PlaybookResolver{playbook}, nil
}
func (r *PlaybookRootResolver) Playbook(ctx context.Context, args struct {
ID string
}) (*PlaybookResolver, error) {
playbookID := args.ID
return getGraphqlPlaybook(ctx, playbookID)
}
func (r *PlaybookRootResolver) Playbooks(ctx context.Context, args struct {
TeamID string
Sort string
Direction string
SearchTerm string
WithMembershipOnly bool
WithArchived bool
}) ([]*PlaybookResolver, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if args.TeamID != "" {
if err = c.permissions.PlaybookList(userID, args.TeamID); err != nil {
return nil, err
}
}
requesterInfo := app.RequesterInfo{
UserID: userID,
TeamID: args.TeamID,
IsAdmin: app.IsSystemAdmin(userID, c.api),
}
opts := app.PlaybookFilterOptions{
Sort: app.SortField(args.Sort),
Direction: app.SortDirection(args.Direction),
SearchTerm: args.SearchTerm,
WithArchived: args.WithArchived,
WithMembershipOnly: args.WithMembershipOnly,
Page: 0,
PerPage: 10000,
}
playbookResults, err := c.playbookService.GetPlaybooksForTeam(requesterInfo, args.TeamID, opts)
if err != nil {
return nil, err
}
ret := make([]*PlaybookResolver, 0, len(playbookResults.Items))
for _, pb := range playbookResults.Items {
ret = append(ret, &PlaybookResolver{pb})
}
return ret, nil
}
func (r *RunRootResolver) UpdatePlaybookFavorite(ctx context.Context, args struct {
ID string
Favorite bool
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err = c.permissions.PlaybookView(userID, args.ID); err != nil {
return "", err
}
currentPlaybook, err := c.playbookService.Get(args.ID)
if err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
if args.Favorite {
if err := c.categoryService.AddFavorite(
app.CategoryItem{
ItemID: currentPlaybook.ID,
Type: app.PlaybookItemType,
},
currentPlaybook.TeamID,
userID,
); err != nil {
return "", err
}
} else {
if err := c.categoryService.DeleteFavorite(
app.CategoryItem{
ItemID: currentPlaybook.ID,
Type: app.PlaybookItemType,
},
currentPlaybook.TeamID,
userID,
); err != nil {
return "", err
}
}
return currentPlaybook.ID, nil
}
func (r *PlaybookRootResolver) UpdatePlaybook(ctx context.Context, args struct {
ID string
Updates struct {
Title *string
Description *string
Public *bool
CreatePublicPlaybookRun *bool
ReminderMessageTemplate *string
ReminderTimerDefaultSeconds *float64
StatusUpdateEnabled *bool
InvitedUserIDs *[]string
InvitedGroupIDs *[]string
InviteUsersEnabled *bool
DefaultOwnerID *string
DefaultOwnerEnabled *bool
BroadcastChannelIDs *[]string
BroadcastEnabled *bool
WebhookOnCreationURLs *[]string
WebhookOnCreationEnabled *bool
MessageOnJoin *string
MessageOnJoinEnabled *bool
RetrospectiveReminderIntervalSeconds *float64
RetrospectiveTemplate *string
RetrospectiveEnabled *bool
WebhookOnStatusUpdateURLs *[]string
WebhookOnStatusUpdateEnabled *bool
SignalAnyKeywords *[]string
SignalAnyKeywordsEnabled *bool
CategorizeChannelEnabled *bool
CategoryName *string
RunSummaryTemplateEnabled *bool
RunSummaryTemplate *string
ChannelNameTemplate *string
Checklists *[]UpdateChecklist
CreateChannelMemberOnNewParticipant *bool
RemoveChannelMemberOnRemovedParticipant *bool
ChannelID *string
ChannelMode *string
}
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentPlaybook, err := c.playbookService.Get(args.ID)
if err != nil {
return "", err
}
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
setmap := map[string]interface{}{}
addToSetmap(setmap, "Title", args.Updates.Title)
addToSetmap(setmap, "Description", args.Updates.Description)
if args.Updates.Public != nil {
if *args.Updates.Public {
if err := c.permissions.PlaybookMakePublic(userID, currentPlaybook); err != nil {
return "", err
}
} else {
if err := c.permissions.PlaybookMakePrivate(userID, currentPlaybook); err != nil {
return "", err
}
}
if !c.licenceChecker.PlaybookAllowed(*args.Updates.Public) {
return "", errors.Wrapf(app.ErrLicensedFeature, "the playbook is not valid with the current license")
}
addToSetmap(setmap, "Public", args.Updates.Public)
}
addToSetmap(setmap, "CreatePublicIncident", args.Updates.CreatePublicPlaybookRun)
addToSetmap(setmap, "ReminderMessageTemplate", args.Updates.ReminderMessageTemplate)
addToSetmap(setmap, "ReminderTimerDefaultSeconds", args.Updates.ReminderTimerDefaultSeconds)
addToSetmap(setmap, "StatusUpdateEnabled", args.Updates.StatusUpdateEnabled)
addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant)
addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant)
if args.Updates.InvitedUserIDs != nil {
filteredInvitedUserIDs := c.permissions.FilterInvitedUserIDs(*args.Updates.InvitedUserIDs, currentPlaybook.TeamID)
addConcatToSetmap(setmap, "ConcatenatedInvitedUserIDs", &filteredInvitedUserIDs)
}
if args.Updates.InvitedGroupIDs != nil {
filteredInvitedGroupIDs := c.permissions.FilterInvitedGroupIDs(*args.Updates.InvitedGroupIDs)
addConcatToSetmap(setmap, "ConcatenatedInvitedGroupIDs", &filteredInvitedGroupIDs)
}
addToSetmap(setmap, "InviteUsersEnabled", args.Updates.InviteUsersEnabled)
if args.Updates.DefaultOwnerID != nil {
if !c.api.HasPermissionToTeam(*args.Updates.DefaultOwnerID, currentPlaybook.TeamID, model.PermissionViewTeam) {
return "", errors.Wrap(app.ErrNoPermissions, "default owner can't view team")
}
addToSetmap(setmap, "DefaultCommanderID", args.Updates.DefaultOwnerID)
}
addToSetmap(setmap, "DefaultCommanderEnabled", args.Updates.DefaultOwnerEnabled)
if args.Updates.BroadcastChannelIDs != nil {
if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, currentPlaybook.BroadcastChannelIDs); err != nil {
return "", err
}
addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs)
}
addToSetmap(setmap, "BroadcastEnabled", args.Updates.BroadcastEnabled)
if args.Updates.WebhookOnCreationURLs != nil {
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnCreationURLs); err != nil {
return "", err
}
addConcatToSetmap(setmap, "ConcatenatedWebhookOnCreationURLs", args.Updates.WebhookOnCreationURLs)
}
addToSetmap(setmap, "WebhookOnCreationEnabled", args.Updates.WebhookOnCreationEnabled)
addToSetmap(setmap, "MessageOnJoin", args.Updates.MessageOnJoin)
addToSetmap(setmap, "MessageOnJoinEnabled", args.Updates.MessageOnJoinEnabled)
addToSetmap(setmap, "RetrospectiveReminderIntervalSeconds", args.Updates.RetrospectiveReminderIntervalSeconds)
addToSetmap(setmap, "RetrospectiveTemplate", args.Updates.RetrospectiveTemplate)
addToSetmap(setmap, "RetrospectiveEnabled", args.Updates.RetrospectiveEnabled)
if args.Updates.WebhookOnStatusUpdateURLs != nil {
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil {
return "", err
}
addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs)
}
addToSetmap(setmap, "WebhookOnStatusUpdateEnabled", args.Updates.WebhookOnStatusUpdateEnabled)
if args.Updates.SignalAnyKeywords != nil {
validSignalAnyKeywords := app.ProcessSignalAnyKeywords(*args.Updates.SignalAnyKeywords)
addConcatToSetmap(setmap, "ConcatenatedSignalAnyKeywords", &validSignalAnyKeywords)
}
addToSetmap(setmap, "SignalAnyKeywordsEnabled", args.Updates.SignalAnyKeywordsEnabled)
addToSetmap(setmap, "CategorizeChannelEnabled", args.Updates.CategorizeChannelEnabled)
if args.Updates.CategoryName != nil {
if err := app.ValidateCategoryName(*args.Updates.CategoryName); err != nil {
return "", err
}
addToSetmap(setmap, "CategoryName", args.Updates.CategoryName)
}
addToSetmap(setmap, "RunSummaryTemplateEnabled", args.Updates.RunSummaryTemplateEnabled)
addToSetmap(setmap, "RunSummaryTemplate", args.Updates.RunSummaryTemplate)
addToSetmap(setmap, "ChannelNameTemplate", args.Updates.ChannelNameTemplate)
addToSetmap(setmap, "ChannelID", args.Updates.ChannelID)
addToSetmap(setmap, "ChannelMode", args.Updates.ChannelMode)
// Not optimal graphql. Stopgap measure. Should be updated separately.
if args.Updates.Checklists != nil {
app.CleanUpChecklists(*args.Updates.Checklists)
if err := validateUpdateTaskActions(*args.Updates.Checklists); err != nil {
return "", errors.Wrapf(err, "failed to validate task actions in graphql json for playbook id: '%s'", args.ID)
}
checklistsJSON, err := json.Marshal(args.Updates.Checklists)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal checklist in graphql json for playbook id: '%s'", args.ID)
}
setmap["ChecklistsJSON"] = checklistsJSON
}
if args.Updates.Checklists != nil || args.Updates.InvitedUserIDs != nil || args.Updates.InviteUsersEnabled != nil {
if err := validatePreAssignmentUpdate(currentPlaybook, args.Updates.Checklists, args.Updates.InvitedUserIDs, args.Updates.InviteUsersEnabled); err != nil {
return "", errors.Wrapf(err, "invalid user pre-assignment for playbook id: '%s'", args.ID)
}
}
if len(setmap) > 0 {
if err := c.playbookStore.GraphqlUpdate(args.ID, setmap); err != nil {
return "", err
}
}
return args.ID, nil
}
func (r *PlaybookRootResolver) AddPlaybookMember(ctx context.Context, args struct {
PlaybookID string
UserID string
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
if err != nil {
return "", err
}
if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
if err := c.playbookStore.AddPlaybookMember(args.PlaybookID, args.UserID); err != nil {
return "", errors.Wrap(err, "unable to add playbook member")
}
return "", nil
}
func (r *PlaybookRootResolver) RemovePlaybookMember(ctx context.Context, args struct {
PlaybookID string
UserID string
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
if err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
// do not require manageMembers permission if the user want to leave playbook
if userID != args.UserID {
if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil {
return "", err
}
}
if err := c.playbookStore.RemovePlaybookMember(args.PlaybookID, args.UserID); err != nil {
return "", errors.Wrap(err, "unable to remove playbook member")
}
return "", nil
}
func (r *PlaybookRootResolver) AddMetric(ctx context.Context, args struct {
PlaybookID string
Title string
Description string
Type string
Target *float64
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentPlaybook, err := c.playbookService.Get(args.PlaybookID)
if err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
return "", err
}
var target null.Int
if args.Target == nil {
target = null.NewInt(0, false)
} else {
target = null.IntFrom(int64(*args.Target))
}
if err := c.playbookStore.AddMetric(args.PlaybookID, app.PlaybookMetricConfig{
Title: args.Title,
Description: args.Description,
Type: args.Type,
Target: target,
}); err != nil {
return "", err
}
return args.PlaybookID, nil
}
func (r *PlaybookRootResolver) UpdateMetric(ctx context.Context, args struct {
ID string
Title *string
Description *string
Target *float64
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentMetric, err := c.playbookStore.GetMetric(args.ID)
if err != nil {
return "", err
}
currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID)
if err != nil {
return "", err
}
if currentPlaybook.DeleteAt != 0 {
return "", errors.New("archived playbooks can not be modified")
}
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
return "", err
}
setmap := map[string]interface{}{}
addToSetmap(setmap, "Title", args.Title)
addToSetmap(setmap, "Description", args.Description)
if args.Target != nil {
setmap["Target"] = null.IntFrom(int64(*args.Target))
}
if len(setmap) > 0 {
if err := c.playbookStore.UpdateMetric(args.ID, setmap); err != nil {
return "", err
}
}
return args.ID, nil
}
func (r *PlaybookRootResolver) DeleteMetric(ctx context.Context, args struct {
ID string
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
currentMetric, err := c.playbookStore.GetMetric(args.ID)
if err != nil {
return "", err
}
currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID)
if err != nil {
return "", err
}
if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil {
return "", err
}
if err := c.playbookStore.DeleteMetric(args.ID); err != nil {
return "", err
}
return args.ID, nil
}
func validatePreAssignmentUpdate[T app.ChecklistCommon](pb app.Playbook, newChecklists *[]T, newInvitedUsers *[]string, newInviteUsersEnabled *bool) error {
assignees := app.GetDistinctAssignees(pb.Checklists)
if newChecklists != nil {
assignees = app.GetDistinctAssignees(*newChecklists)
}
invitedUsers := pb.InvitedUserIDs
if newInvitedUsers != nil {
invitedUsers = *newInvitedUsers
}
inviteUsersEnabled := pb.InviteUsersEnabled
if newInviteUsersEnabled != nil {
inviteUsersEnabled = *newInviteUsersEnabled
}
return app.ValidatePreAssignment(assignees, invitedUsers, inviteUsersEnabled)
}
// validateUpdateTaskActions validates the taskactions in the given checklist
// NOTE: Any changes to this function must be made to function 'validateTaskActions' for the REST endpoint.
func validateUpdateTaskActions(checklists []UpdateChecklist) error {
for _, checklist := range checklists {
for _, item := range checklist.Items {
if taskActions := item.TaskActions; taskActions != nil {
for _, ta := range *taskActions {
if err := app.ValidateTrigger(ta.Trigger); err != nil {
return err
}
for _, a := range ta.Actions {
if err := app.ValidateAction(a); err != nil {
return err
}
}
}
}
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/client"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
// RunRootResolver hold all queries and mutations for a playbookRun
type RunRootResolver struct {
}
func (r *RunRootResolver) Run(ctx context.Context, args struct {
ID string `url:"id,omitempty"`
}) (*RunResolver, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err = c.permissions.RunView(userID, args.ID); err != nil {
return nil, err
}
run, err := c.playbookRunService.GetPlaybookRun(args.ID)
if err != nil {
return nil, err
}
return &RunResolver{*run}, nil
}
func (r *RunRootResolver) Runs(ctx context.Context, args struct {
TeamID string
Sort string
Direction string
Statuses []string
ParticipantOrFollowerID string
ChannelID string
First *int32
After *string
Types []string
}) (*RunConnectionResolver, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
requesterInfo := app.RequesterInfo{
UserID: userID,
TeamID: args.TeamID,
IsAdmin: app.IsSystemAdmin(userID, c.api),
}
if args.ParticipantOrFollowerID == client.Me {
args.ParticipantOrFollowerID = userID
}
perPage := 10000 // If paging not specified, get "everything"
if args.First != nil {
perPage = int(*args.First)
}
page := 0
if args.After != nil {
page, err = decodeRunConnectionCursor(*args.After)
if err != nil {
return nil, err
}
}
filterOptions := app.PlaybookRunFilterOptions{
Sort: app.SortField(args.Sort),
Direction: app.SortDirection(args.Direction),
TeamID: args.TeamID,
Statuses: args.Statuses,
ParticipantOrFollowerID: args.ParticipantOrFollowerID,
ChannelID: args.ChannelID,
IncludeFavorites: true,
Types: args.Types,
Page: page,
PerPage: perPage,
}
runResults, err := c.playbookRunService.GetPlaybookRuns(requesterInfo, filterOptions)
if err != nil {
return nil, err
}
return &RunConnectionResolver{results: *runResults, page: page}, nil
}
func (r *RunRootResolver) SetRunFavorite(ctx context.Context, args struct {
ID string
Fav bool
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err = c.permissions.RunView(userID, args.ID); err != nil {
return "", err
}
playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID)
if err != nil {
return "", err
}
if args.Fav {
if err := c.categoryService.AddFavorite(
app.CategoryItem{
ItemID: playbookRun.ID,
Type: app.RunItemType,
},
playbookRun.TeamID,
userID,
); err != nil {
return "", err
}
} else {
if err := c.categoryService.DeleteFavorite(
app.CategoryItem{
ItemID: playbookRun.ID,
Type: app.RunItemType,
},
playbookRun.TeamID,
userID,
); err != nil {
return "", err
}
}
return playbookRun.ID, nil
}
type RunUpdates struct {
Name *string
Summary *string
ChannelID *string
CreateChannelMemberOnNewParticipant *bool
RemoveChannelMemberOnRemovedParticipant *bool
StatusUpdateBroadcastChannelsEnabled *bool
StatusUpdateBroadcastWebhooksEnabled *bool
BroadcastChannelIDs *[]string
WebhookOnStatusUpdateURLs *[]string
}
func (r *RunRootResolver) UpdateRun(ctx context.Context, args struct {
ID string
Updates RunUpdates
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err = c.permissions.RunManageProperties(userID, args.ID); err != nil {
return "", err
}
playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID)
if err != nil {
return "", err
}
now := model.GetMillis()
// scalar updates
setmap := map[string]interface{}{}
addToSetmap(setmap, "Name", args.Updates.Name)
addToSetmap(setmap, "Description", args.Updates.Summary)
addToSetmap(setmap, "ChannelID", args.Updates.ChannelID)
addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant)
addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant)
addToSetmap(setmap, "StatusUpdateBroadcastChannelsEnabled", args.Updates.StatusUpdateBroadcastChannelsEnabled)
addToSetmap(setmap, "StatusUpdateBroadcastWebhooksEnabled", args.Updates.StatusUpdateBroadcastWebhooksEnabled)
if args.Updates.Summary != nil {
addToSetmap(setmap, "SummaryModifiedAt", &now)
}
if args.Updates.BroadcastChannelIDs != nil {
if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, playbookRun.BroadcastChannelIDs); err != nil {
return "", err
}
addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs)
}
if args.Updates.WebhookOnStatusUpdateURLs != nil {
if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil {
return "", err
}
addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs)
}
if err := c.playbookRunService.GraphqlUpdate(args.ID, setmap); err != nil {
return "", err
}
return playbookRun.ID, nil
}
func (r *RunRootResolver) AddRunParticipants(ctx context.Context, args struct {
RunID string
UserIDs []string
ForceAddToChannel bool
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
// When user is joining run RunView permission is enough, otherwise user need manage permissions
if updatesOnlyRequesterMembership(userID, args.UserIDs) {
if err := c.permissions.RunView(userID, args.RunID); err != nil {
return "", errors.Wrap(err, "attempted to join run without permissions")
}
} else {
if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil {
return "", errors.Wrap(err, "attempted to modify participants without permissions")
}
}
if err := c.playbookRunService.AddParticipants(args.RunID, args.UserIDs, userID, args.ForceAddToChannel); err != nil {
return "", errors.Wrap(err, "failed to add participant from run")
}
return "", nil
}
func (r *RunRootResolver) RemoveRunParticipants(ctx context.Context, args struct {
RunID string
UserIDs []string
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
// When user is leaving run RunView permission is enough, otherwise user need manage permissions
if updatesOnlyRequesterMembership(userID, args.UserIDs) {
if err := c.permissions.RunView(userID, args.RunID); err != nil {
return "", errors.Wrap(err, "attempted to modify participants without permissions")
}
} else {
if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil {
return "", errors.Wrap(err, "attempted to modify participants without permissions")
}
}
if err := c.playbookRunService.RemoveParticipants(args.RunID, args.UserIDs, userID); err != nil {
return "", errors.Wrap(err, "failed to remove participant from run")
}
for _, userID := range args.UserIDs {
if err := c.playbookRunService.Unfollow(args.RunID, userID); err != nil {
return "", errors.Wrap(err, "failed to make participant to unfollow run")
}
}
return "", nil
}
func updatesOnlyRequesterMembership(requesterUserID string, userIDs []string) bool {
return len(userIDs) == 1 && userIDs[0] == requesterUserID
}
func (r *RunRootResolver) ChangeRunOwner(ctx context.Context, args struct {
RunID string
OwnerID string
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
requesterID := c.r.Header.Get("Mattermost-User-ID")
if err := c.permissions.RunManageProperties(requesterID, args.RunID); err != nil {
return "", errors.Wrap(err, "attempted to modify the run owner without permissions")
}
if err := c.playbookRunService.ChangeOwner(args.RunID, requesterID, args.OwnerID); err != nil {
return "", errors.Wrap(err, "failed to change the run owner")
}
return "", nil
}
func (r *RunRootResolver) UpdateRunTaskActions(ctx context.Context, args struct {
RunID string
ChecklistNum float64
ItemNum float64
TaskActions *[]app.TaskAction
}) (string, error) {
c, err := getContext(ctx)
if err != nil {
return "", err
}
if args.TaskActions == nil {
return "", err
}
userID := c.r.Header.Get("Mattermost-User-ID")
if err := validateTaskActions(*args.TaskActions); err != nil {
return "", err
}
if err := c.playbookRunService.SetTaskActionsToChecklistItem(args.RunID, userID, int(args.ChecklistNum), int(args.ItemNum), *args.TaskActions); err != nil {
return "", err
}
return "", nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"strconv"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
type RunResolver struct {
app.PlaybookRun
}
// NumTasks is a computed attribute (not stored in database) which
// returns the number of total tasks in a playbook run:
func (r *RunResolver) NumTasks() int32 {
total := 0
for _, checklist := range r.PlaybookRun.Checklists {
total += len(checklist.Items)
}
return int32(total)
}
// NumTasksClosed is a computed attribute (not stored in database) which
// returns the number of tasks closed in a playbook run:
func (r *RunResolver) NumTasksClosed() int32 {
closed := 0
for _, checklist := range r.PlaybookRun.Checklists {
for _, item := range checklist.Items {
if item.State == app.ChecklistItemStateClosed || item.State == app.ChecklistItemStateSkipped {
closed++
}
}
}
return int32(closed)
}
func (r *RunResolver) Type() string {
return r.PlaybookRun.Type
}
func (r *RunResolver) CreateAt() float64 {
return float64(r.PlaybookRun.CreateAt)
}
func (r *RunResolver) EndAt() float64 {
return float64(r.PlaybookRun.EndAt)
}
func (r *RunResolver) SummaryModifiedAt() float64 {
return float64(r.PlaybookRun.SummaryModifiedAt)
}
func (r *RunResolver) LastStatusUpdateAt() float64 {
return float64(r.PlaybookRun.LastStatusUpdateAt)
}
func (r *RunResolver) RetrospectivePublishedAt() float64 {
return float64(r.PlaybookRun.RetrospectivePublishedAt)
}
func (r *RunResolver) ReminderTimerDefaultSeconds() float64 {
return float64(r.PlaybookRun.ReminderTimerDefaultSeconds)
}
func (r *RunResolver) PreviousReminder() float64 {
return float64(r.PlaybookRun.PreviousReminder)
}
func (r *RunResolver) RetrospectiveReminderIntervalSeconds() float64 {
return float64(r.PlaybookRun.RetrospectiveReminderIntervalSeconds)
}
func (r *RunResolver) Checklists() []*ChecklistResolver {
checklistResolvers := make([]*ChecklistResolver, 0, len(r.PlaybookRun.Checklists))
for _, checklist := range r.PlaybookRun.Checklists {
checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist})
}
return checklistResolvers
}
func (r *RunResolver) StatusPosts() []*StatusPostResolver {
statusPostResolvers := make([]*StatusPostResolver, 0, len(r.PlaybookRun.StatusPosts))
for _, statusPost := range r.PlaybookRun.StatusPosts {
statusPostResolvers = append(statusPostResolvers, &StatusPostResolver{statusPost})
}
return statusPostResolvers
}
func (r *RunResolver) TimelineEvents() []*TimelineEventResolver {
timelineEventResolvers := make([]*TimelineEventResolver, 0, len(r.PlaybookRun.StatusPosts))
for _, event := range r.PlaybookRun.TimelineEvents {
timelineEventResolvers = append(timelineEventResolvers, &TimelineEventResolver{event})
}
return timelineEventResolvers
}
func (r *RunResolver) IsFavorite(ctx context.Context) (bool, error) {
c, err := getContext(ctx)
if err != nil {
return false, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
thunk := c.favoritesLoader.Load(ctx, favoriteInfo{
TeamID: r.TeamID,
UserID: userID,
Type: app.RunItemType,
ID: r.ID,
})
result, err := thunk()
if err != nil {
return false, err
}
return result, nil
}
type StatusPostResolver struct {
app.StatusPost
}
func (r *StatusPostResolver) CreateAt() float64 {
return float64(r.StatusPost.CreateAt)
}
func (r *StatusPostResolver) DeleteAt() float64 {
return float64(r.StatusPost.DeleteAt)
}
type TimelineEventResolver struct {
app.TimelineEvent
}
func (r *TimelineEventResolver) CreateAt() float64 {
return float64(r.TimelineEvent.CreateAt)
}
func (r *TimelineEventResolver) EventType() string {
return string(r.TimelineEvent.EventType)
}
func (r *TimelineEventResolver) DeleteAt() float64 {
return float64(r.TimelineEvent.DeleteAt)
}
func (r *RunResolver) Followers(ctx context.Context) ([]string, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
metadata, err := c.playbookRunService.GetPlaybookRunMetadata(r.ID)
if err != nil {
return nil, errors.Wrap(err, "can't get metadata")
}
return metadata.Followers, nil
}
func (r *RunResolver) Playbook(ctx context.Context) (*PlaybookResolver, error) {
c, err := getContext(ctx)
if err != nil {
return nil, err
}
userID := c.r.Header.Get("Mattermost-User-ID")
thunk := c.playbooksLoader.Load(ctx, playbookInfo{
UserID: userID,
ID: r.PlaybookID,
TeamID: r.TeamID,
})
result, err := thunk()
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
return &PlaybookResolver{*result}, nil
}
func (r *RunResolver) LastUpdatedAt(ctx context.Context) float64 {
if len(r.PlaybookRun.TimelineEvents) < 1 {
return float64(r.PlaybookRun.CreateAt)
}
return float64(r.PlaybookRun.TimelineEvents[len(r.PlaybookRun.TimelineEvents)-1].EventAt)
}
type RunConnectionResolver struct {
results app.GetPlaybookRunsResults
page int
}
func (r *RunConnectionResolver) TotalCount() int32 {
return int32(r.results.TotalCount)
}
func (r *RunConnectionResolver) Edges() []*RunEdgeResolver {
ret := make([]*RunEdgeResolver, 0, len(r.results.Items))
// Cursor is just the end cursor for the page for now
cursor := r.results.PageCount
for _, run := range r.results.Items {
ret = append(ret, &RunEdgeResolver{run, cursor})
}
return ret
}
func (r *RunConnectionResolver) PageInfo() *PageInfoResolver {
startCursor := ""
endCursor := ""
if len(r.results.Items) > 0 {
// "Cursors" are just the page numbers
startCursor = encodeRunConnectionCursor(r.page)
endCursor = encodeRunConnectionCursor(r.page + 1)
}
return &PageInfoResolver{
HasNextPage: r.results.HasMore,
StartCursor: startCursor,
EndCursor: endCursor,
}
}
func encodeRunConnectionCursor(cursor int) string {
return strconv.Itoa(cursor)
}
func decodeRunConnectionCursor(cursor string) (int, error) {
num, err := strconv.Atoi(cursor)
if err != nil {
return 0, errors.Wrap(err, "unable to decode cursor")
}
return num, nil
}
type RunEdgeResolver struct {
run app.PlaybookRun
cursor int
}
func (r *RunEdgeResolver) Node() *RunResolver {
return &RunResolver{r.run}
}
func (r *RunEdgeResolver) Cursor() string {
return encodeRunConnectionCursor(r.cursor)
}
type PageInfoResolver struct {
HasNextPage bool
StartCursor string
EndCursor string
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/sirupsen/logrus"
)
// statusRecorder intercepts and saves the status code written to an http.ResponseWriter.
type statusRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *statusRecorder) WriteHeader(code int) {
// Forward the write
r.ResponseWriter.WriteHeader(code)
// Save the status code
r.statusCode = code
}
// LogRequest logs each request, attaching a unique request_id to the request context to trace
// logs throughout the request lifecycle.
func LogRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := statusRecorder{w, 200}
requestID := model.NewId()
startMilis := time.Now().UnixNano() / int64(time.Millisecond)
logger := logrus.WithFields(logrus.Fields{
"method": r.Method,
"url": r.URL.String(),
"user_id": r.Header.Get("Mattermost-User-Id"),
"request_id": requestID,
"user_agent": r.Header.Get("User-Agent"),
})
r = r.WithContext(context.WithValue(r.Context(), requestIDContextKey, requestID))
logger.Debug("Received HTTP request")
next.ServeHTTP(&recorder, r)
gqlOp := r.Header.Get("X-GQL-Operation")
if gqlOp != "" {
logger = logger.WithField("gql_operation", gqlOp)
}
endMilis := time.Now().UnixNano() / int64(time.Millisecond)
logger.WithFields(logrus.Fields{
"time": endMilis - startMilis,
"status": recorder.statusCode,
}).Debug("Handled HTTP request")
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/client"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// PlaybookRunHandler is the API handler.
type PlaybookRunHandler struct {
*ErrorHandler
config config.Service
playbookRunService app.PlaybookRunService
playbookService app.PlaybookService
permissions *app.PermissionsService
licenseChecker app.LicenseChecker
api playbooks.ServicesAPI
poster bot.Poster
}
// NewPlaybookRunHandler Creates a new Plugin API handler.
func NewPlaybookRunHandler(
router *mux.Router,
playbookRunService app.PlaybookRunService,
playbookService app.PlaybookService,
permissions *app.PermissionsService,
licenseChecker app.LicenseChecker,
api playbooks.ServicesAPI,
poster bot.Poster,
configService config.Service,
) *PlaybookRunHandler {
handler := &PlaybookRunHandler{
ErrorHandler: &ErrorHandler{},
playbookRunService: playbookRunService,
playbookService: playbookService,
api: api,
poster: poster,
config: configService,
permissions: permissions,
licenseChecker: licenseChecker,
}
playbookRunsRouter := router.PathPrefix("/runs").Subrouter()
playbookRunsRouter.HandleFunc("", withContext(handler.getPlaybookRuns)).Methods(http.MethodGet)
playbookRunsRouter.HandleFunc("", withContext(handler.createPlaybookRunFromPost)).Methods(http.MethodPost)
playbookRunsRouter.HandleFunc("/dialog", withContext(handler.createPlaybookRunFromDialog)).Methods(http.MethodPost)
playbookRunsRouter.HandleFunc("/add-to-timeline-dialog", withContext(handler.addToTimelineDialog)).Methods(http.MethodPost)
playbookRunsRouter.HandleFunc("/owners", withContext(handler.getOwners)).Methods(http.MethodGet)
playbookRunsRouter.HandleFunc("/channels", withContext(handler.getChannels)).Methods(http.MethodGet)
playbookRunsRouter.HandleFunc("/checklist-autocomplete", withContext(handler.getChecklistAutocomplete)).Methods(http.MethodGet)
playbookRunsRouter.HandleFunc("/checklist-autocomplete-item", withContext(handler.getChecklistAutocompleteItem)).Methods(http.MethodGet)
playbookRunsRouter.HandleFunc("/runs-autocomplete", withContext(handler.getChannelRunsAutocomplete)).Methods(http.MethodGet)
playbookRunRouter := playbookRunsRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter()
playbookRunRouter.HandleFunc("", withContext(handler.getPlaybookRun)).Methods(http.MethodGet)
playbookRunRouter.HandleFunc("/metadata", withContext(handler.getPlaybookRunMetadata)).Methods(http.MethodGet)
playbookRunRouter.HandleFunc("/status-updates", withContext(handler.getStatusUpdates)).Methods(http.MethodGet)
playbookRunRouter.HandleFunc("/request-update", withContext(handler.requestUpdate)).Methods(http.MethodPost)
playbookRunRouter.HandleFunc("/request-join-channel", withContext(handler.requestJoinChannel)).Methods(http.MethodPost)
playbookRunRouterAuthorized := playbookRunRouter.PathPrefix("").Subrouter()
playbookRunRouterAuthorized.Use(handler.checkEditPermissions)
playbookRunRouterAuthorized.HandleFunc("", withContext(handler.updatePlaybookRun)).Methods(http.MethodPatch)
playbookRunRouterAuthorized.HandleFunc("/owner", withContext(handler.changeOwner)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/status", withContext(handler.status)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/finish", withContext(handler.finish)).Methods(http.MethodPut)
playbookRunRouterAuthorized.HandleFunc("/finish-dialog", withContext(handler.finishDialog)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/update-status-dialog", withContext(handler.updateStatusDialog)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/reminder/button-update", withContext(handler.reminderButtonUpdate)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/reminder", withContext(handler.reminderReset)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/no-retrospective-button", withContext(handler.noRetrospectiveButton)).Methods(http.MethodPost)
playbookRunRouterAuthorized.HandleFunc("/timeline/{eventID:[A-Za-z0-9]+}", withContext(handler.removeTimelineEvent)).Methods(http.MethodDelete)
playbookRunRouterAuthorized.HandleFunc("/restore", withContext(handler.restore)).Methods(http.MethodPut)
playbookRunRouterAuthorized.HandleFunc("/status-update-enabled", withContext(handler.toggleStatusUpdates)).Methods(http.MethodPut)
channelRouter := playbookRunsRouter.PathPrefix("/channel/{channel_id:[A-Za-z0-9]+}").Subrouter()
channelRouter.HandleFunc("", withContext(handler.getPlaybookRunByChannel)).Methods(http.MethodGet)
channelRouter.HandleFunc("/runs", withContext(handler.getPlaybookRunsForChannelByUser)).Methods(http.MethodGet)
checklistsRouter := playbookRunRouterAuthorized.PathPrefix("/checklists").Subrouter()
checklistsRouter.HandleFunc("", withContext(handler.addChecklist)).Methods(http.MethodPost)
checklistsRouter.HandleFunc("/move", withContext(handler.moveChecklist)).Methods(http.MethodPost)
checklistsRouter.HandleFunc("/move-item", withContext(handler.moveChecklistItem)).Methods(http.MethodPost)
checklistRouter := checklistsRouter.PathPrefix("/{checklist:[0-9]+}").Subrouter()
checklistRouter.HandleFunc("", withContext(handler.removeChecklist)).Methods(http.MethodDelete)
checklistRouter.HandleFunc("/add", withContext(handler.addChecklistItem)).Methods(http.MethodPost)
checklistRouter.HandleFunc("/rename", withContext(handler.renameChecklist)).Methods(http.MethodPut)
checklistRouter.HandleFunc("/add-dialog", withContext(handler.addChecklistItemDialog)).Methods(http.MethodPost)
checklistRouter.HandleFunc("/skip", withContext(handler.checklistSkip)).Methods(http.MethodPut)
checklistRouter.HandleFunc("/restore", withContext(handler.checklistRestore)).Methods(http.MethodPut)
checklistRouter.HandleFunc("/duplicate", withContext(handler.duplicateChecklist)).Methods(http.MethodPost)
checklistItem := checklistRouter.PathPrefix("/item/{item:[0-9]+}").Subrouter()
checklistItem.HandleFunc("", withContext(handler.itemDelete)).Methods(http.MethodDelete)
checklistItem.HandleFunc("", withContext(handler.itemEdit)).Methods(http.MethodPut)
checklistItem.HandleFunc("/skip", withContext(handler.itemSkip)).Methods(http.MethodPut)
checklistItem.HandleFunc("/restore", withContext(handler.itemRestore)).Methods(http.MethodPut)
checklistItem.HandleFunc("/state", withContext(handler.itemSetState)).Methods(http.MethodPut)
checklistItem.HandleFunc("/assignee", withContext(handler.itemSetAssignee)).Methods(http.MethodPut)
checklistItem.HandleFunc("/command", withContext(handler.itemSetCommand)).Methods(http.MethodPut)
checklistItem.HandleFunc("/run", withContext(handler.itemRun)).Methods(http.MethodPost)
checklistItem.HandleFunc("/duplicate", withContext(handler.itemDuplicate)).Methods(http.MethodPost)
checklistItem.HandleFunc("/duedate", withContext(handler.itemSetDueDate)).Methods(http.MethodPut)
retrospectiveRouter := playbookRunRouterAuthorized.PathPrefix("/retrospective").Subrouter()
retrospectiveRouter.HandleFunc("", withContext(handler.updateRetrospective)).Methods(http.MethodPost)
retrospectiveRouter.HandleFunc("/publish", withContext(handler.publishRetrospective)).Methods(http.MethodPost)
followersRouter := playbookRunRouter.PathPrefix("/followers").Subrouter()
followersRouter.HandleFunc("", withContext(handler.follow)).Methods(http.MethodPut)
followersRouter.HandleFunc("", withContext(handler.unfollow)).Methods(http.MethodDelete)
followersRouter.HandleFunc("", withContext(handler.getFollowers)).Methods(http.MethodGet)
return handler
}
func (h *PlaybookRunHandler) checkEditPermissions(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := getLogger(r)
vars := mux.Vars(r)
userID := r.Header.Get("Mattermost-User-ID")
playbookRun, err := h.playbookRunService.GetPlaybookRun(vars["id"])
if err != nil {
h.HandleError(w, logger, err)
return
}
if !h.PermissionsCheck(w, logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) {
return
}
next.ServeHTTP(w, r)
})
}
// createPlaybookRunFromPost handles the POST /runs endpoint
func (h *PlaybookRunHandler) createPlaybookRunFromPost(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var playbookRunCreateOptions client.PlaybookRunCreateOptions
if err := json.NewDecoder(r.Body).Decode(&playbookRunCreateOptions); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook run create options", err)
return
}
playbookRun, err := h.createPlaybookRun(
app.PlaybookRun{
OwnerUserID: playbookRunCreateOptions.OwnerUserID,
TeamID: playbookRunCreateOptions.TeamID,
ChannelID: playbookRunCreateOptions.ChannelID,
Name: playbookRunCreateOptions.Name,
Summary: playbookRunCreateOptions.Description,
PostID: playbookRunCreateOptions.PostID,
PlaybookID: playbookRunCreateOptions.PlaybookID,
Type: playbookRunCreateOptions.Type,
},
userID,
playbookRunCreateOptions.CreatePublicRun,
app.RunSourcePost,
)
if errors.Is(err, app.ErrNoPermissions) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "unable to create playbook run", err)
return
}
if errors.Is(err, app.ErrMalformedPlaybookRun) {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to create playbook run", err)
return
}
if err != nil {
h.HandleError(w, c.logger, errors.Wrapf(err, "unable to create playbook run"))
return
}
h.poster.PublishWebsocketEventToUser(app.PlaybookRunCreatedWSEvent, map[string]interface{}{
"playbook_run": playbookRun,
}, userID)
w.Header().Add("Location", fmt.Sprintf("/api/v0/runs/%s", playbookRun.ID))
ReturnJSON(w, &playbookRun, http.StatusCreated)
}
// Note that this currently does nothing. This is temporary given the removal of stages. Will be used by status.
func (h *PlaybookRunHandler) updatePlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
oldPlaybookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
var updates app.UpdateOptions
if err = json.NewDecoder(r.Body).Decode(&updates); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err)
return
}
updatedPlaybookRun := oldPlaybookRun
ReturnJSON(w, updatedPlaybookRun, http.StatusOK)
}
// createPlaybookRunFromDialog handles the interactive dialog submission when a user presses confirm on
// the create playbook run dialog.
func (h *PlaybookRunHandler) createPlaybookRunFromDialog(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var request *model.SubmitDialogRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil || request == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err)
return
}
if userID != request.UserId {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil)
return
}
var state app.DialogState
err = json.Unmarshal([]byte(request.State), &state)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal dialog state", err)
return
}
var playbookID, name string
if rawPlaybookID, ok := request.Submission[app.DialogFieldPlaybookIDKey].(string); ok {
playbookID = rawPlaybookID
}
if rawName, ok := request.Submission[app.DialogFieldNameKey].(string); ok {
name = rawName
}
playbook, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to get playbook", err)
return
}
playbookRun, err := h.createPlaybookRun(
app.PlaybookRun{
OwnerUserID: request.UserId,
TeamID: request.TeamId,
ChannelID: playbook.GetRunChannelID(),
Name: name,
PostID: state.PostID,
PlaybookID: playbookID,
Type: app.RunTypePlaybook,
},
request.UserId,
nil,
app.RunSourceDialog,
)
if err != nil {
if errors.Is(err, app.ErrMalformedPlaybookRun) {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to create playbook run", err)
return
}
if errors.Is(err, app.ErrNoPermissions) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to make runs from this playbook", err)
return
}
var msg string
if errors.Is(err, app.ErrChannelDisplayNameInvalid) {
msg = "The name is invalid or too long. Please use a valid name with fewer than 64 characters."
}
if msg != "" {
resp := &model.SubmitDialogResponse{
Errors: map[string]string{
app.DialogFieldNameKey: msg,
},
}
respBytes, _ := json.Marshal(resp)
_, _ = w.Write(respBytes)
return
}
h.HandleError(w, c.logger, err)
return
}
channel, err := h.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to get new channel", err)
return
}
// Delay sending the websocket message because the front end may try to change to the newly created
// channel, and the server may respond with a "channel not found" error. This happens in e2e tests,
// and possibly in the wild.
go func() {
time.Sleep(1 * time.Second) // arbitrary 1 second magic number
h.poster.PublishWebsocketEventToUser(app.PlaybookRunCreatedWSEvent, map[string]interface{}{
"client_id": state.ClientID,
"playbook_run": playbookRun,
"channel_name": channel.Name,
}, request.UserId)
}()
if err := h.postPlaybookRunCreatedMessage(playbookRun, request.ChannelId); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.Header().Add("Location", fmt.Sprintf("/api/v0/runs/%s", playbookRun.ID))
w.WriteHeader(http.StatusCreated)
}
// addToTimelineDialog handles the interactive dialog submission when a user clicks the
// corresponding post action.
func (h *PlaybookRunHandler) addToTimelineDialog(c *Context, w http.ResponseWriter, r *http.Request) {
if !h.licenseChecker.TimelineAllowed() {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "timeline feature is not covered by current server license", nil)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var request *model.SubmitDialogRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil || request == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err)
return
}
if userID != request.UserId {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil)
return
}
var playbookRunID, summary string
if rawPlaybookRunID, ok := request.Submission[app.DialogFieldPlaybookRunKey].(string); ok {
playbookRunID = rawPlaybookRunID
}
if rawSummary, ok := request.Submission[app.DialogFieldSummary].(string); ok {
summary = rawSummary
}
playbookRun, incErr := h.playbookRunService.GetPlaybookRun(playbookRunID)
if incErr != nil {
h.HandleError(w, c.logger, incErr)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) {
return
}
var state app.DialogStateAddToTimeline
err = json.Unmarshal([]byte(request.State), &state)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal dialog state", err)
return
}
if err = h.playbookRunService.AddPostToTimeline(playbookRunID, userID, state.PostID, summary); err != nil {
h.HandleError(w, c.logger, errors.Wrap(err, "failed to add post to timeline"))
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) createPlaybookRun(playbookRun app.PlaybookRun, userID string, createPublicRun *bool, source string) (*app.PlaybookRun, error) {
// Validate initial data
if playbookRun.ID != "" {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook run already has an id")
}
if playbookRun.CreateAt != 0 {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook run channel already has created at date")
}
if playbookRun.TeamID == "" && playbookRun.ChannelID == "" {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "must provide team or channel to create playbook run")
}
if playbookRun.OwnerUserID == "" {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "missing owner user id of playbook run")
}
if strings.TrimSpace(playbookRun.Name) == "" && playbookRun.ChannelID == "" {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "missing name of playbook run")
}
// Retrieve channel if needed and validate it
// If a channel is specified, ensure it's from the given team (if one provided), or
// just grab the team for that channel.
var channel *model.Channel
var err error
if playbookRun.ChannelID != "" {
channel, err = h.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channel")
}
if playbookRun.TeamID == "" {
playbookRun.TeamID = channel.TeamId
} else if channel.TeamId != playbookRun.TeamID {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "channel not in given team")
}
}
// Copy data from playbook if needed
public := true
if createPublicRun != nil {
public = *createPublicRun
}
var playbook *app.Playbook
if playbookRun.PlaybookID != "" {
var pb app.Playbook
pb, err = h.playbookService.Get(playbookRun.PlaybookID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get playbook")
}
playbook = &pb
if playbook.DeleteAt != 0 {
return nil, errors.New("playbook is archived, cannot create a new run using an archived playbook")
}
if err = h.permissions.RunCreate(userID, *playbook); err != nil {
return nil, err
}
if source == "dialog" && playbook.ChannelMode == app.PlaybookRunLinkExistingChannel && playbookRun.ChannelID == "" {
return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook is configured to be linked to existing channel but no channel is configured. Run can not be created from dialog")
}
if createPublicRun == nil {
public = pb.CreatePublicPlaybookRun
}
playbookRun.SetChecklistFromPlaybook(*playbook)
playbookRun.SetConfigurationFromPlaybook(*playbook, source)
}
// Check the permissions on the channel: the user must be able to create it or,
// if one's already provided, they need to be able to manage it.
if channel == nil {
permission := model.PermissionCreatePrivateChannel
permissionMessage := "You are not able to create a private channel"
if public {
permission = model.PermissionCreatePublicChannel
permissionMessage = "You are not able to create a public channel"
}
if !h.api.HasPermissionToTeam(userID, playbookRun.TeamID, permission) {
return nil, errors.Wrap(app.ErrNoPermissions, permissionMessage)
}
} else {
permission := model.PermissionManagePublicChannelProperties
permissionMessage := "You are not able to manage public channel properties"
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelProperties
permissionMessage = "You are not able to manage private channel properties"
} else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
permission = model.PermissionReadChannel
permissionMessage = "You do not have access to this channel"
}
if !h.api.HasPermissionToChannel(userID, channel.Id, permission) {
return nil, errors.Wrap(app.ErrNoPermissions, permissionMessage)
}
}
// Check the permissions on the provided post: the user must have access to the post's channel
if playbookRun.PostID != "" {
var post *model.Post
post, err = h.api.GetPost(playbookRun.PostID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get playbook run original post")
}
if !h.api.HasPermissionToChannel(userID, post.ChannelId, model.PermissionReadChannel) {
return nil, errors.New("user does not have access to the channel containing the playbook run's original post")
}
}
playbookRunReturned, err := h.playbookRunService.CreatePlaybookRun(&playbookRun, playbook, userID, public)
if err != nil {
return nil, err
}
// force database retrieval to ensure all data is processed correctly (i.e participantIds)
return h.playbookRunService.GetPlaybookRun(playbookRunReturned.ID)
}
func (h *PlaybookRunHandler) getRequesterInfo(userID string) (app.RequesterInfo, error) {
return app.GetRequesterInfo(userID, h.api)
}
// getPlaybookRuns handles the GET /runs endpoint.
func (h *PlaybookRunHandler) getPlaybookRuns(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
filterOptions, err := parsePlaybookRunsFilterOptions(r.URL, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad parameter", err)
return
}
requesterInfo, err := h.getRequesterInfo(userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
results, err := h.playbookRunService.GetPlaybookRuns(requesterInfo, *filterOptions)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, results, http.StatusOK)
}
// getPlaybookRun handles the /runs/{id} endpoint.
func (h *PlaybookRunHandler) getPlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
return
}
playbookRunToGet, err := h.playbookRunService.GetPlaybookRun(playbookRunID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, playbookRunToGet, http.StatusOK)
}
// getPlaybookRunMetadata handles the /runs/{id}/metadata endpoint.
func (h *PlaybookRunHandler) getPlaybookRunMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
return
}
playbookRunMetadata, err := h.playbookRunService.GetPlaybookRunMetadata(playbookRunID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, playbookRunMetadata, http.StatusOK)
}
// getPlaybookRunByChannel handles the /runs/channel/{channel_id} endpoint.
// Notice that it returns both playbook runs as well as channel checklists
func (h *PlaybookRunHandler) getPlaybookRunByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
channelID := vars["channel_id"]
userID := r.Header.Get("Mattermost-User-ID")
// get playbook runs for the specific channel and user
playbookRunsResult, err := h.playbookRunService.GetPlaybookRuns(
app.RequesterInfo{
UserID: userID,
},
app.PlaybookRunFilterOptions{
ChannelID: channelID,
Page: 0,
PerPage: 2,
},
)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
playbookRuns := playbookRunsResult.Items
if len(playbookRuns) == 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found",
errors.Errorf("playbook run for channel id %s not found", channelID))
return
}
if len(playbookRuns) > 1 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "multiple runs in the channel", nil)
return
}
playbookRun := playbookRuns[0]
ReturnJSON(w, &playbookRun, http.StatusOK)
}
// getOwners handles the /runs/owners api endpoint.
func (h *PlaybookRunHandler) getOwners(c *Context, w http.ResponseWriter, r *http.Request) {
teamID := r.URL.Query().Get("team_id")
userID := r.Header.Get("Mattermost-User-ID")
options := app.PlaybookRunFilterOptions{
TeamID: teamID,
}
requesterInfo, err := h.getRequesterInfo(userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
owners, err := h.playbookRunService.GetOwners(requesterInfo, options)
if err != nil {
h.HandleError(w, c.logger, errors.Wrapf(err, "failed to get owners"))
return
}
if owners == nil {
owners = []app.OwnerInfo{}
}
ReturnJSON(w, owners, http.StatusOK)
}
func (h *PlaybookRunHandler) getChannels(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
filterOptions, err := parsePlaybookRunsFilterOptions(r.URL, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad parameter", err)
return
}
requesterInfo, err := h.getRequesterInfo(userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
playbookRuns, err := h.playbookRunService.GetPlaybookRuns(requesterInfo, *filterOptions)
if err != nil {
h.HandleError(w, c.logger, errors.Wrapf(err, "failed to get playbookRuns"))
return
}
channelIds := make([]string, 0, len(playbookRuns.Items))
for _, playbookRun := range playbookRuns.Items {
channelIds = append(channelIds, playbookRun.ChannelID)
}
ReturnJSON(w, channelIds, http.StatusOK)
}
// changeOwner handles the /runs/{id}/change-owner api endpoint.
func (h *PlaybookRunHandler) changeOwner(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
OwnerID string `json:"owner_id"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "could not decode request body", err)
return
}
if err := h.playbookRunService.ChangeOwner(vars["id"], userID, params.OwnerID); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{}, http.StatusOK)
}
// updateStatusD handles the POST /runs/{id}/status endpoint, user has edit permissions
func (h *PlaybookRunHandler) status(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
var options app.StatusUpdateOptions
if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode body into StatusUpdateOptions", err)
return
}
if publicMsg, internalErr := h.updateStatus(playbookRunID, userID, options); internalErr != nil {
if errors.Is(internalErr, app.ErrNoPermissions) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, publicMsg, internalErr)
} else {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, publicMsg, internalErr)
}
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"OK"}`))
}
// updateStatus returns a publicMessage and an internal error
func (h *PlaybookRunHandler) updateStatus(playbookRunID, userID string, options app.StatusUpdateOptions) (string, error) {
// user must be a participant to be able to post an update
if err := h.permissions.RunManageProperties(userID, playbookRunID); err != nil {
return "Not authorized", err
}
options.Message = strings.TrimSpace(options.Message)
if options.Message == "" {
return "message must not be empty", errors.New("message field empty")
}
if options.Reminder <= 0 && !options.FinishRun {
return "the reminder must be set and not 0", errors.New("reminder was 0")
}
if options.Reminder < 0 || options.FinishRun {
options.Reminder = 0
}
options.Reminder = options.Reminder * time.Second
if err := h.playbookRunService.UpdateStatus(playbookRunID, userID, options); err != nil {
return "An internal error has occurred. Check app server logs for details.", err
}
if options.FinishRun {
if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil {
return "An internal error has occurred. Check app server logs for details.", err
}
}
return "", nil
}
// updateStatusD handles the POST /runs/{id}/finish endpoint, user has edit permissions
func (h *PlaybookRunHandler) finish(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"OK"}`))
}
// getStatusUpdates handles the GET /runs/{id}/status endpoint
//
// Our goal is to deliver status updates to any user (when playbook is public) or
// any playbook member (when playbook is private). To do that we need to bypass the
// permissions system and avoid checking channel membership.
//
// This approach will be deprecated as a step towards channel-playbook decoupling.
func (h *PlaybookRunHandler) getStatusUpdates(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to get status updates", nil)
return
}
playbookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
posts := make([]*app.StatusPostComplete, 0)
for _, p := range playbookRun.StatusPosts {
post, err := h.api.GetPost(p.ID)
if err != nil {
c.logger.WithError(err).WithField("post_id", p.ID).Error("statusUpdates: can not retrieve post")
continue
}
// Given the fact that we are bypassing some permissions,
// an additional check is added to limit the risk
if post.Type == "custom_run_update" {
posts = append(posts, app.NewStatusPostComplete(post))
}
}
// sort by creation date, so that the first element is the newest post
sort.Slice(posts, func(i, j int) bool {
return posts[i].CreateAt > posts[j].CreateAt
})
ReturnJSON(w, posts, http.StatusOK)
}
// restore "un-finishes" a playbook run
func (h *PlaybookRunHandler) restore(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.RestorePlaybookRun(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"OK"}`))
}
// requestUpdate posts a status update request message in the run's channel
func (h *PlaybookRunHandler) requestUpdate(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to post update request", nil)
return
}
if err := h.playbookRunService.RequestUpdate(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
}
// requestJoinChannel posts a channel-join request message in the run's channel
func (h *PlaybookRunHandler) requestJoinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
// user must be a participant to be able to request to join the channel
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRunID)) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to request join channel", nil)
return
}
if err := h.playbookRunService.RequestJoinChannel(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
}
// updateStatusDialog handles the POST /runs/{id}/finish-dialog endpoint, called when a
// user submits the Finish Run dialog.
func (h *PlaybookRunHandler) finishDialog(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbookRun, incErr := h.playbookRunService.GetPlaybookRun(playbookRunID)
if incErr != nil {
h.HandleError(w, c.logger, incErr)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) {
return
}
if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
}
func (h *PlaybookRunHandler) toggleStatusUpdates(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
var payload struct {
StatusEnabled bool `json:"status_enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.HandleError(w, c.logger, err)
return
}
if err := h.playbookRunService.ToggleStatusUpdates(playbookRunID, userID, payload.StatusEnabled); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{"success": true}, http.StatusOK)
}
// updateStatusDialog handles the POST /runs/{id}/update-status-dialog endpoint, called when a
// user submits the Update Status dialog.
func (h *PlaybookRunHandler) updateStatusDialog(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
var request *model.SubmitDialogRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil || request == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err)
return
}
var options app.StatusUpdateOptions
if message, ok := request.Submission[app.DialogFieldMessageKey]; ok {
options.Message = message.(string)
}
if reminderI, ok := request.Submission[app.DialogFieldReminderInSecondsKey]; ok {
var reminder int
reminder, err = strconv.Atoi(reminderI.(string))
if err != nil {
h.HandleError(w, c.logger, err)
return
}
options.Reminder = time.Duration(reminder)
}
if finishB, ok := request.Submission[app.DialogFieldFinishRun]; ok {
var finish bool
if finish, ok = finishB.(bool); ok {
options.FinishRun = finish
}
}
if publicMsg, internalErr := h.updateStatus(playbookRunID, userID, options); internalErr != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, publicMsg, internalErr)
return
}
w.WriteHeader(http.StatusOK)
}
// reminderButtonUpdate handles the POST /runs/{id}/reminder/button-update endpoint, called when a
// user clicks on the reminder interactive button
func (h *PlaybookRunHandler) reminderButtonUpdate(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
var requestData *model.PostActionIntegrationRequest
err := json.NewDecoder(r.Body).Decode(&requestData)
if err != nil || requestData == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "missing request data", nil)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(requestData.UserId, playbookRunID)) {
return
}
if err = h.playbookRunService.OpenUpdateStatusDialog(playbookRunID, requestData.UserId, requestData.TriggerId); err != nil {
h.HandleError(w, c.logger, errors.New("reminderButtonUpdate failed to open update status dialog"))
return
}
ReturnJSON(w, nil, http.StatusOK)
}
// reminderReset handles the POST /runs/{id}/reminder endpoint, called when a
// user clicks on the reminder custom_update_status time selector
func (h *PlaybookRunHandler) reminderReset(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
var payload struct {
NewReminderSeconds int `json:"new_reminder_seconds"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.HandleError(w, c.logger, err)
return
}
if payload.NewReminderSeconds <= 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "new_reminder_seconds must be > 0", errors.New("new_reminder_seconds was <= 0"))
return
}
storedPlaybookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID)
if err != nil {
err = errors.Wrapf(err, "reminderReset: no playbook run for path's playbookRunID: %s", playbookRunID)
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "no playbook run for path's playbookRunID", err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, storedPlaybookRun.ID)) {
return
}
if err = h.playbookRunService.ResetReminder(playbookRunID, time.Duration(payload.NewReminderSeconds)*time.Second); err != nil {
err = errors.Wrapf(err, "reminderReset: error setting new reminder for playbookRunID %s", playbookRunID)
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error removing reminder post", err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) noRetrospectiveButton(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbookRunToCancelRetro, err := h.playbookRunService.GetPlaybookRun(playbookRunID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRunToCancelRetro.ID)) {
return
}
if err := h.playbookRunService.CancelRetrospective(playbookRunToCancelRetro.ID, userID); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to cancel retrospective", err)
return
}
ReturnJSON(w, nil, http.StatusOK)
}
// removeTimelineEvent handles the DELETE /runs/{id}/timeline/{eventID} endpoint.
// User has been authenticated to edit the playbook run.
func (h *PlaybookRunHandler) removeTimelineEvent(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
eventID := vars["eventID"]
if err := h.playbookRunService.RemoveTimelineEvent(id, userID, eventID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) getChecklistAutocompleteItem(c *Context, w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
channelID := query.Get("channel_id")
userID := r.Header.Get("Mattermost-User-ID")
playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError,
fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err)
return
}
if len(playbookRuns) == 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found",
errors.Errorf("playbook run for channel id %s not found", channelID))
return
}
data, err := h.playbookRunService.GetChecklistItemAutocomplete(playbookRuns)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, data, http.StatusOK)
}
func (h *PlaybookRunHandler) getChecklistAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
channelID := query.Get("channel_id")
userID := r.Header.Get("Mattermost-User-ID")
playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError,
fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err)
return
}
if len(playbookRuns) == 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found",
errors.Errorf("playbook run for channel id %s not found", channelID))
return
}
data, err := h.playbookRunService.GetChecklistAutocomplete(playbookRuns)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, data, http.StatusOK)
}
func (h *PlaybookRunHandler) getChannelRunsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
channelID := query.Get("channel_id")
userID := r.Header.Get("Mattermost-User-ID")
playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError,
fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err)
return
}
if len(playbookRuns) == 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found",
errors.Errorf("playbook run for channel id %s not found", channelID))
return
}
data, err := h.playbookRunService.GetRunsAutocomplete(playbookRuns)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, data, http.StatusOK)
}
func (h *PlaybookRunHandler) getPlaybookRunsForChannelByUser(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
channelID := vars["channel_id"]
userID := r.Header.Get("Mattermost-User-ID")
playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError,
fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err)
return
}
ReturnJSON(w, playbookRuns, http.StatusOK)
}
func (h *PlaybookRunHandler) itemSetState(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
NewState string `json:"new_state"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err)
return
}
if !app.IsValidChecklistItemState(params.NewState) {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter new state", nil)
return
}
if err := h.playbookRunService.ModifyCheckedState(id, userID, params.NewState, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{}, http.StatusOK)
}
func (h *PlaybookRunHandler) itemSetAssignee(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
AssigneeID string `json:"assignee_id"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err)
return
}
if err := h.playbookRunService.SetAssignee(id, userID, params.AssigneeID, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{}, http.StatusOK)
}
func (h *PlaybookRunHandler) itemSetDueDate(c *Context, w http.ResponseWriter, r *http.Request) {
if !h.licenseChecker.ChecklistItemDueDateAllowed() {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "checklist item due date feature is not covered by current server license", nil)
return
}
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
DueDate int64 `json:"due_date"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err)
return
}
if err := h.playbookRunService.SetDueDate(id, userID, params.DueDate, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{}, http.StatusOK)
}
func (h *PlaybookRunHandler) itemSetCommand(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
Command string `json:"command"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err)
return
}
if err := h.playbookRunService.SetCommandToChecklistItem(id, userID, checklistNum, itemNum, params.Command); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{}, http.StatusOK)
}
func (h *PlaybookRunHandler) itemRun(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
triggerID, err := h.playbookRunService.RunChecklistItemSlashCommand(playbookRunID, userID, checklistNum, itemNum)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, map[string]interface{}{"trigger_id": triggerID}, http.StatusOK)
}
func (h *PlaybookRunHandler) itemDuplicate(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.DuplicateChecklistItem(playbookRunID, userID, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusCreated)
}
func (h *PlaybookRunHandler) addChecklist(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var checklist app.Checklist
if err := json.NewDecoder(r.Body).Decode(&checklist); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode Checklist", err)
return
}
checklist.Title = strings.TrimSpace(checklist.Title)
if checklist.Title == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist title",
errors.New("checklist title must not be blank"))
return
}
if err := h.playbookRunService.AddChecklist(id, userID, checklist); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusCreated)
}
func (h *PlaybookRunHandler) removeChecklist(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.RemoveChecklist(id, userID, checklistNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusCreated)
}
func (h *PlaybookRunHandler) duplicateChecklist(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.DuplicateChecklist(playbookRunID, userID, checklistNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusCreated)
}
func (h *PlaybookRunHandler) addChecklistItem(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var checklistItem app.ChecklistItem
if err := json.NewDecoder(r.Body).Decode(&checklistItem); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode ChecklistItem", err)
return
}
checklistItem.Title = strings.TrimSpace(checklistItem.Title)
if checklistItem.Title == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist item title",
errors.New("checklist item title must not be blank"))
return
}
if err := h.playbookRunService.AddChecklistItem(id, userID, checklistNum, checklistItem); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusCreated)
}
// addChecklistItemDialog handles the interactive dialog submission when a user clicks add new task
func (h *PlaybookRunHandler) addChecklistItemDialog(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
vars := mux.Vars(r)
playbookRunID := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
var request *model.SubmitDialogRequest
err = json.NewDecoder(r.Body).Decode(&request)
if err != nil || request == nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err)
return
}
if userID != request.UserId {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil)
return
}
var name, description string
if rawName, ok := request.Submission[app.DialogFieldItemNameKey].(string); ok {
name = rawName
}
if rawDescription, ok := request.Submission[app.DialogFieldItemDescriptionKey].(string); ok {
description = rawDescription
}
checklistItem := app.ChecklistItem{
Title: name,
Description: description,
}
checklistItem.Title = strings.TrimSpace(checklistItem.Title)
if checklistItem.Title == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist item title",
errors.New("checklist item title must not be blank"))
return
}
if err := h.playbookRunService.AddChecklistItem(playbookRunID, userID, checklistNum, checklistItem); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) itemDelete(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.RemoveChecklistItem(id, userID, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) checklistSkip(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.SkipChecklist(id, userID, checklistNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) checklistRestore(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.RestoreChecklist(id, userID, checklistNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) itemSkip(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.SkipChecklistItem(id, userID, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) itemRestore(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.RestoreChecklistItem(id, userID, checklistNum, itemNum); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookRunHandler) itemEdit(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
itemNum, err := strconv.Atoi(vars["item"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
Title string `json:"title"`
Command string `json:"command"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params state", err)
return
}
if err := h.playbookRunService.EditChecklistItem(id, userID, checklistNum, itemNum, params.Title, params.Command, params.Description); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) renameChecklist(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
checklistNum, err := strconv.Atoi(vars["checklist"])
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err)
return
}
userID := r.Header.Get("Mattermost-User-ID")
var modificationParams struct {
NewTitle string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&modificationParams); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal new title", err)
return
}
if modificationParams.NewTitle == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist title",
errors.New("checklist title must not be blank"))
return
}
if err := h.playbookRunService.RenameChecklist(id, userID, checklistNum, modificationParams.NewTitle); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) moveChecklist(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params", err)
return
}
if err := h.playbookRunService.MoveChecklist(id, userID, params.SourceChecklistIdx, params.DestChecklistIdx); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) moveChecklistItem(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
SourceItemIdx int `json:"source_item_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
DestItemIdx int `json:"dest_item_idx"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params", err)
return
}
if err := h.playbookRunService.MoveChecklistItem(id, userID, params.SourceChecklistIdx, params.SourceItemIdx, params.DestChecklistIdx, params.DestItemIdx); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) postPlaybookRunCreatedMessage(playbookRun *app.PlaybookRun, channelID string) error {
channel, err := h.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
return err
}
post := &model.Post{
Message: fmt.Sprintf("Playbook run %s started in ~%s", playbookRun.Name, channel.Name),
}
h.poster.EphemeralPost(playbookRun.OwnerUserID, channelID, post)
return nil
}
func (h *PlaybookRunHandler) updateRetrospective(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var retroUpdate app.RetrospectiveUpdate
if err := json.NewDecoder(r.Body).Decode(&retroUpdate); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err)
return
}
if err := h.playbookRunService.UpdateRetrospective(playbookRunID, userID, retroUpdate); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to update retrospective", err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) publishRetrospective(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookRunID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var retroUpdate app.RetrospectiveUpdate
if err := json.NewDecoder(r.Body).Decode(&retroUpdate); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err)
return
}
if err := h.playbookRunService.PublishRetrospective(playbookRunID, userID, retroUpdate); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to publish retrospective", err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) follow(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
return
}
if err := h.playbookRunService.Follow(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) unfollow(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if err := h.playbookRunService.Unfollow(playbookRunID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookRunHandler) getFollowers(c *Context, w http.ResponseWriter, r *http.Request) {
playbookRunID := mux.Vars(r)["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) {
return
}
var followers []string
var err error
if followers, err = h.playbookRunService.GetFollowers(playbookRunID); err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, followers, http.StatusOK)
}
// parsePlaybookRunsFilterOptions is only for parsing. Put validation logic in app.validateOptions.
func parsePlaybookRunsFilterOptions(u *url.URL, currentUserID string) (*app.PlaybookRunFilterOptions, error) {
teamID := u.Query().Get("team_id")
pageParam := u.Query().Get("page")
if pageParam == "" {
pageParam = "0"
}
page, err := strconv.Atoi(pageParam)
if err != nil {
return nil, errors.Wrapf(err, "bad parameter 'page'")
}
perPageParam := u.Query().Get("per_page")
if perPageParam == "" {
perPageParam = "0"
}
perPage, err := strconv.Atoi(perPageParam)
if err != nil {
return nil, errors.Wrapf(err, "bad parameter 'per_page'")
}
sort := u.Query().Get("sort")
direction := u.Query().Get("direction")
// Parse statuses= query string parameters as an array.
statuses := u.Query()["statuses"]
ownerID := u.Query().Get("owner_user_id")
if ownerID == client.Me {
ownerID = currentUserID
}
searchTerm := u.Query().Get("search_term")
participantID := u.Query().Get("participant_id")
if participantID == client.Me {
participantID = currentUserID
}
participantOrFollowerID := u.Query().Get("participant_or_follower_id")
if participantOrFollowerID == client.Me {
participantOrFollowerID = currentUserID
}
playbookID := u.Query().Get("playbook_id")
activeGTEParam := u.Query().Get("active_gte")
if activeGTEParam == "" {
activeGTEParam = "0"
}
activeGTE, _ := strconv.ParseInt(activeGTEParam, 10, 64)
activeLTParam := u.Query().Get("active_lt")
if activeLTParam == "" {
activeLTParam = "0"
}
activeLT, _ := strconv.ParseInt(activeLTParam, 10, 64)
startedGTEParam := u.Query().Get("started_gte")
if startedGTEParam == "" {
startedGTEParam = "0"
}
startedGTE, _ := strconv.ParseInt(startedGTEParam, 10, 64)
startedLTParam := u.Query().Get("started_lt")
if startedLTParam == "" {
startedLTParam = "0"
}
startedLT, _ := strconv.ParseInt(startedLTParam, 10, 64)
// Parse types= query string parameters as an array.
types := u.Query()["types"]
options := app.PlaybookRunFilterOptions{
TeamID: teamID,
Page: page,
PerPage: perPage,
Sort: app.SortField(sort),
Direction: app.SortDirection(direction),
Statuses: statuses,
OwnerID: ownerID,
SearchTerm: searchTerm,
ParticipantID: participantID,
ParticipantOrFollowerID: participantOrFollowerID,
PlaybookID: playbookID,
ActiveGTE: activeGTE,
ActiveLT: activeLT,
StartedGTE: startedGTE,
StartedLT: startedLT,
Types: types,
}
options, err = options.Validate()
if err != nil {
return nil, err
}
return &options, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/timeutils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// PlaybookHandler is the API handler.
type PlaybookHandler struct {
*ErrorHandler
playbookService app.PlaybookService
api playbooks.ServicesAPI
config config.Service
permissions *app.PermissionsService
}
const SettingsKey = "global_settings"
const maxPlaybooksToAutocomplete = 15
// NewPlaybookHandler returns a new playbook api handler
func NewPlaybookHandler(router *mux.Router, playbookService app.PlaybookService, api playbooks.ServicesAPI, configService config.Service, permissions *app.PermissionsService) *PlaybookHandler {
handler := &PlaybookHandler{
ErrorHandler: &ErrorHandler{},
playbookService: playbookService,
api: api,
config: configService,
permissions: permissions,
}
playbooksRouter := router.PathPrefix("/playbooks").Subrouter()
playbooksRouter.HandleFunc("", withContext(handler.createPlaybook)).Methods(http.MethodPost)
playbooksRouter.HandleFunc("", withContext(handler.getPlaybooks)).Methods(http.MethodGet)
playbooksRouter.HandleFunc("/autocomplete", withContext(handler.getPlaybooksAutoComplete)).Methods(http.MethodGet)
playbooksRouter.HandleFunc("/import", withContext(handler.importPlaybook)).Methods(http.MethodPost)
playbookRouter := playbooksRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter()
playbookRouter.HandleFunc("", withContext(handler.getPlaybook)).Methods(http.MethodGet)
playbookRouter.HandleFunc("", withContext(handler.updatePlaybook)).Methods(http.MethodPut)
playbookRouter.HandleFunc("", withContext(handler.archivePlaybook)).Methods(http.MethodDelete)
playbookRouter.HandleFunc("/restore", withContext(handler.restorePlaybook)).Methods(http.MethodPut)
playbookRouter.HandleFunc("/export", withContext(handler.exportPlaybook)).Methods(http.MethodGet)
playbookRouter.HandleFunc("/duplicate", withContext(handler.duplicatePlaybook)).Methods(http.MethodPost)
autoFollowsRouter := playbookRouter.PathPrefix("/autofollows").Subrouter()
autoFollowsRouter.HandleFunc("", withContext(handler.getAutoFollows)).Methods(http.MethodGet)
autoFollowRouter := autoFollowsRouter.PathPrefix("/{userID:[A-Za-z0-9]+}").Subrouter()
autoFollowRouter.HandleFunc("", withContext(handler.autoFollow)).Methods(http.MethodPut)
autoFollowRouter.HandleFunc("", withContext(handler.autoUnfollow)).Methods(http.MethodDelete)
insightsRouter := playbooksRouter.PathPrefix("/insights").Subrouter()
insightsRouter.HandleFunc("/user/me", withContext(handler.getTopPlaybooksForUser)).Methods(http.MethodGet)
insightsRouter.HandleFunc("/teams/{teamID}", withContext(handler.getTopPlaybooksForTeam)).Methods(http.MethodGet)
return handler
}
func (h *PlaybookHandler) validPlaybook(w http.ResponseWriter, logger logrus.FieldLogger, playbook *app.Playbook) bool {
if playbook.WebhookOnCreationEnabled {
if err := app.ValidateWebhookURLs(playbook.WebhookOnCreationURLs); err != nil {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err)
return false
}
}
if playbook.WebhookOnStatusUpdateEnabled {
if err := app.ValidateWebhookURLs(playbook.WebhookOnStatusUpdateURLs); err != nil {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err)
return false
}
}
if playbook.CategorizeChannelEnabled {
if err := app.ValidateCategoryName(playbook.CategoryName); err != nil {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid category name", err)
return false
}
}
if len(playbook.SignalAnyKeywords) != 0 {
playbook.SignalAnyKeywords = app.ProcessSignalAnyKeywords(playbook.SignalAnyKeywords)
}
if playbook.BroadcastEnabled { //nolint
for _, channelID := range playbook.BroadcastChannelIDs {
channel, err := h.api.GetChannelByID(channelID)
if err != nil {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to invalid channel ID", err)
return false
}
// check if channel is archived
if channel.DeleteAt != 0 {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to archived channel", err)
return false
}
}
}
for listIndex := range playbook.Checklists {
for itemIndex := range playbook.Checklists[listIndex].Items {
if err := validateTaskActions(playbook.Checklists[listIndex].Items[itemIndex].TaskActions); err != nil {
h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid task actions", err)
return false
}
}
}
return true
}
func (h *PlaybookHandler) createPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var playbook app.Playbook
if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err)
return
}
if playbook.ID != "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook given already has ID", nil)
return
}
if playbook.ReminderTimerDefaultSeconds <= 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook ReminderTimerDefaultSeconds must be > 0", nil)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
return
}
// If not specified make the creator the sole admin
if len(playbook.Members) == 0 {
playbook.Members = []app.PlaybookMember{
{
UserID: userID,
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
},
}
}
if !h.validPlaybook(w, c.logger, &playbook) {
return
}
if err := h.validateMetrics(playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err)
return
}
app.CleanUpChecklists(playbook.Checklists)
if err := validatePreAssignment(playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid pre-assignment", err)
return
}
id, err := h.playbookService.Create(playbook, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
result := struct {
ID string `json:"id"`
}{
ID: id,
}
w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id))
ReturnJSON(w, &result, http.StatusCreated)
}
func (h *PlaybookHandler) getPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
return
}
playbook, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, &playbook, http.StatusOK)
}
func (h *PlaybookHandler) updatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := r.Header.Get("Mattermost-User-ID")
var playbook app.Playbook
if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err)
return
}
// Force parsed playbook id to be URL parameter id
playbook.ID = vars["id"]
oldPlaybook, err := h.playbookService.Get(playbook.ID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if err = h.validateMetrics(playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookModifyWithFixes(userID, &playbook, oldPlaybook)) {
return
}
if oldPlaybook.DeleteAt != 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook cannot be modified", fmt.Errorf("playbook with id '%s' cannot be modified because it is archived", playbook.ID))
return
}
if !h.validPlaybook(w, c.logger, &playbook) {
return
}
app.CleanUpChecklists(playbook.Checklists)
if err = validatePreAssignment(playbook); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid user pre-assignment", err)
return
}
err = h.playbookService.Update(playbook, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func validatePreAssignment(pb app.Playbook) error {
assignees := app.GetDistinctAssignees(pb.Checklists)
return app.ValidatePreAssignment(assignees, pb.InvitedUserIDs, pb.InviteUsersEnabled)
}
// validateTaskActions validates the taskactions in the given checklist
// NOTE: Any changes to this function must be made to function 'validateUpdateTaskActions' for the GraphQL endpoint.
func validateTaskActions(taskActions []app.TaskAction) error {
for _, ta := range taskActions {
if err := app.ValidateTrigger(ta.Trigger); err != nil {
return err
}
for _, a := range ta.Actions {
if err := app.ValidateAction(a); err != nil {
return err
}
}
}
return nil
}
func (h *PlaybookHandler) archivePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbookToArchive, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToArchive)) {
return
}
err = h.playbookService.Archive(playbookToArchive, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookHandler) restorePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbookToRestore, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToRestore)) {
return
}
err = h.playbookService.Restore(playbookToRestore, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *PlaybookHandler) getPlaybooks(c *Context, w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
teamID := params.Get("team_id")
userID := r.Header.Get("Mattermost-User-ID")
opts, err := parseGetPlaybooksOptions(r.URL)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("failed to get playbooks: %s", err.Error()), nil)
return
}
if teamID != "" && !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
return
}
requesterInfo := app.RequesterInfo{
UserID: userID,
TeamID: teamID,
IsAdmin: app.IsSystemAdmin(userID, h.api),
}
playbookResults, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, opts)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, playbookResults, http.StatusOK)
}
func (h *PlaybookHandler) getPlaybooksAutoComplete(c *Context, w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
teamID := query.Get("team_id")
userID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
return
}
requesterInfo := app.RequesterInfo{
UserID: userID,
TeamID: teamID,
IsAdmin: app.IsSystemAdmin(userID, h.api),
}
playbooksResult, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, app.PlaybookFilterOptions{
Page: 0,
PerPage: maxPlaybooksToAutocomplete,
WithArchived: query.Get("with_archived") == "true",
})
if err != nil {
h.HandleError(w, c.logger, err)
return
}
list := make([]model.AutocompleteListItem, 0)
for _, playbook := range playbooksResult.Items {
list = append(list, model.AutocompleteListItem{
Item: playbook.ID,
HelpText: playbook.Title,
})
}
ReturnJSON(w, list, http.StatusOK)
}
func parseGetPlaybooksOptions(u *url.URL) (app.PlaybookFilterOptions, error) {
params := u.Query()
var sortField app.SortField
param := strings.ToLower(params.Get("sort"))
switch param {
case "title", "":
sortField = app.SortByTitle
case "stages":
sortField = app.SortByStages
case "steps":
sortField = app.SortBySteps
case "runs":
sortField = app.SortByRuns
case "last_run_at":
sortField = app.SortByLastRunAt
case "active_runs":
sortField = app.SortByActiveRuns
default:
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'sort' (%s): it should be empty or one of 'title', 'stages', 'steps', 'runs', 'last_run_at'", param)
}
var sortDirection app.SortDirection
param = strings.ToLower(params.Get("direction"))
switch param {
case "asc", "":
sortDirection = app.DirectionAsc
case "desc":
sortDirection = app.DirectionDesc
default:
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'direction' (%s): it should be empty or one of 'asc' or 'desc'", param)
}
pageParam := params.Get("page")
if pageParam == "" {
pageParam = "0"
}
page, err := strconv.Atoi(pageParam)
if err != nil {
return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'page': it should be a number")
}
if page < 0 {
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'page': it should be a positive number")
}
perPageParam := params.Get("per_page")
if perPageParam == "" || perPageParam == "0" {
perPageParam = "1000"
}
perPage, err := strconv.Atoi(perPageParam)
if err != nil {
return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'per_page': it should be a number")
}
if perPage < 0 {
return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'per_page': it should be a positive number")
}
searchTerm := u.Query().Get("search_term")
withArchived, _ := strconv.ParseBool(u.Query().Get("with_archived"))
return app.PlaybookFilterOptions{
Sort: sortField,
Direction: sortDirection,
Page: page,
PerPage: perPage,
SearchTerm: searchTerm,
WithArchived: withArchived,
}, nil
}
func (h *PlaybookHandler) autoFollow(c *Context, w http.ResponseWriter, r *http.Request) {
playbookID := mux.Vars(r)["id"]
currentUserID := r.Header.Get("Mattermost-User-ID")
userID := mux.Vars(r)["userID"]
if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
return
}
if err := h.playbookService.AutoFollow(playbookID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *PlaybookHandler) autoUnfollow(c *Context, w http.ResponseWriter, r *http.Request) {
playbookID := mux.Vars(r)["id"]
currentUserID := r.Header.Get("Mattermost-User-ID")
userID := mux.Vars(r)["userID"]
if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) {
return
}
if err := h.playbookService.AutoUnfollow(playbookID, userID); err != nil {
h.HandleError(w, c.logger, err)
return
}
w.WriteHeader(http.StatusOK)
}
// getAutoFollows returns the list of users that have marked this playbook for auto-following runs
func (h *PlaybookHandler) getAutoFollows(c *Context, w http.ResponseWriter, r *http.Request) {
playbookID := mux.Vars(r)["id"]
currentUserID := r.Header.Get("Mattermost-User-ID")
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(currentUserID, playbookID)) {
return
}
autoFollowers, err := h.playbookService.GetAutoFollows(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, autoFollowers, http.StatusOK)
}
func (h *PlaybookHandler) exportPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbook, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) {
return
}
export, err := app.GeneratePlaybookExport(playbook)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(export)
}
func (h *PlaybookHandler) duplicatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
playbookID := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
playbook, err := h.playbookService.Get(playbookID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) {
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
return
}
newPlaybookID, err := h.playbookService.Duplicate(playbook, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
result := struct {
ID string `json:"id"`
}{
ID: newPlaybookID,
}
ReturnJSON(w, &result, http.StatusCreated)
}
func (h *PlaybookHandler) importPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
teamID := params.Get("team_id")
userID := r.Header.Get("Mattermost-User-ID")
var importBlock struct {
app.Playbook
Version int `json:"version"`
}
if err := json.NewDecoder(r.Body).Decode(&importBlock); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook import", err)
return
}
playbook := importBlock.Playbook
if playbook.ID != "" {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook import should not have ID field", nil)
return
}
if importBlock.Version != app.CurrentPlaybookExportVersion {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Unsupported import version", nil)
return
}
// Make the importer the sole admin of the playbook.
playbook.Members = []app.PlaybookMember{
{
UserID: userID,
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
},
}
// Force the imported playbook to be public to avoid licencing issues
playbook.Public = true
if teamID != "" {
playbook.TeamID = teamID
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) {
return
}
if !h.validPlaybook(w, c.logger, &playbook) {
return
}
id, err := h.playbookService.Import(playbook, userID)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
result := struct {
ID string `json:"id"`
}{
ID: id,
}
w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id))
ReturnJSON(w, &result, http.StatusCreated)
}
func (h *PlaybookHandler) validateMetrics(pb app.Playbook) error {
if len(pb.Metrics) > app.MaxMetricsPerPlaybook {
return errors.Errorf(fmt.Sprintf("playbook cannot have more than %d key metrics", app.MaxMetricsPerPlaybook))
}
//check if titles are unique
titles := make(map[string]bool)
for _, m := range pb.Metrics {
if titles[m.Title] {
return errors.Errorf("metrics names must be unique")
}
titles[m.Title] = true
}
return nil
}
func (h *PlaybookHandler) getTopPlaybooksForUser(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
params := r.URL.Query()
timeRange := params.Get("time_range")
teamID := params.Get("team_id")
if teamID == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty"))
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
return
}
page, err := strconv.Atoi(params.Get("page"))
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err)
return
}
perPage, err := strconv.Atoi(params.Get("per_page"))
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err)
return
}
// setting startTime as per user's location
user, err := h.api.GetUserByID(userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err)
return
}
timezone, err := timeutils.GetUserTimezone(user)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user timezone", err)
return
}
if timezone == nil {
timezone = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone)
if appErr != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr)
return
}
topPlaybooks, err := h.playbookService.GetTopPlaybooksForUser(teamID, userID, &model.InsightsOpts{
StartUnixMilli: model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, &topPlaybooks, http.StatusOK)
}
func (h *PlaybookHandler) getTopPlaybooksForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := r.Header.Get("Mattermost-User-ID")
params := r.URL.Query()
timeRange := params.Get("time_range")
if teamID == "" {
h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty"))
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) {
return
}
page, err := strconv.Atoi(params.Get("page"))
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err)
return
}
perPage, err := strconv.Atoi(params.Get("per_page"))
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err)
return
}
// setting startTime as per user's location
user, err := h.api.GetUserByID(userID)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err)
return
}
timezone, err := timeutils.GetUserTimezone(user)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user timezone", err)
return
}
if timezone == nil {
timezone = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone)
if appErr != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr)
return
}
topPlaybooks, err := h.playbookService.GetTopPlaybooksForTeam(teamID, userID, &model.InsightsOpts{
StartUnixMilli: model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
h.HandleError(w, c.logger, err)
return
}
ReturnJSON(w, &topPlaybooks, http.StatusOK)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/playbooks/client"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// SettingsHandler is the API handler.
type SettingsHandler struct {
*ErrorHandler
api playbooks.ServicesAPI
config config.Service
}
// NewSettingsHandler returns a new settings api handler
func NewSettingsHandler(router *mux.Router, api playbooks.ServicesAPI, configService config.Service) *SettingsHandler {
handler := &SettingsHandler{
ErrorHandler: &ErrorHandler{},
api: api,
config: configService,
}
settingsRouter := router.PathPrefix("/settings").Subrouter()
settingsRouter.HandleFunc("", handler.getSettings).Methods(http.MethodGet)
return handler
}
func (h *SettingsHandler) getSettings(w http.ResponseWriter, r *http.Request) {
cfg := h.config.GetConfiguration()
settings := client.GlobalSettings{
EnableExperimentalFeatures: cfg.EnableExperimentalFeatures,
}
ReturnJSON(w, &settings, http.StatusOK)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type SignalHandler struct {
*ErrorHandler
api playbooks.ServicesAPI
playbookRunService app.PlaybookRunService
playbookService app.PlaybookService
keywordsThreadIgnorer app.KeywordsThreadIgnorer
}
func NewSignalHandler(router *mux.Router, api playbooks.ServicesAPI, playbookRunService app.PlaybookRunService, playbookService app.PlaybookService, keywordsThreadIgnorer app.KeywordsThreadIgnorer) *SignalHandler {
handler := &SignalHandler{
ErrorHandler: &ErrorHandler{},
api: api,
playbookRunService: playbookRunService,
playbookService: playbookService,
keywordsThreadIgnorer: keywordsThreadIgnorer,
}
signalRouter := router.PathPrefix("/signal").Subrouter()
keywordsRouter := signalRouter.PathPrefix("/keywords").Subrouter()
keywordsRouter.HandleFunc("/run-playbook", withContext(handler.playbookRun)).Methods(http.MethodPost)
keywordsRouter.HandleFunc("/ignore-thread", withContext(handler.ignoreKeywords)).Methods(http.MethodPost)
return handler
}
func (h *SignalHandler) playbookRun(c *Context, w http.ResponseWriter, r *http.Request) {
publicErrorMessage := "unable to decode post action integration request"
var req *model.PostActionIntegrationRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
h.returnError(publicErrorMessage, err, c.logger, w)
return
}
if req == nil {
h.returnError(publicErrorMessage, errors.New("nil request"), c.logger, w)
return
}
id, err := getStringField("selected_option", req.Context, w)
if err != nil {
h.returnError(publicErrorMessage, err, c.logger, w)
return
}
pbook, err := h.playbookService.Get(id)
if err != nil {
h.returnError("can't get chosen playbook", errors.Wrapf(err, "can't get chosen playbook, id - %s", id), c.logger, w)
return
}
if err := h.playbookRunService.OpenCreatePlaybookRunDialog(req.TeamId, req.UserId, req.TriggerId, "", "", []app.Playbook{pbook}); err != nil {
h.returnError("can't open dialog", errors.Wrap(err, "can't open a dialog"), c.logger, w)
return
}
ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK)
if _, err := h.api.DeletePost(req.PostId); err != nil {
h.returnError("unable to delete original post", err, c.logger, w)
return
}
}
func (h *SignalHandler) ignoreKeywords(c *Context, w http.ResponseWriter, r *http.Request) {
publicErrorMessage := "unable to decode post action integration request"
var req *model.PostActionIntegrationRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil || req == nil {
h.returnError(publicErrorMessage, err, c.logger, w)
return
}
postID, err := getStringField("postID", req.Context, w)
if err != nil {
h.returnError(publicErrorMessage, err, c.logger, w)
return
}
post, err := h.api.GetPost(postID)
if err != nil {
h.returnError(publicErrorMessage, err, c.logger, w)
return
}
h.keywordsThreadIgnorer.Ignore(postID, post.UserId)
if post.RootId != "" {
h.keywordsThreadIgnorer.Ignore(post.RootId, post.UserId)
}
ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK)
if _, err := h.api.DeletePost(req.PostId); err != nil {
h.returnError("unable to delete original post", err, c.logger, w)
return
}
}
func (h *SignalHandler) returnError(returnMessage string, err error, logger logrus.FieldLogger, w http.ResponseWriter) {
resp := model.PostActionIntegrationResponse{
EphemeralText: fmt.Sprintf("Error: %s", returnMessage),
}
logger.WithError(err).Warn(returnMessage)
ReturnJSON(w, &resp, http.StatusOK)
}
func getStringField(field string, context map[string]interface{}, w http.ResponseWriter) (string, error) {
fieldInt, ok := context[field]
if !ok {
return "", errors.Errorf("no %s field in the request context", field)
}
fieldValue, ok := fieldInt.(string)
if !ok {
return "", errors.Errorf("%s field is not a string", field)
}
return fieldValue, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"math"
"net/http"
"net/url"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"gopkg.in/guregu/null.v4"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/sqlstore"
"github.com/pkg/errors"
)
type StatsHandler struct {
*ErrorHandler
api playbooks.ServicesAPI
statsStore *sqlstore.StatsStore
playbookService app.PlaybookService
permissions *app.PermissionsService
licenseChecker app.LicenseChecker
}
func NewStatsHandler(router *mux.Router, api playbooks.ServicesAPI, statsStore *sqlstore.StatsStore, playbookService app.PlaybookService, permissions *app.PermissionsService, licenseChecker app.LicenseChecker) *StatsHandler {
handler := &StatsHandler{
ErrorHandler: &ErrorHandler{},
api: api,
statsStore: statsStore,
playbookService: playbookService,
permissions: permissions,
licenseChecker: licenseChecker,
}
statsRouter := router.PathPrefix("/stats").Subrouter()
statsRouter.HandleFunc("/site", withContext(handler.playbookSiteStats)).Methods(http.MethodGet)
statsRouter.HandleFunc("/playbook", withContext(handler.playbookStats)).Methods(http.MethodGet)
return handler
}
type PlaybookStats struct {
RunsInProgress int `json:"runs_in_progress"`
ParticipantsActive int `json:"participants_active"`
RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"`
RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"`
RunsStartedPerWeek []int `json:"runs_started_per_week"`
RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"`
ActiveRunsPerDay []int `json:"active_runs_per_day"`
ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"`
ActiveParticipantsPerDay []int `json:"active_participants_per_day"`
ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"`
MetricOverallAverage []null.Int `json:"metric_overall_average"`
MetricRollingAverage []null.Int `json:"metric_rolling_average"`
MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"`
MetricValueRange [][]int64 `json:"metric_value_range"`
MetricRollingValues [][]int64 `json:"metric_rolling_values"`
LastXRunNames []string `json:"last_x_run_names"`
}
const (
MetricChartPeriod = 10
MetricRollingAveragePeriod = 10
)
func parsePlaybookStatsFilters(u *url.URL) (*sqlstore.StatsFilters, error) {
playbookID := u.Query().Get("playbook_id")
if playbookID == "" {
return nil, errors.New("bad parameter 'playbook_id'; 'playbook_id' is required")
}
return &sqlstore.StatsFilters{
PlaybookID: playbookID,
}, nil
}
// playbookStats handles the internal plugin stats
func (h *StatsHandler) playbookStats(c *Context, w http.ResponseWriter, r *http.Request) {
if !h.licenseChecker.StatsAllowed() {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "timeline feature is not covered by current server license", nil)
return
}
userID := r.Header.Get("Mattermost-User-ID")
filters, err := parsePlaybookStatsFilters(r.URL)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad filters", err)
return
}
if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, filters.PlaybookID)) {
return
}
runsFinishedLast30Days := h.statsStore.RunsFinishedBetweenDays(filters, 30, 0)
runsFinishedBetween60and30DaysAgo := h.statsStore.RunsFinishedBetweenDays(filters, 60, 31)
var percentageChange int
if runsFinishedBetween60and30DaysAgo == 0 {
percentageChange = 99999999
} else {
percentageChange = int(math.Floor(float64((runsFinishedLast30Days-runsFinishedBetween60and30DaysAgo)/runsFinishedBetween60and30DaysAgo) * 100))
}
runsStartedPerWeek, runsStartedPerWeekTimes := h.statsStore.RunsStartedPerWeekLastXWeeks(12, filters)
activeRunsPerDay, activeRunsPerDayTimes := h.statsStore.ActiveRunsPerDayLastXDays(14, filters)
activeParticipantsPerDay, activeParticipantsPerDayTimes := h.statsStore.ActiveParticipantsPerDayLastXDays(14, filters)
metricOverallAverage := h.statsStore.MetricOverallAverage(*filters)
metricRollingValues, lastXRunNames := h.statsStore.MetricRollingValuesLastXRuns(MetricChartPeriod, 0, *filters)
metricRollingAverage, metricRollingAverageChange := h.statsStore.MetricRollingAverageAndChange(MetricRollingAveragePeriod, *filters)
metricValueRange := h.statsStore.MetricValueRange(*filters)
ReturnJSON(w, &PlaybookStats{
RunsInProgress: h.statsStore.TotalInProgressPlaybookRuns(filters),
ParticipantsActive: h.statsStore.TotalActiveParticipants(filters),
RunsFinishedPrev30Days: runsFinishedLast30Days,
RunsFinishedPercentageChange: percentageChange,
RunsStartedPerWeek: runsStartedPerWeek,
RunsStartedPerWeekTimes: runsStartedPerWeekTimes,
ActiveRunsPerDay: activeRunsPerDay,
ActiveRunsPerDayTimes: activeRunsPerDayTimes,
ActiveParticipantsPerDay: activeParticipantsPerDay,
ActiveParticipantsPerDayTimes: activeParticipantsPerDayTimes,
MetricOverallAverage: metricOverallAverage,
MetricRollingValues: metricRollingValues,
MetricValueRange: metricValueRange,
MetricRollingAverage: metricRollingAverage,
MetricRollingAverageChange: metricRollingAverageChange,
LastXRunNames: lastXRunNames,
}, http.StatusOK)
}
type PlaybookSiteStats struct {
TotalPlaybooks int `json:"total_playbooks"`
TotalPlaybookRuns int `json:"total_playbook_runs"`
}
// playbooSitekStats collects and sends the stats used for system-console > statistics
//
// Response 200: PlaybookSiteStats
// Response 401: when user is not authenticated
// Response 403: when user has no permissions to see stats
func (h *StatsHandler) playbookSiteStats(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
// user must have right to access analytics
if !h.api.HasPermissionTo(userID, model.PermissionGetAnalytics) {
h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "user is not allowed to get site stats", nil)
return
}
totalPlaybooks, err := h.statsStore.TotalPlaybooks()
if err != nil {
c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbooks")
}
totalRuns, err := h.statsStore.TotalPlaybookRuns()
if err != nil {
c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbook runs")
}
ReturnJSON(w, &PlaybookSiteStats{
TotalPlaybooks: totalPlaybooks,
TotalPlaybookRuns: totalRuns,
}, http.StatusOK)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// TelemetryHandler is the API handler.
type TelemetryHandler struct {
*ErrorHandler
playbookRunService app.PlaybookRunService
playbookRunTelemetry app.PlaybookRunTelemetry
playbookService app.PlaybookService
permissions *app.PermissionsService
playbookTelemetry app.PlaybookTelemetry
genericTelemetry app.GenericTelemetry
botTelemetry bot.Telemetry
api playbooks.ServicesAPI
}
// NewTelemetryHandler Creates a new Plugin API handler.
func NewTelemetryHandler(
router *mux.Router,
playbookRunService app.PlaybookRunService,
api playbooks.ServicesAPI,
playbookRunTelemetry app.PlaybookRunTelemetry,
playbookService app.PlaybookService,
playbookTelemetry app.PlaybookTelemetry,
genericTelemetry app.GenericTelemetry,
botTelemetry bot.Telemetry,
permissions *app.PermissionsService,
) *TelemetryHandler {
handler := &TelemetryHandler{
ErrorHandler: &ErrorHandler{},
playbookRunService: playbookRunService,
playbookRunTelemetry: playbookRunTelemetry,
playbookService: playbookService,
playbookTelemetry: playbookTelemetry,
genericTelemetry: genericTelemetry,
botTelemetry: botTelemetry,
api: api,
permissions: permissions,
}
telemetryRouter := router.PathPrefix("/telemetry").Subrouter()
telemetryRouter.HandleFunc("", withContext(handler.createEvent)).Methods(http.MethodPost)
startTrialRouter := telemetryRouter.PathPrefix("/start-trial").Subrouter()
startTrialRouter.HandleFunc("", withContext(handler.startTrial)).Methods(http.MethodPost)
playbookRunTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/run").Subrouter()
playbookRunTelemetryRouterAuthorized.Use(handler.checkPlaybookRunViewPermissions)
playbookRunTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybookRun)).Methods(http.MethodPost)
playbookTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/playbook").Subrouter()
playbookTelemetryRouterAuthorized.Use(handler.checkPlaybookViewPermissions)
playbookTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybook)).Methods(http.MethodPost)
templateRouter := telemetryRouter.PathPrefix("/template").Subrouter()
templateRouter.HandleFunc("", withContext(handler.telemetryForTemplate))
return handler
}
type EventData struct {
Name string
Type app.TelemetryType
Properties map[string]interface{}
}
func (h *TelemetryHandler) createEvent(c *Context, w http.ResponseWriter, r *http.Request) {
var event EventData
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
return
}
if event.Properties == nil {
event.Properties = map[string]interface{}{}
}
event.Properties["UserActualID"] = r.Header.Get("Mattermost-User-ID")
switch event.Type {
case app.TelemetryTypePage:
name, err := app.NewTelemetryPage(event.Name)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid page tracking", err)
return
}
h.genericTelemetry.Page(*name, event.Properties)
case app.TelemetryTypeTrack:
name, err := app.NewTelemetryTrack(event.Name)
if err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid event tracking", err)
return
}
h.genericTelemetry.Track(*name, event.Properties)
default:
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid type to be tracked", nil)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *TelemetryHandler) checkPlaybookRunViewPermissions(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := r.Header.Get("Mattermost-User-ID")
runID := vars["id"]
if err := h.permissions.RunView(userID, runID); err != nil {
logger := getLogger(r)
if errors.Is(err, app.ErrNoPermissions) {
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err)
return
}
h.HandleError(w, logger, err)
return
}
next.ServeHTTP(w, r)
})
}
func (h *TelemetryHandler) checkPlaybookViewPermissions(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := r.Header.Get("Mattermost-User-ID")
playbookID := vars["id"]
if err := h.permissions.PlaybookView(userID, playbookID); err != nil {
logger := getLogger(r)
if errors.Is(err, app.ErrNoPermissions) {
h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err)
return
}
h.HandleError(w, logger, err)
return
}
next.ServeHTTP(w, r)
})
}
type TrackerPayload struct {
Action string `json:"action"`
}
// telemetryForPlaybookRun handles the /telemetry/run/{id}?action=the_action endpoint. The frontend
// can use this endpoint to track events that occur in the context of a playbook run.
func (h *TelemetryHandler) telemetryForPlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var params TrackerPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
return
}
if params.Action == "" {
h.HandleError(w, c.logger, errors.New("must provide action"))
return
}
playbookRun, err := h.playbookRunService.GetPlaybookRun(id)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
h.playbookRunTelemetry.FrontendTelemetryForPlaybookRun(playbookRun, userID, params.Action)
w.WriteHeader(http.StatusNoContent)
}
func (h *TelemetryHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var params TrackerPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
return
}
h.botTelemetry.StartTrial(userID, params.Action)
w.WriteHeader(http.StatusNoContent)
}
// telemetryForPlaybook handles the /telemetry/playbook/{id}?action=the_action endpoint. The frontend
// can use this endpoint to track events that occur in the context of a playbook.
func (h *TelemetryHandler) telemetryForPlaybook(c *Context, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID := r.Header.Get("Mattermost-User-ID")
var params TrackerPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
return
}
if params.Action == "" {
h.HandleError(w, c.logger, errors.New("must provide action"))
return
}
playbook, err := h.playbookService.Get(id)
if err != nil {
h.HandleError(w, c.logger, err)
return
}
h.playbookTelemetry.FrontendTelemetryForPlaybook(playbook, userID, params.Action)
w.WriteHeader(http.StatusNoContent)
}
func (h *TelemetryHandler) telemetryForTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var params struct {
TemplateName string `json:"template_name"`
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err)
return
}
if params.TemplateName == "" {
h.HandleError(w, c.logger, errors.New("must provide template_name"))
return
}
if params.Action == "" {
h.HandleError(w, c.logger, errors.New("must provide action"))
return
}
h.playbookTelemetry.FrontendTelemetryForPlaybookTemplate(params.TemplateName, userID, params.Action)
w.WriteHeader(http.StatusNoContent)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"fmt"
"net/url"
"path"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const defaultBaseAPIURL = "plugins/playbooks/api/v0"
func getAPIBaseURL(api playbooks.ServicesAPI) (string, error) {
siteURL := model.ServiceSettingsDefaultSiteURL
if api.GetConfig().ServiceSettings.SiteURL != nil {
siteURL = *api.GetConfig().ServiceSettings.SiteURL
}
parsedSiteURL, err := url.Parse(siteURL)
if err != nil {
return "", errors.Wrapf(err, "failed to parse siteURL %s", siteURL)
}
return path.Join(parsedSiteURL.Path, defaultBaseAPIURL), nil
}
func makeAPIURL(api playbooks.ServicesAPI, apiPath string, args ...interface{}) string {
apiBaseURL, err := getAPIBaseURL(api)
if err != nil {
logrus.WithError(err).Error("failed to build api base url")
apiBaseURL = defaultBaseAPIURL
}
return path.Join("/", apiBaseURL, fmt.Sprintf(apiPath, args...))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type PlaybookGetter interface {
Get(id string) (Playbook, error)
}
type channelActionServiceImpl struct {
poster bot.Poster
configService config.Service
store ChannelActionStore
api playbooks.ServicesAPI
playbookGetter PlaybookGetter
keywordsThreadIgnorer KeywordsThreadIgnorer
telemetry ChannelActionTelemetry
}
func NewChannelActionsService(api playbooks.ServicesAPI, poster bot.Poster, configService config.Service, store ChannelActionStore, playbookGetter PlaybookGetter, keywordsThreadIgnorer KeywordsThreadIgnorer, telemetry ChannelActionTelemetry) ChannelActionService {
return &channelActionServiceImpl{
poster: poster,
configService: configService,
store: store,
api: api,
playbookGetter: playbookGetter,
keywordsThreadIgnorer: keywordsThreadIgnorer,
telemetry: telemetry,
}
}
// setViewedChannelForEveryMember mark channelID as viewed for all its existing members
func (a *channelActionServiceImpl) setViewedChannelForEveryMember(channelID string) error {
// TODO: this is a magic number, we should load test this function to find a
// good threshold to share the workload between the goroutines
perPage := 200
page := 0
var wg sync.WaitGroup
var goroutineErr error
for {
members, err := a.api.GetChannelMembers(channelID, page, perPage)
if err != nil {
return fmt.Errorf("unable to retrieve members of channel with ID %q", channelID)
}
if len(members) == 0 {
break
}
wg.Add(1)
go func() {
defer wg.Done()
userIDs := make([]string, 0, len(members))
for _, member := range members {
userIDs = append(userIDs, member.UserId)
}
if err := a.store.SetMultipleViewedChannel(userIDs, channelID); err != nil {
// We don't care whether multiple goroutines assign this value, as we're
// only interested in knowing if there was at least one error
goroutineErr = errors.Wrapf(err, "unable to mark channel with ID %q as viewed for users %v", channelID, userIDs)
}
}()
page++
}
wg.Wait()
return goroutineErr
}
func (a *channelActionServiceImpl) Create(action GenericChannelAction) (string, error) {
actions, err := a.store.GetChannelActions(action.ChannelID, GetChannelActionOptions{
ActionType: action.ActionType,
TriggerType: action.TriggerType,
})
if err != nil {
return "", err
}
if len(actions) > 0 {
return "", fmt.Errorf("only one action of action type %q and trigger type %q is allowed", string(action.ActionType), string(action.TriggerType))
}
if action.ActionType == ActionTypeWelcomeMessage && action.Enabled {
if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil {
return "", err
}
}
return a.store.Create(action)
}
func (a *channelActionServiceImpl) Get(id string) (GenericChannelAction, error) {
return a.store.Get(id)
}
func (a *channelActionServiceImpl) GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error) {
return a.store.GetChannelActions(channelID, options)
}
func (a *channelActionServiceImpl) Validate(action GenericChannelAction) error {
// Validate the trigger type and action types
switch action.TriggerType {
case TriggerTypeNewMemberJoins:
switch action.ActionType {
case ActionTypeWelcomeMessage:
break
case ActionTypeCategorizeChannel:
break
default:
return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType)
}
case TriggerTypeKeywordsPosted:
if action.ActionType != ActionTypePromptRunPlaybook {
return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType)
}
default:
return fmt.Errorf("trigger type %q not recognized", action.TriggerType)
}
// Validate the payload depending on the action type
switch action.ActionType {
case ActionTypeWelcomeMessage:
var payload WelcomeMessagePayload
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
return fmt.Errorf("unable to decode payload from action")
}
case ActionTypePromptRunPlaybook:
var payload PromptRunPlaybookFromKeywordsPayload
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
return fmt.Errorf("unable to decode payload from action")
}
if err := checkValidPromptRunPlaybookFromKeywordsPayload(payload); err != nil {
return err
}
case ActionTypeCategorizeChannel:
var payload CategorizeChannelPayload
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
return fmt.Errorf("unable to decode payload from action")
}
default:
return fmt.Errorf("action type %q not recognized", action.ActionType)
}
return nil
}
func checkValidPromptRunPlaybookFromKeywordsPayload(payload PromptRunPlaybookFromKeywordsPayload) error {
for _, keyword := range payload.Keywords {
if keyword == "" {
return fmt.Errorf("payload field 'keywords' must contain only non-empty keywords")
}
}
if payload.PlaybookID != "" && !model.IsValidId(payload.PlaybookID) {
return fmt.Errorf("payload field 'playbook_id' must be a valid ID")
}
return nil
}
func (a *channelActionServiceImpl) Update(action GenericChannelAction, userID string) error {
oldAction, err := a.Get(action.ID)
if err != nil {
return fmt.Errorf("unable to retrieve existing action with ID %q", action.ID)
}
if action.ActionType == ActionTypeWelcomeMessage && !oldAction.Enabled && action.Enabled {
if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil {
return err
}
}
if err := a.store.Update(action); err != nil {
return err
}
a.telemetry.UpdateChannelAction(action, userID)
return nil
}
// UserHasJoinedChannel is called when userID has joined channelID. If actorID is not blank, userID
// was invited by actorID.
func (a *channelActionServiceImpl) UserHasJoinedChannel(userID, channelID, actorID string) {
user, err := a.api.GetUserByID(userID)
if err != nil {
logrus.WithError(err).WithField("user_id", userID).Error("failed to resolve user")
return
}
channel, err := a.api.GetChannelByID(channelID)
if err != nil {
logrus.WithError(err).WithField("channel_id", channelID).Error("failed to resolve channel")
return
}
if user.IsBot {
return
}
actions, err := a.GetChannelActions(channelID, GetChannelActionOptions{
ActionType: ActionTypeCategorizeChannel,
TriggerType: TriggerTypeNewMemberJoins,
})
if err != nil {
logrus.WithError(err).WithField("channel_id", channelID).Error("failed to get the channel actions")
return
}
if len(actions) > 1 {
logrus.WithFields(logrus.Fields{
"action_type": ActionTypeCategorizeChannel,
"trigger_type": TriggerTypeNewMemberJoins,
"num_actions": len(actions),
}).Error("expected only one action to be retrieved")
}
if len(actions) != 1 {
return
}
action := actions[0]
if !action.Enabled {
return
}
var payload CategorizeChannelPayload
if err = mapstructure.Decode(action.Payload, &payload); err != nil {
logrus.WithError(err).Error("unable to decode payload of CategorizeChannelPayload")
return
}
if payload.CategoryName != "" {
// Update sidebar category in the go-routine not to block the UserHasJoinedChannel hook
go func() {
// Wait for 5 seconds(a magic number) for the webapp to get the `user_added` event,
// finish channel categorization and update it's state in redux.
// Currently there is no way to detect when webapp finishes the job.
// After that we can update the categories safely.
// Technically if user starts multiple runs simultaneously we will still get the race condition
// on category update. Since that's not realistic at the moment we are not adding the
// distributed lock here.
time.Sleep(5 * time.Second)
err = a.createOrUpdatePlaybookRunSidebarCategory(userID, channelID, channel.TeamId, payload.CategoryName)
if err != nil {
logrus.WithError(err).Error("failed to categorize channel")
}
a.telemetry.RunChannelAction(action, userID)
}()
}
}
// createOrUpdatePlaybookRunSidebarCategory creates or updates a "Playbook Runs" sidebar category if
// it does not already exist and adds the channel within the sidebar category
func (a *channelActionServiceImpl) createOrUpdatePlaybookRunSidebarCategory(userID, channelID, teamID, categoryName string) error {
sidebar, err := a.api.GetChannelSidebarCategories(userID, teamID)
if err != nil {
return err
}
var categoryID string
for _, category := range sidebar.Categories {
if strings.EqualFold(category.DisplayName, categoryName) {
categoryID = category.Id
if !sliceContains(category.Channels, channelID) {
category.Channels = append(category.Channels, channelID)
}
break
}
}
if categoryID == "" {
_, err = a.api.CreateChannelSidebarCategory(userID, teamID, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userID,
TeamId: teamID,
DisplayName: categoryName,
Muted: false,
},
Channels: []string{channelID},
})
if err != nil {
return err
}
return nil
}
// remove channel from previous category
for _, category := range sidebar.Categories {
if strings.EqualFold(category.DisplayName, categoryName) {
continue
}
for i, channel := range category.Channels {
if channel == channelID {
category.Channels = append(category.Channels[:i], category.Channels[i+1:]...)
break
}
}
}
_, err = a.api.UpdateChannelSidebarCategories(userID, teamID, sidebar.Categories)
if err != nil {
return err
}
return nil
}
func sliceContains(strs []string, target string) bool {
for _, s := range strs {
if s == target {
return true
}
}
return false
}
// CheckAndSendMessageOnJoin checks if userID has viewed channelID and sends
// playbookRun.MessageOnJoin if it exists. Returns true if the message was sent.
func (a *channelActionServiceImpl) CheckAndSendMessageOnJoin(userID, channelID string) bool {
hasViewed := a.store.HasViewedChannel(userID, channelID)
if hasViewed {
return true
}
actions, err := a.store.GetChannelActions(channelID, GetChannelActionOptions{
TriggerType: TriggerTypeNewMemberJoins,
})
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"channel_id": channelID,
"trigger_type": TriggerTypeNewMemberJoins,
}).Error("failed to resolve actions")
return false
}
if err = a.store.SetViewedChannel(userID, channelID); err != nil {
// If duplicate entry, userID has viewed channelID. If not a duplicate, assume they haven't.
return errors.Is(err, ErrDuplicateEntry)
}
// Look for the ActionTypeWelcomeMessage action
for _, action := range actions {
if action.ActionType == ActionTypeWelcomeMessage {
var payload WelcomeMessagePayload
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
logrus.WithError(err).WithField("action_type", action.ActionType).Error("payload of action is not valid")
}
// Run the action
a.poster.SystemEphemeralPost(userID, channelID, &model.Post{
Message: payload.Message,
})
a.telemetry.RunChannelAction(action, userID)
}
}
return true
}
func (a *channelActionServiceImpl) MessageHasBeenPosted(post *model.Post) {
if post.IsSystemMessage() || a.keywordsThreadIgnorer.IsIgnored(post.RootId, post.UserId) || a.poster.IsFromPoster(post) {
return
}
actions, err := a.GetChannelActions(post.ChannelId, GetChannelActionOptions{
TriggerType: TriggerTypeKeywordsPosted,
ActionType: ActionTypePromptRunPlaybook,
})
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"channel_id": post.ChannelId,
"trigger_type": TriggerTypeKeywordsPosted,
}).Error("unable to retrieve channel actions")
return
}
// Finish early if there are no actions to prompt running a playbook
if len(actions) == 0 {
return
}
triggeredPlaybooksMap := make(map[string]Playbook)
presentTriggers := []string{}
for _, action := range actions {
if !action.Enabled {
continue
}
var payload PromptRunPlaybookFromKeywordsPayload
if err := mapstructure.Decode(action.Payload, &payload); err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"payload": payload,
"actionType": action.ActionType,
"triggerType": action.TriggerType,
}).Error("unable to decode payload from action")
continue
}
if len(payload.Keywords) == 0 || payload.PlaybookID == "" {
continue
}
suggestedPlaybook, err := a.playbookGetter.Get(payload.PlaybookID)
if err != nil {
logrus.WithError(err).WithField("playbook_id", payload.PlaybookID).Error("unable to get playbook to run action")
continue
}
triggers := payload.Keywords
actionExecuted := false
for _, trigger := range triggers {
if strings.Contains(post.Message, trigger) || containsAttachments(post.Attachments(), trigger) {
triggeredPlaybooksMap[payload.PlaybookID] = suggestedPlaybook
presentTriggers = append(presentTriggers, trigger)
actionExecuted = true
}
}
if actionExecuted {
a.telemetry.RunChannelAction(action, post.UserId)
}
}
if len(triggeredPlaybooksMap) == 0 {
return
}
triggeredPlaybooks := []Playbook{}
for _, playbook := range triggeredPlaybooksMap {
triggeredPlaybooks = append(triggeredPlaybooks, playbook)
}
message := getPlaybookSuggestionsMessage(triggeredPlaybooks, presentTriggers)
attachment := getPlaybookSuggestionsSlackAttachment(triggeredPlaybooks, post.Id, "playbooks")
rootID := post.RootId
if rootID == "" {
rootID = post.Id
}
newPost := &model.Post{
Message: message,
ChannelId: post.ChannelId,
}
model.ParseSlackAttachment(newPost, []*model.SlackAttachment{attachment})
if err := a.poster.PostMessageToThread(rootID, newPost); err != nil {
logrus.WithError(err).Error("unable to post message with suggestions to run playbooks")
}
}
func getPlaybookSuggestionsMessage(suggestedPlaybooks []Playbook, triggers []string) string {
message := ""
triggerMessage := ""
if len(triggers) == 1 {
triggerMessage = fmt.Sprintf("`%s` is a trigger", triggers[0])
} else {
triggerMessage = fmt.Sprintf("`%s` are triggers", strings.Join(triggers, "`, `"))
}
if len(suggestedPlaybooks) == 1 {
playbookURL := fmt.Sprintf("[%s](%s)", suggestedPlaybooks[0].Title, GetPlaybookDetailsRelativeURL(suggestedPlaybooks[0].ID))
message = fmt.Sprintf("%s for the %s playbook, would you like to run it?", triggerMessage, playbookURL)
} else {
message = fmt.Sprintf("%s for the multiple playbooks, would you like to run one of them?", triggerMessage)
}
return message
}
func getPlaybookSuggestionsSlackAttachment(playbooks []Playbook, triggeringPostID string, pluginID string) *model.SlackAttachment {
ignoreButton := &model.PostAction{
Id: "ignoreKeywordsButton",
Name: "No, ignore thread",
Type: model.PostActionTypeButton,
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/ignore-thread", pluginID),
Context: map[string]interface{}{
"postID": triggeringPostID,
},
},
}
if len(playbooks) == 1 {
yesButton := &model.PostAction{
Id: "runPlaybookButton",
Name: "Yes, run playbook",
Type: model.PostActionTypeButton,
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID),
Context: map[string]interface{}{
"postID": triggeringPostID,
"selected_option": playbooks[0].ID,
},
},
Style: "primary",
}
attachment := &model.SlackAttachment{
Actions: []*model.PostAction{yesButton, ignoreButton},
Text: "Open Channel Actions in the channel header to view and edit keywords.",
}
return attachment
}
options := []*model.PostActionOptions{}
for _, playbook := range playbooks {
option := &model.PostActionOptions{
Value: playbook.ID,
Text: playbook.Title,
}
options = append(options, option)
}
playbookChooser := &model.PostAction{
Id: "playbookChooser",
Name: "Select a playbook to run",
Type: model.PostActionTypeSelect,
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID),
Context: map[string]interface{}{
"postID": triggeringPostID,
},
},
Options: options,
Style: "primary",
}
attachment := &model.SlackAttachment{
Actions: []*model.PostAction{playbookChooser, ignoreButton},
}
return attachment
}
func containsAttachments(attachments []*model.SlackAttachment, trigger string) bool {
// Check PreText, Title, Text and Footer SlackAttachments fields for trigger.
for _, attachment := range attachments {
switch {
case strings.Contains(attachment.Pretext, trigger):
return true
case strings.Contains(attachment.Title, trigger):
return true
case strings.Contains(attachment.Text, trigger):
return true
case strings.Contains(attachment.Footer, trigger):
return true
default:
continue
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"strings"
)
type CategoryItemType string
const (
PlaybookItemType CategoryItemType = "p"
RunItemType CategoryItemType = "r"
)
func StringToItemType(item string) (CategoryItemType, error) {
var convertedItem CategoryItemType
if item == string(PlaybookItemType) {
convertedItem = PlaybookItemType
} else if item == string(RunItemType) {
convertedItem = RunItemType
} else {
return PlaybookItemType, errors.New("unknown item type")
}
return convertedItem, nil
}
type CategoryItem struct {
ItemID string `json:"item_id"`
Type CategoryItemType `json:"type"`
Name string `json:"name"`
Public bool `json:"public"`
}
// Category represents sidebar category with items
type Category struct {
ID string `json:"id"`
Name string `json:"name"`
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
Collapsed bool `json:"collapsed"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Items []CategoryItem `json:"items"`
}
func (c *Category) IsValid() error {
if strings.TrimSpace(c.ID) == "" {
return errors.New("category ID cannot be empty")
}
if strings.TrimSpace(c.Name) == "" {
return errors.New("category name cannot be empty")
}
if strings.TrimSpace(c.UserID) == "" {
return errors.New("category user ID cannot be empty")
}
if strings.TrimSpace(c.TeamID) == "" {
return errors.New("category team id ID cannot be empty")
}
for _, item := range c.Items {
if item.ItemID == "" {
return errors.New("item ID cannot be empty")
}
if item.Type != PlaybookItemType && item.Type != RunItemType {
return errors.New("item type is incorrect")
}
}
return nil
}
func (c *Category) ContainsItem(item CategoryItem) bool {
for _, catItem := range c.Items {
if catItem.ItemID == item.ItemID && catItem.Type == item.Type {
return true
}
}
return false
}
// CategoryService is the category service for managing categories
type CategoryService interface {
// Create creates a new Category
Create(category Category) (string, error)
// Get retrieves category with categoryID for user for team
Get(categoryID string) (Category, error)
// GetCategories retrieves all categories for user for team
GetCategories(teamID, userID string) ([]Category, error)
// Update updates a category
Update(category Category) error
// Delete deletes a category
Delete(categoryID string) error
// AddFavorite favorites an item, which may be either run or playbook
AddFavorite(item CategoryItem, teamID, userID string) error
// DeleteFavorite unfavorites an item, which may be either run or playbook
DeleteFavorite(item CategoryItem, teamID, userID string) error
// IsItemFavorite returns whether item was favorited or not
IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error)
AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error)
}
type CategoryStore interface {
// Get retrieves a Category. Returns ErrNotFound if not found.
Get(id string) (Category, error)
// Create creates a new Category
Create(category Category) error
// GetCategories retrieves all categories for user for team
GetCategories(teamID, userID string) ([]Category, error)
// Update updates a category
Update(category Category) error
// Delete deletes a category
Delete(categoryID string) error
// GetFavoriteCategory returns favorite category
GetFavoriteCategory(teamID, userID string) (Category, error)
// AddItemToFavoriteCategory adds an item to favorite category,
// if favorite category does not exist it creates one
AddItemToFavoriteCategory(item CategoryItem, teamID, userID string) error
// AddItemToCategory adds an item to category
AddItemToCategory(item CategoryItem, categoryID string) error
// DeleteItemFromCategory adds an item to category
DeleteItemFromCategory(item CategoryItem, categoryID string) error
}
type CategoryTelemetry interface {
// FavoriteItem tracks run favoriting of an item. Item can be run or a playbook
FavoriteItem(item CategoryItem, userID string)
// UnfavoriteItem tracks run unfavoriting of an item. Item can be run or a playbook
UnfavoriteItem(item CategoryItem, userID string)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
)
type categoryService struct {
store CategoryStore
api playbooks.ServicesAPI
telemetry CategoryTelemetry
}
// NewPlaybookService returns a new playbook service
func NewCategoryService(store CategoryStore, api playbooks.ServicesAPI, categoryTelemetry CategoryTelemetry) CategoryService {
return &categoryService{
store: store,
api: api,
telemetry: categoryTelemetry,
}
}
// Create creates a new Category
func (c *categoryService) Create(category Category) (string, error) {
if category.ID != "" {
return "", errors.New("ID should be empty")
}
category.ID = model.NewId()
category.CreateAt = model.GetMillis()
category.UpdateAt = category.CreateAt
if err := category.IsValid(); err != nil {
return "", errors.Wrap(err, "invalid category")
}
if err := c.store.Create(category); err != nil {
return "", errors.Wrap(err, "Can't create category")
}
return category.ID, nil
}
func (c *categoryService) Get(categoryID string) (Category, error) {
category, err := c.store.Get(categoryID)
if err != nil {
return Category{}, errors.Wrap(err, "Can't get category")
}
return category, nil
}
// GetCategories retrieves all categories for user for team
func (c *categoryService) GetCategories(teamID, userID string) ([]Category, error) {
if !model.IsValidId(teamID) {
return nil, errors.New("teamID is not valid")
}
if !model.IsValidId(userID) {
return nil, errors.New("userID is not valid")
}
return c.store.GetCategories(teamID, userID)
}
// Update updates a category
func (c *categoryService) Update(category Category) error {
if category.ID == "" {
return errors.New("id should not be empty")
}
if category.Name == "" {
return errors.New("name should not be empty")
}
category.UpdateAt = model.GetMillis()
if err := category.IsValid(); err != nil {
return errors.Wrap(err, "invalid category")
}
if err := c.store.Update(category); err != nil {
return errors.Wrap(err, "can't update category")
}
return nil
}
// Delete deletes a category
func (c *categoryService) Delete(categoryID string) error {
if err := c.store.Delete(categoryID); err != nil {
return errors.Wrap(err, "can't delete category")
}
return nil
}
// AddFavorite favorites an item, which may be either run or playbook
func (c *categoryService) AddFavorite(item CategoryItem, teamID, userID string) error {
if err := c.store.AddItemToFavoriteCategory(item, teamID, userID); err != nil {
return errors.Wrap(err, "failed to add favorite")
}
c.telemetry.FavoriteItem(item, userID)
return nil
}
func (c *categoryService) DeleteFavorite(item CategoryItem, teamID, userID string) error {
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
if err != nil {
return errors.Wrap(err, "can't get favorite category")
}
found := false
for _, favItem := range favoriteCategory.Items {
if favItem.ItemID == item.ItemID && favItem.Type == item.Type {
found = true
}
}
if !found {
return errors.New("Item is not favorited")
}
if err := c.store.DeleteItemFromCategory(item, favoriteCategory.ID); err != nil {
return errors.Wrap(err, "can't delete item from favorite category")
}
c.telemetry.UnfavoriteItem(item, userID)
return nil
}
func (c *categoryService) IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error) {
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
if err == sql.ErrNoRows {
return false, nil
} else if err != nil {
return false, errors.Wrap(err, "can't get favorite category")
}
found := false
for _, favItem := range favoriteCategory.Items {
if favItem.ItemID == item.ItemID && favItem.Type == item.Type {
found = true
}
}
return found, nil
}
func (c *categoryService) AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error) {
result := make([]bool, len(items))
favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID)
if err == sql.ErrNoRows {
return result, nil
} else if err != nil {
return result, errors.Wrap(err, "can't get favorite category")
}
categoryResult := make(map[CategoryItem]bool)
for _, favItem := range favoriteCategory.Items {
categoryResult[CategoryItem{
ItemID: favItem.ItemID,
Type: favItem.Type,
}] = true
}
for i, item := range items {
result[i] = categoryResult[item]
}
return result, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"reflect"
)
const CurrentPlaybookExportVersion = 1
func getFieldsForExport(in interface{}) map[string]interface{} {
out := map[string]interface{}{}
inType := reflect.TypeOf(in)
inValue := reflect.ValueOf(in)
for i := 0; i < inType.NumField(); i++ {
field := inType.Field(i)
tag := field.Tag.Get("export")
fieldValue := inValue.Field(i)
if tag != "" && tag != "-" && !fieldValue.IsZero() {
out[tag] = fieldValue.Interface()
}
}
return out
}
func generateChecklistItemExport(checklistItems []ChecklistItem) []interface{} {
exported := make([]interface{}, 0, len(checklistItems))
for _, item := range checklistItems {
exportItem := getFieldsForExport(item)
exported = append(exported, exportItem)
}
return exported
}
func generateChecklistExport(checklists []Checklist) []interface{} {
exported := make([]interface{}, 0, len(checklists))
for _, checklist := range checklists {
exportList := getFieldsForExport(checklist)
exportList["items"] = generateChecklistItemExport(checklist.Items)
exported = append(exported, exportList)
}
return exported
}
func generateMetricsExport(metrics []PlaybookMetricConfig) []interface{} {
exported := make([]interface{}, 0, len(metrics))
for _, checklist := range metrics {
exportList := getFieldsForExport(checklist)
exported = append(exported, exportList)
}
return exported
}
// GeneratePlaybookExport returns a playbook in export format.
// Fields marked with the stuct tag "export" are included using the given string.
func GeneratePlaybookExport(playbook Playbook) ([]byte, error) {
export := getFieldsForExport(playbook)
export["version"] = CurrentPlaybookExportVersion
export["checklists"] = generateChecklistExport(playbook.Checklists)
export["metrics"] = generateMetricsExport(playbook.Metrics)
result, err := json.MarshalIndent(export, "", " ")
if err != nil {
return nil, err
}
return result, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "sync"
type KeywordsThreadIgnorer interface {
Ignore(postID, userID string)
IsIgnored(postID, userID string) bool
}
type keywordsThreadIgnorerImpl struct {
ignoredThreads map[string]map[string]bool // [postID][userID]
mutex sync.RWMutex
}
func NewKeywordsThreadIgnorer() KeywordsThreadIgnorer {
return &keywordsThreadIgnorerImpl{
ignoredThreads: map[string]map[string]bool{},
mutex: sync.RWMutex{},
}
}
// Ignores ignores thread postID for the userID,
// other users will still get notifications in this thread
func (i *keywordsThreadIgnorerImpl) Ignore(postID, userID string) {
i.mutex.Lock()
defer i.mutex.Unlock()
if _, ok := i.ignoredThreads[postID]; !ok {
i.ignoredThreads[postID] = map[string]bool{}
}
i.ignoredThreads[postID][userID] = true
}
// IsIgnored checks whether this thread should be ignored for userID
func (i *keywordsThreadIgnorerImpl) IsIgnored(postID, userID string) bool {
i.mutex.RLock()
defer i.mutex.RUnlock()
if _, ok := i.ignoredThreads[postID]; !ok {
return false
}
return i.ignoredThreads[postID][userID]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ErrNoPermissions if the error is caused by the user not having permissions
var ErrNoPermissions = errors.New("does not have permissions")
// ErrLicensedFeature if the error is caused by the server not having the needed license for the feature
var ErrLicensedFeature = errors.New("not covered by current server license")
type LicenseChecker interface {
PlaybookAllowed(isPlaybookPublic bool) bool
RetrospectiveAllowed() bool
TimelineAllowed() bool
StatsAllowed() bool
ChecklistItemDueDateAllowed() bool
}
type PermissionsService struct {
playbookService PlaybookService
runService PlaybookRunService
api playbooks.ServicesAPI
configService config.Service
licenseChecker LicenseChecker
}
func NewPermissionsService(
playbookService PlaybookService,
runService PlaybookRunService,
api playbooks.ServicesAPI,
configService config.Service,
licenseChecker LicenseChecker,
) *PermissionsService {
return &PermissionsService{
playbookService,
runService,
api,
configService,
licenseChecker,
}
}
func (p *PermissionsService) PlaybookIsPublic(playbook Playbook) bool {
return playbook.Public
}
func (p *PermissionsService) getPlaybookRole(userID string, playbook Playbook) []string {
if !p.canViewTeam(userID, playbook.TeamID) {
return []string{}
}
for _, member := range playbook.Members {
if member.UserID == userID {
return member.SchemeRoles
}
}
// Public playbooks
if playbook.Public {
if playbook.DefaultPlaybookMemberRole == "" {
return []string{playbook.DefaultPlaybookMemberRole}
}
return []string{PlaybookRoleMember}
}
return []string{}
}
func (p *PermissionsService) hasPermissionsToPlaybook(userID string, playbook Playbook, permission *model.Permission) bool {
// Check at playbook level
if p.api.RolesGrantPermission(p.getPlaybookRole(userID, playbook), permission.Id) {
return true
}
// Cascade normally to higher level permissions
return p.api.HasPermissionToTeam(userID, playbook.TeamID, permission)
}
func (p *PermissionsService) HasPermissionsToRun(userID string, run *PlaybookRun, permission *model.Permission) bool {
// Check at run level
if err := p.runManagePropertiesWithPlaybookRun(userID, run); err != nil {
return false
}
// Cascade normally to higher level permissions
return p.api.HasPermissionToTeam(userID, run.TeamID, permission)
}
func (p *PermissionsService) canViewTeam(userID string, teamID string) bool {
if teamID == "" || userID == "" {
return false
}
return p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam)
}
func (p *PermissionsService) PlaybookCreate(userID string, playbook Playbook) error {
if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(playbook)) {
return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license")
}
// Check the user has permissions over all broadcast channels
for _, channelID := range playbook.BroadcastChannelIDs {
if !p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
return errors.Errorf("user `%s` does not have permission to create posts in channel `%s`", userID, channelID)
}
}
// Check all invited users have permissions to the team.
for _, userID := range playbook.InvitedUserIDs {
if !p.api.HasPermissionToTeam(userID, playbook.TeamID, model.PermissionViewTeam) {
return errors.Errorf(
"invited user `%s` does not have permission to playbook's team `%s`",
userID,
playbook.TeamID,
)
}
}
// Respect setting for not allowing mentions of a group.
for _, groupID := range playbook.InvitedGroupIDs {
group, err := p.api.GetGroup(groupID)
if err != nil {
return errors.Wrap(err, "invalid group")
}
if !group.AllowReference {
return errors.Errorf(
"group `%s` does not allow references",
groupID,
)
}
}
// Check general permissions
permission := model.PermissionPrivatePlaybookCreate
if p.PlaybookIsPublic(playbook) {
permission = model.PermissionPublicPlaybookCreate
}
if p.api.HasPermissionToTeam(userID, playbook.TeamID, permission) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create playbook", userID)
}
func (p *PermissionsService) PlaybookManageProperties(userID string, playbook Playbook) error {
permission := model.PermissionPrivatePlaybookManageProperties
if p.PlaybookIsPublic(playbook) {
permission = model.PermissionPublicPlaybookManageProperties
}
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have access to playbook `%s`", userID, playbook.ID)
}
// PlaybookodifyWithFixes checks both ManageProperties and ManageMembers permissions
// performs permissions checks that can be resolved though modification of the input.
// This function modifies the playbook argument.
func (p *PermissionsService) PlaybookModifyWithFixes(userID string, playbook *Playbook, oldPlaybook Playbook) error {
// It is assumed that if you are calling this function there are properties changes
// This means that you need the manage properties permission to manage members for now.
if err := p.PlaybookManageProperties(userID, oldPlaybook); err != nil {
return err
}
if err := p.NoAddedBroadcastChannelsWithoutPermission(userID, playbook.BroadcastChannelIDs, oldPlaybook.BroadcastChannelIDs); err != nil {
return err
}
filteredUsers := p.FilterInvitedUserIDs(playbook.InvitedUserIDs, playbook.TeamID)
playbook.InvitedUserIDs = filteredUsers
filteredGroups := p.FilterInvitedGroupIDs(playbook.InvitedGroupIDs)
playbook.InvitedGroupIDs = filteredGroups
if playbook.DefaultOwnerID != "" {
if !p.api.HasPermissionToTeam(playbook.DefaultOwnerID, playbook.TeamID, model.PermissionViewTeam) {
logrus.WithFields(logrus.Fields{
"team_id": playbook.TeamID,
"user_id": playbook.DefaultOwnerID,
}).Warn("owner is not a member of the playbook's team, disabling default owner")
playbook.DefaultOwnerID = ""
playbook.DefaultOwnerEnabled = false
}
}
// Check if we have changed members, if so check that permission.
if !reflect.DeepEqual(oldPlaybook.Members, playbook.Members) {
if err := p.PlaybookManageMembers(userID, oldPlaybook); err != nil {
return errors.Wrap(err, "attempted to modify members without permissions")
}
oldMemberRoles := map[string]string{}
for _, member := range oldPlaybook.Members {
oldMemberRoles[member.UserID] = strings.Join(member.Roles, ",")
}
// Also need to check if roles changed. If so we need to check manage roles permission.
for _, member := range playbook.Members {
oldRoles, memberExisted := oldMemberRoles[member.UserID]
userAddedAsMember := !memberExisted && len(member.Roles) == 1 && member.Roles[0] == PlaybookRoleMember
rolesHaveNotChanged := memberExisted && strings.Join(member.Roles, ",") == oldRoles
if !(userAddedAsMember || rolesHaveNotChanged) {
if err := p.PlaybookManageRoles(userID, oldPlaybook); err != nil {
return errors.Wrap(err, "attempted to modify members without permissions")
}
break
}
}
}
// Check if we have done a public conversion
if oldPlaybook.Public != playbook.Public {
if oldPlaybook.Public {
if err := p.PlaybookMakePrivate(userID, oldPlaybook); err != nil {
return errors.Wrap(err, "attempted to make playbook private without permissions")
}
} else {
if err := p.PlaybookMakePublic(userID, oldPlaybook); err != nil {
return errors.Wrap(err, "attempted to make playbook public without permissions")
}
}
}
if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(*playbook)) {
return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license")
}
return nil
}
func (p *PermissionsService) FilterInvitedUserIDs(invitedUserIDs []string, teamID string) []string {
filteredUsers := []string{}
for _, userID := range invitedUserIDs {
if !p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
logrus.WithFields(logrus.Fields{
"team_id": teamID,
"user_id": userID,
}).Warn("user does not have permissions to playbook's team, removing from automated invite list")
continue
}
filteredUsers = append(filteredUsers, userID)
}
return filteredUsers
}
func (p *PermissionsService) FilterInvitedGroupIDs(invitedGroupIDs []string) []string {
filteredGroups := []string{}
for _, groupID := range invitedGroupIDs {
var group *model.Group
group, err := p.api.GetGroup(groupID)
if err != nil {
logrus.WithField("group_id", groupID).Error("failed to query group")
continue
}
if !group.AllowReference {
logrus.WithField("group_id", groupID).Warn("group does not allow references, removing from automated invite list")
continue
}
filteredGroups = append(filteredGroups, groupID)
}
return filteredGroups
}
func (p *PermissionsService) DeletePlaybook(userID string, playbook Playbook) error {
return p.PlaybookManageProperties(userID, playbook)
}
func (p *PermissionsService) NoAddedBroadcastChannelsWithoutPermission(userID string, broadcastChannelIDs, oldBroadcastChannelIDs []string) error {
oldChannelsSet := make(map[string]bool)
for _, channelID := range oldBroadcastChannelIDs {
oldChannelsSet[channelID] = true
}
for _, channelID := range broadcastChannelIDs {
if !oldChannelsSet[channelID] &&
!p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
return errors.Wrapf(
ErrNoPermissions,
"user `%s` does not have permission to create posts in channel `%s`",
userID,
channelID,
)
}
}
return nil
}
func (p *PermissionsService) PlaybookManageMembers(userID string, playbook Playbook) error {
permission := model.PermissionPrivatePlaybookManageMembers
if p.PlaybookIsPublic(playbook) {
permission = model.PermissionPublicPlaybookManageMembers
}
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage members for playbook `%s`", userID, playbook.ID)
}
func (p *PermissionsService) PlaybookManageRoles(userID string, playbook Playbook) error {
permission := model.PermissionPrivatePlaybookManageRoles
if p.PlaybookIsPublic(playbook) {
permission = model.PermissionPublicPlaybookManageRoles
}
if p.hasPermissionsToPlaybook(userID, playbook, permission) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage roles for playbook `%s`", userID, playbook.ID)
}
func (p *PermissionsService) PlaybookView(userID string, playbookID string) error {
playbook, err := p.playbookService.Get(playbookID)
if err != nil {
return errors.Wrapf(err, "Unable to get playbook to determine permissions, playbook id `%s`", playbookID)
}
return p.PlaybookViewWithPlaybook(userID, playbook)
}
func (p *PermissionsService) PlaybookList(userID, teamID string) error {
// Can list playbooks if you are on the team
if p.canViewTeam(userID, teamID) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to list playbooks for team `%s`", userID, teamID)
}
func (p *PermissionsService) PlaybookViewWithPlaybook(userID string, playbook Playbook) error {
noAccessErr := errors.Wrapf(
ErrNoPermissions,
"user `%s` to access playbook `%s`",
userID,
playbook.ID,
)
// Playbooks are tied to teams. You must have permission to the team to have permission to the playbook.
if !p.canViewTeam(userID, playbook.TeamID) {
return errors.Wrapf(noAccessErr, "no playbook access; no team view permission for team `%s`", playbook.TeamID)
}
// If the playbook is public team access is enough to view
if p.PlaybookIsPublic(playbook) {
return nil
}
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookView) {
return nil
}
return noAccessErr
}
func (p *PermissionsService) PlaybookMakePrivate(userID string, playbook Playbook) error {
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPublicPlaybookMakePrivate) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` private", userID, playbook.ID)
}
func (p *PermissionsService) PlaybookMakePublic(userID string, playbook Playbook) error {
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookMakePublic) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` public", userID, playbook.ID)
}
func (p *PermissionsService) RunCreate(userID string, playbook Playbook) error {
if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionRunCreate) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to run playbook `%s`", userID, playbook.ID)
}
func (p *PermissionsService) RunManageProperties(userID, runID string) error {
run, err := p.runService.GetPlaybookRun(runID)
if err != nil {
return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID)
}
return p.runManagePropertiesWithPlaybookRun(userID, run)
}
func (p *PermissionsService) runManagePropertiesWithPlaybookRun(userID string, run *PlaybookRun) error {
if run.OwnerUserID == userID {
return nil
}
for _, participantID := range run.ParticipantIDs {
if participantID == userID {
return nil
}
}
if IsSystemAdmin(userID, p.api) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage run `%s`", userID, run.ID)
}
func (p *PermissionsService) RunView(userID, runID string) error {
run, err := p.runService.GetPlaybookRun(runID)
if err != nil {
return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID)
}
// Has permission if is the owner of the run
if run.OwnerUserID == userID {
return nil
}
// Or if is a participant of the run
for _, participantID := range run.ParticipantIDs {
if participantID == userID {
return nil
}
}
// Or has view access to the playbook that created it
return p.PlaybookView(userID, run.PlaybookID)
}
func (p *PermissionsService) ChannelActionCreate(userID, channelID string) error {
if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create actions for channel `%s`", userID, channelID)
}
func (p *PermissionsService) ChannelActionView(userID, channelID string) error {
if p.api.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to view actions for channel `%s`", userID, channelID)
}
func (p *PermissionsService) ChannelActionUpdate(userID, channelID string) error {
if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) {
return nil
}
return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to update actions for channel `%s`", userID, channelID)
}
// IsSystemAdmin returns true if the userID is a system admin
func IsSystemAdmin(userID string, api playbooks.ServicesAPI) bool {
return api.HasPermissionTo(userID, model.PermissionManageSystem)
}
// CanManageChannelProperties returns true if the userID is allowed to manage the properties of channelID
func CanManageChannelProperties(userID, channelID string, api playbooks.ServicesAPI) bool {
channel, err := api.GetChannelByID(channelID)
if err != nil {
return false
}
permission := model.PermissionManagePublicChannelProperties
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelProperties
}
return api.HasPermissionToChannel(userID, channelID, permission)
}
func CanPostToChannel(userID, channelID string, api playbooks.ServicesAPI) bool {
return api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost)
}
func IsMemberOfTeam(userID, teamID string, api playbooks.ServicesAPI) bool {
teamMember, err := api.GetTeamMember(teamID, userID)
if err != nil {
return false
}
return teamMember.DeleteAt == 0
}
// RequesterInfo holds the userID and teamID that this request is regarding, and permissions
// for the user making the request
type RequesterInfo struct {
UserID string
TeamID string
IsAdmin bool
IsGuest bool
}
// IsGuest returns true if the userID is a system guest
func IsGuest(userID string, api playbooks.ServicesAPI) (bool, error) {
user, err := api.GetUserByID(userID)
if err != nil {
return false, errors.Wrapf(err, "Unable to get user to determine permissions, user id `%s`", userID)
}
return user.IsGuest(), nil
}
func GetRequesterInfo(userID string, api playbooks.ServicesAPI) (RequesterInfo, error) {
isAdmin := IsSystemAdmin(userID, api)
isGuest, err := IsGuest(userID, api)
if err != nil {
return RequesterInfo{}, err
}
return RequesterInfo{
UserID: userID,
IsAdmin: isAdmin,
IsGuest: isGuest,
}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql/driver"
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"gopkg.in/guregu/null.v4"
"github.com/pkg/errors"
)
// Playbook represents a desired business outcome, from which playbook runs are started to solve
// a specific instance.
// The tag export supports the export/import feature. If the field makes sense for export, the value should be
// the JSON name of the item in the export format. If the field should not be exported the value should be "-".
// Fields should be exported if they are not server specific like InvitedUserIDs or are tracking metadata like CreateAt.
type Playbook struct {
ID string `json:"id" export:"-"`
Title string `json:"title" export:"title"`
Description string `json:"description" export:"description"`
Public bool `json:"public" export:"-"`
TeamID string `json:"team_id" export:"-"`
CreatePublicPlaybookRun bool `json:"create_public_playbook_run" export:"-"`
CreateAt int64 `json:"create_at" export:"-"`
UpdateAt int64 `json:"update_at" export:"-"`
DeleteAt int64 `json:"delete_at" export:"-"`
NumStages int64 `json:"num_stages" export:"-"`
NumSteps int64 `json:"num_steps" export:"-"`
NumRuns int64 `json:"num_runs" export:"-"`
NumActions int64 `json:"num_actions" export:"-"`
LastRunAt int64 `json:"last_run_at" export:"-"`
Checklists []Checklist `json:"checklists" export:"-"`
Members []PlaybookMember `json:"members" export:"-"`
ReminderMessageTemplate string `json:"reminder_message_template" export:"reminder_message_template"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds" export:"reminder_timer_default_seconds"`
StatusUpdateEnabled bool `json:"status_update_enabled" export:"status_update_enabled"`
InvitedUserIDs []string `json:"invited_user_ids" export:"-"`
InvitedGroupIDs []string `json:"invited_group_ids" export:"-"`
InviteUsersEnabled bool `json:"invite_users_enabled" export:"-"`
DefaultOwnerID string `json:"default_owner_id" export:"-"`
DefaultOwnerEnabled bool `json:"default_owner_enabled" export:"-"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids" export:"-"`
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls" export:"-"`
WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled" export:"-"`
MessageOnJoin string `json:"message_on_join" export:"message_on_join"`
MessageOnJoinEnabled bool `json:"message_on_join_enabled" export:"message_on_join_enabled"`
RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds" export:"retrospective_reminder_interval_seconds"`
RetrospectiveTemplate string `json:"retrospective_template" export:"retrospective_template"`
RetrospectiveEnabled bool `json:"retrospective_enabled" export:"retrospective_enabled"`
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls" export:"-"`
SignalAnyKeywords []string `json:"signal_any_keywords" export:"signal_any_keywords"`
SignalAnyKeywordsEnabled bool `json:"signal_any_keywords_enabled" export:"signal_any_keywords_enabled"`
CategorizeChannelEnabled bool `json:"categorize_channel_enabled" export:"categorize_channel_enabled"`
CategoryName string `json:"category_name" export:"category_name"`
RunSummaryTemplateEnabled bool `json:"run_summary_template_enabled" export:"run_summary_template_enabled"`
RunSummaryTemplate string `json:"run_summary_template" export:"run_summary_template"`
ChannelNameTemplate string `json:"channel_name_template" export:"channel_name_template"`
DefaultPlaybookAdminRole string `json:"default_playbook_admin_role" export:"-"`
DefaultPlaybookMemberRole string `json:"default_playbook_member_role" export:"-"`
DefaultRunAdminRole string `json:"default_run_admin_role" export:"-"`
DefaultRunMemberRole string `json:"default_run_member_role" export:"-"`
Metrics []PlaybookMetricConfig `json:"metrics" export:"metrics"`
ActiveRuns int64 `json:"active_runs" export:"-"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant" export:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant" export:"create_channel_member_on_removed_participant"`
// ChannelID is the identifier of the channel that would be -potentially- linked
// to any new run of this playbook
ChannelID string `json:"channel_id" export:"channel_id"`
// ChannelMode is the playbook>run>channel flow used
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
// Deprecated: preserved for backwards compatibility with v1.27
BroadcastEnabled bool `json:"broadcast_enabled" export:"-"`
WebhookOnStatusUpdateEnabled bool `json:"webhook_on_status_update_enabled" export:"-"`
}
const (
PlaybookRoleMember = "playbook_member"
PlaybookRoleAdmin = "playbook_admin"
)
const (
MetricTypeDuration = "metric_duration"
MetricTypeCurrency = "metric_currency"
MetricTypeInteger = "metric_integer"
)
const MaxMetricsPerPlaybook = 4
type PlaybookMember struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
SchemeRoles []string `json:"scheme_roles"`
}
type PlaybookMetricConfig struct {
ID string `json:"id" export:"-"`
PlaybookID string `json:"playbook_id" export:"-"`
Title string `json:"title" export:"title"`
Description string `json:"description" export:"description"`
Type string `json:"type" export:"type"`
Target null.Int `json:"target" export:"target"`
}
func (pm PlaybookMember) Clone() PlaybookMember {
newPlaybookMember := pm
if len(pm.Roles) != 0 {
newPlaybookMember.Roles = append([]string(nil), pm.Roles...)
}
if len(pm.SchemeRoles) != 0 {
newPlaybookMember.SchemeRoles = append([]string(nil), pm.SchemeRoles...)
}
return newPlaybookMember
}
func (p Playbook) Clone() Playbook {
newPlaybook := p
var newChecklists []Checklist
for _, c := range p.Checklists {
newChecklists = append(newChecklists, c.Clone())
}
newPlaybook.Checklists = newChecklists
newPlaybook.Metrics = append([]PlaybookMetricConfig(nil), p.Metrics...)
var newMembers []PlaybookMember
for _, m := range p.Members {
newMembers = append(newMembers, m.Clone())
}
newPlaybook.Members = newMembers
if len(p.InvitedUserIDs) != 0 {
newPlaybook.InvitedUserIDs = append([]string(nil), p.InvitedUserIDs...)
}
if len(p.InvitedGroupIDs) != 0 {
newPlaybook.InvitedGroupIDs = append([]string(nil), p.InvitedGroupIDs...)
}
if len(p.SignalAnyKeywords) != 0 {
newPlaybook.SignalAnyKeywords = append([]string(nil), p.SignalAnyKeywords...)
}
if len(p.BroadcastChannelIDs) != 0 {
newPlaybook.BroadcastChannelIDs = append([]string(nil), p.BroadcastChannelIDs...)
}
if len(p.WebhookOnCreationURLs) != 0 {
newPlaybook.WebhookOnCreationURLs = append([]string(nil), p.WebhookOnCreationURLs...)
}
if len(p.WebhookOnStatusUpdateURLs) != 0 {
newPlaybook.WebhookOnStatusUpdateURLs = append([]string(nil), p.WebhookOnStatusUpdateURLs...)
}
return newPlaybook
}
func (p Playbook) MarshalJSON() ([]byte, error) {
type Alias Playbook
old := Alias(p.Clone())
// replace nils with empty slices for the frontend
if old.Checklists == nil {
old.Checklists = []Checklist{}
}
for j, cl := range old.Checklists {
if cl.Items == nil {
old.Checklists[j].Items = []ChecklistItem{}
}
}
if old.Members == nil {
old.Members = []PlaybookMember{}
}
if old.Metrics == nil {
old.Metrics = []PlaybookMetricConfig{}
}
if old.InvitedUserIDs == nil {
old.InvitedUserIDs = []string{}
}
if old.InvitedGroupIDs == nil {
old.InvitedGroupIDs = []string{}
}
if old.SignalAnyKeywords == nil {
old.SignalAnyKeywords = []string{}
}
if old.BroadcastChannelIDs == nil {
old.BroadcastChannelIDs = []string{}
}
if old.WebhookOnCreationURLs == nil {
old.WebhookOnCreationURLs = []string{}
}
if old.WebhookOnStatusUpdateURLs == nil {
old.WebhookOnStatusUpdateURLs = []string{}
}
return json.Marshal(old)
}
func (p Playbook) GetRunChannelID() string {
if p.ChannelMode == PlaybookRunLinkExistingChannel {
return p.ChannelID
}
return ""
}
// ChecklistCommon allows access on common fields of Checklist and api.UpdateChecklist
type ChecklistCommon interface {
GetItems() []ChecklistItemCommon
}
// Checklist represents a checklist in a playbook.
type Checklist struct {
// ID is the identifier of the checklist.
ID string `json:"id" export:"-"`
// Title is the name of the checklist.
Title string `json:"title" export:"title"`
// Items is an array of all the items in the checklist.
Items []ChecklistItem `json:"items" export:"-"`
}
func (c Checklist) GetItems() []ChecklistItemCommon {
items := make([]ChecklistItemCommon, len(c.Items))
for i := range c.Items {
items[i] = &c.Items[i]
}
return items
}
func (c Checklist) Clone() Checklist {
newChecklist := c
newChecklist.Items = append([]ChecklistItem(nil), c.Items...)
return newChecklist
}
// ChecklistItemCommon allows access on common fields of ChecklistItem and api.UpdateChecklistItem
type ChecklistItemCommon interface {
GetAssigneeID() string
SetAssigneeModified(modified int64)
SetState(state string)
SetStateModified(modified int64)
SetCommandLastRun(lastRun int64)
}
// ChecklistItem represents an item in a checklist.
type ChecklistItem struct {
// ID is the identifier of the checklist item.
ID string `json:"id" export:"-"`
// Title is the content of the checklist item.
Title string `json:"title" export:"title"`
// State is the state of the checklist item: "closed" if it's checked, "skipped" if it has
// been skipped, the empty string otherwise.
State string `json:"state" export:"-"`
// StateModified is the timestamp, in milliseconds since epoch, of the last time the item's
// state was modified. 0 if it was never modified.
StateModified int64 `json:"state_modified" export:"-"`
// AssigneeID is the identifier of the user to whom this item is assigned.
AssigneeID string `json:"assignee_id" export:"-"`
// AssigneeModified is the timestamp, in milliseconds since epoch, of the last time the item's
// assignee was modified. 0 if it was never modified.
AssigneeModified int64 `json:"assignee_modified" export:"-"`
// Command, if not empty, is the slash command that can be run as part of this item.
Command string `json:"command" export:"command"`
// CommandLastRun is the timestamp, in milliseconds since epoch, of the last time the item's
// slash command was run. 0 if it was never run.
CommandLastRun int64 `json:"command_last_run" export:"-"`
// Description is a string with the markdown content of the long description of the item.
Description string `json:"description" export:"description"`
// LastSkipped is the timestamp, in milliseconds since epoch, of the last time the item
// was skipped. 0 if it was never skipped.
LastSkipped int64 `json:"delete_at" export:"-"`
// DueDate is the timestamp, in milliseconds since epoch. indicates relative or absolute due date
// of the checklist item. 0 if not set.
// Playbook can have only relative timstamp, run can have only absolute timestamp.
DueDate int64 `json:"due_date" export:"due_date"`
// TaskActions is an array of all the task actions associated with this task.
TaskActions []TaskAction `json:"task_actions" export:"-"`
}
func (ci *ChecklistItem) GetAssigneeID() string {
return ci.AssigneeID
}
func (ci *ChecklistItem) SetAssigneeModified(modified int64) {
ci.AssigneeModified = modified
}
func (ci *ChecklistItem) SetState(state string) {
ci.State = state
}
func (ci *ChecklistItem) SetStateModified(modified int64) {
ci.StateModified = modified
}
func (ci *ChecklistItem) SetCommandLastRun(lastRun int64) {
ci.CommandLastRun = lastRun
}
type GetPlaybooksResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []Playbook `json:"items"`
}
// MarshalJSON customizes the JSON marshalling for GetPlaybooksResults by rendering a nil Items as
// an empty slice instead.
func (r GetPlaybooksResults) MarshalJSON() ([]byte, error) {
type Alias GetPlaybooksResults
if r.Items == nil {
r.Items = []Playbook{}
}
aux := &struct {
*Alias
}{
Alias: (*Alias)(&r),
}
return json.Marshal(aux)
}
// PlaybookService is the playbook service for managing playbooks
// userID is the user initiating the event.
type PlaybookService interface {
// Get retrieves a playbook. Returns ErrNotFound if not found.
Get(id string) (Playbook, error)
// Create creates a new playbook
Create(playbook Playbook, userID string) (string, error)
// Import imports a new playbook
Import(playbook Playbook, userID string) (string, error)
// GetPlaybooks retrieves all playbooks
GetPlaybooks() ([]Playbook, error)
// GetPlaybooksForTeam retrieves all playbooks on the specified team given the provided options
GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error)
// Update updates a playbook
Update(playbook Playbook, userID string) error
// Archive archives a playbook
Archive(playbook Playbook, userID string) error
// Restores an archived playbook
Restore(playbook Playbook, userID string) error
// AutoFollow method lets user auto-follow all runs of a specific playbook
AutoFollow(playbookID, userID string) error
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
AutoUnfollow(playbookID, userID string) error
// GetAutoFollows returns list of users who auto-follows a playbook
GetAutoFollows(playbookID string) ([]string, error)
// Duplicate duplicates a playbook
Duplicate(playbook Playbook, userID string) (string, error)
// Get top playbooks for teams
GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
// Get top playbooks for users
GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
}
// PlaybookStore is an interface for storing playbooks
type PlaybookStore interface {
// Get retrieves a playbook
Get(id string) (Playbook, error)
// Create creates a new playbook
Create(playbook Playbook) (string, error)
// GetPlaybooks retrieves all playbooks
GetPlaybooks() ([]Playbook, error)
// GetPlaybooksForTeam retrieves all playbooks on the specified team
GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error)
// GetPlaybooksWithKeywords retrieves all playbooks with keywords enabled
GetPlaybooksWithKeywords(opts PlaybookFilterOptions) ([]Playbook, error)
// GetTimeLastUpdated retrieves time last playbook was updated at.
// Passed argument determines whether to include playbooks with
// SignalAnyKeywordsEnabled flag or not.
GetTimeLastUpdated(onlyPlaybooksWithKeywordsEnabled bool) (int64, error)
// GetPlaybookIDsForUser retrieves playbooks user can access
GetPlaybookIDsForUser(userID, teamID string) ([]string, error)
// Update updates a playbook
Update(playbook Playbook) error
// GraphqlUpdate taking a setmap for graphql
GraphqlUpdate(id string, setmap map[string]interface{}) error
// Archive archives a playbook
Archive(id string) error
// Restore restores a deleted playbook
Restore(id string) error
// AutoFollow method lets user auto-follow all runs of a specific playbook
AutoFollow(playbookID, userID string) error
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
AutoUnfollow(playbookID, userID string) error
// GetAutoFollows returns list of users who auto-follows a playbook
GetAutoFollows(playbookID string) ([]string, error)
// GetPlaybooksActiveTotal returns number of active playbooks
GetPlaybooksActiveTotal() (int64, error)
// GetMetric retrieves a metric by ID
GetMetric(id string) (*PlaybookMetricConfig, error)
// AddMetric adds a metric
AddMetric(playbookID string, config PlaybookMetricConfig) error
// UpdateMetric updates a metric
UpdateMetric(id string, setmap map[string]interface{}) error
// DeleteMetric deletes a metric
DeleteMetric(id string) error
// Get top playbooks for teams
GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
// Get top playbooks for users
GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error)
// AddPlaybookMember adds a user as a member to a playbook
AddPlaybookMember(id string, memberID string) error
// RemovePlaybookMember removes a user from a playbook
RemovePlaybookMember(id string, memberID string) error
}
// PlaybookTelemetry defines the methods that the Playbook service needs from the RudderTelemetry.
// userID is the user initiating the event.
type PlaybookTelemetry interface {
// CreatePlaybook tracks the creation of a playbook.
CreatePlaybook(playbook Playbook, userID string)
// ImportPlaybook tracks the import of a playbook.
ImportPlaybook(playbook Playbook, userID string)
// UpdatePlaybook tracks the update of a playbook.
UpdatePlaybook(playbook Playbook, userID string)
// DeletePlaybook tracks the deletion of a playbook.
DeletePlaybook(playbook Playbook, userID string)
// RestorePlaybook tracks the restoration of a playbook.
RestorePlaybook(playbook Playbook, userID string)
// FrontendTelemetryForPlaybook tracks an event originating from the frontend
FrontendTelemetryForPlaybook(playbook Playbook, userID, action string)
// FrontendTelemetryForPlaybookTemplate tracks an event originating from the frontend
FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string)
// AutoFollowPlaybook tracks the auto-follow of a playbook.
AutoFollowPlaybook(playbook Playbook, userID string)
// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook.
AutoUnfollowPlaybook(playbook Playbook, userID string)
}
const (
ChecklistItemStateOpen = ""
ChecklistItemStateInProgress = "in_progress"
ChecklistItemStateClosed = "closed"
ChecklistItemStateSkipped = "skipped"
)
func IsValidChecklistItemState(state string) bool {
return state == ChecklistItemStateClosed ||
state == ChecklistItemStateInProgress ||
state == ChecklistItemStateOpen ||
state == ChecklistItemStateSkipped
}
func IsValidChecklistItemIndex(checklists []Checklist, checklistNum, itemNum int) bool {
return checklists != nil && checklistNum >= 0 && itemNum >= 0 && checklistNum < len(checklists) && itemNum < len(checklists[checklistNum].Items)
}
// PlaybookFilterOptions specifies the parameters when getting playbooks.
type PlaybookFilterOptions struct {
Sort SortField
Direction SortDirection
SearchTerm string
WithArchived bool
WithMembershipOnly bool //if true will return only playbooks you are a member of
PlaybookIDs []string
// Pagination options.
Page int
PerPage int
}
// Clone duplicates the given options.
func (o *PlaybookFilterOptions) Clone() PlaybookFilterOptions {
return *o
}
// Validate returns a new, validated filter options or returns an error if invalid.
func (o PlaybookFilterOptions) Validate() (PlaybookFilterOptions, error) {
options := o.Clone()
if options.PerPage <= 0 {
options.PerPage = PerPageDefault
}
options.Sort = SortField(strings.ToLower(string(options.Sort)))
switch options.Sort {
case SortByID:
case SortByTitle:
case SortByStages:
case SortBySteps:
case "": // default
options.Sort = SortByID
default:
return PlaybookFilterOptions{}, errors.Errorf("unsupported sort '%s'", options.Sort)
}
options.Direction = SortDirection(strings.ToUpper(string(options.Direction)))
switch options.Direction {
case DirectionAsc:
case DirectionDesc:
case "": //default
options.Direction = DirectionAsc
default:
return PlaybookFilterOptions{}, errors.Errorf("unsupported direction '%s'", options.Direction)
}
return options, nil
}
func ValidateWebhookURLs(urls []string) error {
if len(urls) > 64 {
return errors.New("too many registered urls, limit to less than 64")
}
for _, webhook := range urls {
reqURL, err := url.ParseRequestURI(webhook)
if err != nil {
return errors.Wrapf(err, "unable to parse webhook: %v", webhook)
}
if reqURL.Scheme != "http" && reqURL.Scheme != "https" {
return fmt.Errorf("protocol in webhook URL is %s; only HTTP and HTTPS are accepted", reqURL.Scheme)
}
}
return nil
}
func ValidateCategoryName(categoryName string) error {
categoryNameLength := len(categoryName)
if categoryNameLength > 22 {
msg := fmt.Sprintf("invalid category name: %s (maximum length is 22 characters)", categoryName)
return errors.Errorf(msg)
}
return nil
}
// CleanUpChecklists sets empty values for checklist fields that are not editable
func CleanUpChecklists[T ChecklistCommon](checklists []T) {
for listIndex := range checklists {
items := checklists[listIndex].GetItems()
for itemIndex := range items {
items[itemIndex].SetAssigneeModified(0)
items[itemIndex].SetState("")
items[itemIndex].SetStateModified(0)
items[itemIndex].SetCommandLastRun(0)
}
}
}
// ValidatePreAssignment checks if invitations are enabled and if all assignees are also invited
func ValidatePreAssignment(assignees []string, invitedUsers []string, inviteUsersEnabled bool) error {
if len(assignees) > 0 && !inviteUsersEnabled {
return errors.New("invitations are disabled")
}
if !assigneesAreInvited(assignees, invitedUsers) {
return errors.New("users missing in invite user list")
}
return nil
}
// GetDistinctAssignees returns a list of distinct user ids that are assignees in the given checklists
func GetDistinctAssignees[T ChecklistCommon](checklists []T) []string {
uMap := make(map[string]bool)
for _, cl := range checklists {
for _, ci := range cl.GetItems() {
if id := ci.GetAssigneeID(); id != "" && !uMap[id] {
uMap[id] = true
}
}
}
uIds := make([]string, 0, len(uMap))
for k := range uMap {
uIds = append(uIds, k)
}
return uIds
}
func assigneesAreInvited(assignees []string, invited []string) bool {
for _, assignee := range assignees {
found := false
for _, user := range invited {
if user == assignee {
found = true
}
}
if !found {
return false
}
}
return true
}
func removeDuplicates(a []string) []string {
items := make(map[string]bool)
for _, item := range a {
if item != "" {
items[item] = true
}
}
res := make([]string, 0, len(items))
for item := range items {
res = append(res, item)
}
return res
}
func ProcessSignalAnyKeywords(keywords []string) []string {
return removeDuplicates(keywords)
}
// models for playbooks-insights
// PlaybooksInsightsList is a response type with pagination support.
type PlaybooksInsightsList struct {
HasNext bool `json:"has_next"`
Items []*PlaybookInsight `json:"items"`
}
// PlaybookInsight gives insight into activities related to a playbook
type PlaybookInsight struct {
// ID of the playbook
// required: true
PlaybookID string `json:"playbook_id"`
// Run count of playbook
// required: true
NumRuns int `json:"num_runs"`
// Title of playbook
// required: true
Title string `json:"title"`
// Time the playbook was last run.
// required: false
LastRunAt int64 `json:"last_run_at"`
}
// ChannelPlaybookMode is a type alias to hold all possible
// modes for playbook > run > channel relation
type ChannelPlaybookMode int
const (
PlaybookRunCreateNewChannel ChannelPlaybookMode = iota
PlaybookRunLinkExistingChannel
)
var channelPlaybookTypes = [...]string{
PlaybookRunCreateNewChannel: "create_new_channel",
PlaybookRunLinkExistingChannel: "link_existing_channel",
}
// String creates the string version of the TelemetryTrack
func (cpm ChannelPlaybookMode) String() string {
return channelPlaybookTypes[cpm]
}
// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON)
func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) {
return []byte(channelPlaybookTypes[cpm]), nil
}
// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON)
func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error {
for i, st := range channelPlaybookTypes {
if st == string(text) {
*cpm = ChannelPlaybookMode(i)
return nil
}
}
return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text))
}
// Scan parses a ChannelPlaybookMode back from the DB
func (cpm *ChannelPlaybookMode) Scan(src interface{}) error {
txt, ok := src.([]byte) // mysql
if !ok {
txt, ok := src.(string) //postgres
if !ok {
return fmt.Errorf("could not cast to string: %v", src)
}
return cpm.UnmarshalText([]byte(txt))
}
return cpm.UnmarshalText(txt)
}
// Value represents a ChannelPlaybookMode as a type writable into the DB
func (cpm ChannelPlaybookMode) Value() (driver.Value, error) {
return cpm.MarshalText()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"strings"
"time"
"gopkg.in/guregu/null.v4"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/playbooks/product/pluginapi/cluster"
)
const (
StatusInProgress = "InProgress"
StatusFinished = "Finished"
)
const (
RunRoleMember = "run_member"
RunRoleAdmin = "run_admin"
)
const (
RunSourcePost = "post"
RunSourceDialog = "dialog"
)
const (
RunTypePlaybook = "playbook"
RunTypeChannelChecklist = "channelChecklist"
)
// PlaybookRun holds the detailed information of a playbook run.
//
// NOTE: When adding a column to the db, search for "When adding a PlaybookRun column" to see where
// that column needs to be added in the sqlstore code.
type PlaybookRun struct {
// ID is the unique identifier of the playbook run.
ID string `json:"id"`
// Name is the name of the playbook run's channel.
Name string `json:"name"`
// Summary is a short string, in Markdown, describing what the run is.
Summary string `json:"summary"`
// SummaryModifiedAt is date when the summary was modified
SummaryModifiedAt int64 `json:"summary_modified_at"`
// OwnerUserID is the user identifier of the playbook run's owner.
OwnerUserID string `json:"owner_user_id"`
// ReporterUserID is the user identifier of the playbook run's reporter; i.e., the user that created the run.
ReporterUserID string `json:"reporter_user_id"`
// TeamID is the identifier of the team the playbook run lives in.
TeamID string `json:"team_id"`
// ChannelID is the identifier of the playbook run's channel.
ChannelID string `json:"channel_id"`
// CreateAt is the timestamp, in milliseconds since epoch, of when the playbook run was created.
CreateAt int64 `json:"create_at"`
// EndAt is the timestamp, in milliseconds since epoch, of when the playbook run was ended.
// If 0, the run is still ongoing.
EndAt int64 `json:"end_at"`
// Deprecated: preserved for backwards compatibility with v1.2.
DeleteAt int64 `json:"delete_at"`
// Deprecated: preserved for backwards compatibility with v1.2.
ActiveStage int `json:"active_stage"`
// Deprecated: preserved for backwards compatibility with v1.2.
ActiveStageTitle string `json:"active_stage_title"`
// PostID, if not empty, is the identifier of the post from which this playbook run was originally created.
PostID string `json:"post_id"`
// PlaybookID is the identifier of the playbook from which this run was created.
PlaybookID string `json:"playbook_id"`
// Checklists is an array of the checklists in the run.
Checklists []Checklist `json:"checklists"`
// StatusPosts is an array of all the status updates posted in the run.
StatusPosts []StatusPost `json:"status_posts"`
// CurrentStatus is the current status of the playbook run.
// It can be StatusInProgress ("InProgress") or StatusFinished ("Finished")
CurrentStatus string `json:"current_status"`
// LastStatusUpdateAt is the timestamp, in milliseconds since epoch, of the time the last
// status update was posted.
LastStatusUpdateAt int64 `json:"last_status_update_at"`
// ReminderPostID, if not empty, is the identifier of the reminder posted to the channel to
// update the status.
ReminderPostID string `json:"reminder_post_id"`
// PreviousReminder, if not empty, is the time.Duration (nanoseconds) at which the next
// scheduled status update will be posted.
PreviousReminder time.Duration `json:"previous_reminder"`
// ReminderMessageTemplate, if not empty, is the template shown when updating the status of the
// playbook run for the first time.
ReminderMessageTemplate string `json:"reminder_message_template"`
// ReminderTimerDefaultSeconds is the expected default interval, in seconds,
// between every status update
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
//Defines if status update functionality is enabled
StatusUpdateEnabled bool `json:"status_update_enabled"`
// InvitedUserIDs, if not empty, is an array containing the identifiers of the users that were
// automatically invited to the playbook run when it was created.
InvitedUserIDs []string `json:"invited_user_ids"`
// InvitedGroupIDs, if not empty, is an array containing the identifiers of the user groups that
// were automatically invited to the playbook run when it was created.
InvitedGroupIDs []string `json:"invited_group_ids"`
// TimelineEvents is an array of the events saved to the timeline of the playbook run.
TimelineEvents []TimelineEvent `json:"timeline_events"`
// DefaultOwnerID, if not empty, is the identifier of the user that was automatically assigned
// as owner of the playbook run when it was created.
DefaultOwnerID string `json:"default_owner_id"`
// BroadcastChannelIDs is an array of the identifiers of the channels where the playbook run
// creation and status updates are announced.
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
// WebhookOnCreationURLs, if not empty, is the URL to which a POST request is made with the whole
// playbook run as payload when the run is created.
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
// WebhookOnStatusUpdateURLs, if not empty, is the URL to which a POST request is made with the
// whole playbook run as payload every time the status of the playbook run is updated.
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
// StatusUpdateBroadcastChannelsEnabled is true if the channels broadcast action is enabled for
// the run status update event, false otherwise.
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
// StatusUpdateBroadcastWebhooksEnabled is true if the webhooks broadcast action is enabled for
// the run status update event, false otherwise.
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
// Retrospective is a string containing the currently saved retrospective.
// If RetrospectivePublishedAt is different than 0, this is the final published retrospective.
Retrospective string `json:"retrospective"`
// RetrospectivePublishedAt is the timestamp, in milliseconds since epoch, of the last time a
// retrospective was published. If 0, the retrospective has not been published yet.
RetrospectivePublishedAt int64 `json:"retrospective_published_at"`
// RetrospectiveWasCanceled is true if the retrospective was cancelled, false otherwise.
RetrospectiveWasCanceled bool `json:"retrospective_was_canceled"`
// RetrospectiveReminderIntervalSeconds is the interval, in seconds, between subsequent reminders
// to fill the retrospective.
RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds"`
// Defines if retrospective functionality is enabled
RetrospectiveEnabled bool `json:"retrospective_enabled"`
// MessageOnJoin, if not empty, is the message shown to every user that joins the channel of
// the playbook run.
MessageOnJoin string `json:"message_on_join"`
// ParticipantIDs is an array of the identifiers of all the participants in the playbook run.
// A participant is any member of the playbook run channel that isn't a bot.
ParticipantIDs []string `json:"participant_ids"`
// CategoryName, if not empty, is the name of the category where the run channel will live.
CategoryName string `json:"category_name"`
// Playbook run metric values
MetricsData []RunMetricData `json:"metrics_data"`
// CreateChannelMemberOnNewParticipant is the Run action flag that defines if a new channel member will be added
// to the run's channel when a new participant is added to the run (by themselve or by other members).
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant" export:"create_channel_member_on_new_participant"`
// RemoveChannelMemberOnRemovedParticipant is the Run action flag that defines if an existent channel member will be removed
// from the run's channel when a new participant is added to the run (by themselve or by other members).
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant" export:"create_channel_member_on_removed_participant"`
// Type determines a type of a run.
// It can be RunTypePlaybook ("playbook") or RunTypeChannelChecklist ("channel")
Type string `json:"type"`
}
func (r *PlaybookRun) Clone() *PlaybookRun {
newPlaybookRun := *r
var newChecklists []Checklist
for _, c := range r.Checklists {
newChecklists = append(newChecklists, c.Clone())
}
newPlaybookRun.Checklists = newChecklists
newPlaybookRun.StatusPosts = append([]StatusPost(nil), r.StatusPosts...)
newPlaybookRun.TimelineEvents = append([]TimelineEvent(nil), r.TimelineEvents...)
newPlaybookRun.InvitedUserIDs = append([]string(nil), r.InvitedUserIDs...)
newPlaybookRun.InvitedGroupIDs = append([]string(nil), r.InvitedGroupIDs...)
newPlaybookRun.ParticipantIDs = append([]string(nil), r.ParticipantIDs...)
newPlaybookRun.WebhookOnCreationURLs = append([]string(nil), r.WebhookOnCreationURLs...)
newPlaybookRun.WebhookOnStatusUpdateURLs = append([]string(nil), r.WebhookOnStatusUpdateURLs...)
newPlaybookRun.MetricsData = append([]RunMetricData(nil), r.MetricsData...)
return &newPlaybookRun
}
func (r PlaybookRun) MarshalJSON() ([]byte, error) {
type Alias PlaybookRun
old := (*Alias)(r.Clone())
// replace nils with empty slices for the frontend
if old.Checklists == nil {
old.Checklists = []Checklist{}
}
for j, cl := range old.Checklists {
if cl.Items == nil {
old.Checklists[j].Items = []ChecklistItem{}
}
}
if old.StatusPosts == nil {
old.StatusPosts = []StatusPost{}
}
if old.InvitedUserIDs == nil {
old.InvitedUserIDs = []string{}
}
if old.InvitedGroupIDs == nil {
old.InvitedGroupIDs = []string{}
}
if old.TimelineEvents == nil {
old.TimelineEvents = []TimelineEvent{}
}
if old.ParticipantIDs == nil {
old.ParticipantIDs = []string{}
}
if old.BroadcastChannelIDs == nil {
old.BroadcastChannelIDs = []string{}
}
if old.WebhookOnCreationURLs == nil {
old.WebhookOnCreationURLs = []string{}
}
if old.WebhookOnStatusUpdateURLs == nil {
old.WebhookOnStatusUpdateURLs = []string{}
}
if old.MetricsData == nil {
old.MetricsData = []RunMetricData{}
}
return json.Marshal(old)
}
// SetChecklistFromPlaybook overwrites this run's checklists with the ones in the provided playbook.
func (r *PlaybookRun) SetChecklistFromPlaybook(playbook Playbook) {
r.Checklists = playbook.Checklists
// Playbooks can only have due dates relative to when a run starts,
// so we should convert them to absolute timestamp.
now := model.GetMillis()
for i := range r.Checklists {
for j := range r.Checklists[i].Items {
if r.Checklists[i].Items[j].DueDate > 0 {
r.Checklists[i].Items[j].DueDate += now
}
}
}
}
// SetConfigurationFromPlaybook overwrites this run's configuration with the data from the provided playbook,
// effectively snapshoting the playbook's configuration in this moment of time.
func (r *PlaybookRun) SetConfigurationFromPlaybook(playbook Playbook, source string) {
// Runs created through managed dialog lack summary, and we should use the template (if enabled)
// Runs created though new modal would have filled the summary in the webapp
if playbook.RunSummaryTemplateEnabled && source == RunSourceDialog {
r.Summary = playbook.RunSummaryTemplate
}
r.ReminderMessageTemplate = playbook.ReminderMessageTemplate
r.StatusUpdateEnabled = playbook.StatusUpdateEnabled
r.PreviousReminder = time.Duration(playbook.ReminderTimerDefaultSeconds) * time.Second
r.ReminderTimerDefaultSeconds = playbook.ReminderTimerDefaultSeconds
r.InvitedUserIDs = []string{}
r.InvitedGroupIDs = []string{}
if playbook.InviteUsersEnabled {
r.InvitedUserIDs = playbook.InvitedUserIDs
r.InvitedGroupIDs = playbook.InvitedGroupIDs
}
if playbook.DefaultOwnerEnabled {
r.DefaultOwnerID = playbook.DefaultOwnerID
}
// Do not propagate StatusUpdateBroadcastChannelsEnabled as true if there are no channels in BroadcastChannelIDs
r.StatusUpdateBroadcastChannelsEnabled = playbook.BroadcastEnabled && len(playbook.BroadcastChannelIDs) > 0
r.BroadcastChannelIDs = playbook.BroadcastChannelIDs
r.WebhookOnCreationURLs = []string{}
if playbook.WebhookOnCreationEnabled {
r.WebhookOnCreationURLs = playbook.WebhookOnCreationURLs
}
// Do not propagate StatusUpdateBroadcastWebhooksEnabled as true if there are no URLs
r.StatusUpdateBroadcastWebhooksEnabled = playbook.WebhookOnStatusUpdateEnabled && len(playbook.WebhookOnStatusUpdateURLs) > 0
r.WebhookOnStatusUpdateURLs = playbook.WebhookOnStatusUpdateURLs
r.RetrospectiveEnabled = playbook.RetrospectiveEnabled
if playbook.RetrospectiveEnabled {
r.RetrospectiveReminderIntervalSeconds = playbook.RetrospectiveReminderIntervalSeconds
r.Retrospective = playbook.RetrospectiveTemplate
}
r.CreateChannelMemberOnNewParticipant = playbook.CreateChannelMemberOnNewParticipant
r.RemoveChannelMemberOnRemovedParticipant = playbook.RemoveChannelMemberOnRemovedParticipant
r.Type = RunTypePlaybook
}
type StatusPost struct {
// ID is the identifier of the post containing the status update.
ID string `json:"id"`
// CreateAt is the timestamp, in milliseconds since epoch, of the time this status update was
// posted.
CreateAt int64 `json:"create_at"`
// DeleteAt is the timestamp, in milliseconds since epoch, of the time the post containing this
// status update was deleted. 0 if it was never deleted.
DeleteAt int64 `json:"delete_at"`
}
// StatusPostComplete is the "complete" representation of a status update
//
// This type is part of an effort to decopuple channels and playbooks, where
// status updates will stop being -only- Posts in a channel.
type StatusPostComplete struct {
// ID is the identifier of the post containing the status update.
ID string `json:"id"`
// CreateAt is the timestamp, in milliseconds since epoch, of the time this status update was
// posted.
CreateAt int64 `json:"create_at"`
// DeleteAt is the timestamp, in milliseconds since epoch, of the time the post containing this
// status update was deleted. 0 if it was never deleted.
DeleteAt int64 `json:"delete_at"`
// Message is the content of the status update. It supports markdown.
Message string `json:"message"`
// AuthorUserName is the username of the user who sent the status update.
AuthorUserName string `json:"author_user_name"`
}
// NewStatusPostComplete creates a StatusUpdate from a channel Post
func NewStatusPostComplete(post *model.Post) *StatusPostComplete {
author, _ := post.GetProp("authorUsername").(string)
return &StatusPostComplete{
ID: post.Id,
CreateAt: post.CreateAt,
DeleteAt: post.DeleteAt,
Message: post.Message,
AuthorUserName: author,
}
}
type UpdateOptions struct {
}
// StatusUpdateOptions encapsulates the fields that can be set when updating a playbook run's status
// NOTE: changes made to this should be reflected in the client package.
type StatusUpdateOptions struct {
Message string `json:"message"`
Reminder time.Duration `json:"reminder"`
FinishRun bool `json:"finish_run"`
}
// Metadata tracks ancillary metadata about a playbook run.
type Metadata struct {
ChannelName string `json:"channel_name"`
ChannelDisplayName string `json:"channel_display_name"`
TeamName string `json:"team_name"`
NumParticipants int64 `json:"num_participants"`
TotalPosts int64 `json:"total_posts"`
Followers []string `json:"followers"`
}
type timelineEventType string
const (
PlaybookRunCreated timelineEventType = "incident_created"
TaskStateModified timelineEventType = "task_state_modified"
StatusUpdated timelineEventType = "status_updated"
StatusUpdateRequested timelineEventType = "status_update_requested"
OwnerChanged timelineEventType = "owner_changed"
AssigneeChanged timelineEventType = "assignee_changed"
RanSlashCommand timelineEventType = "ran_slash_command"
EventFromPost timelineEventType = "event_from_post"
UserJoinedLeft timelineEventType = "user_joined_left"
ParticipantsChanged timelineEventType = "participants_changed"
PublishedRetrospective timelineEventType = "published_retrospective"
CanceledRetrospective timelineEventType = "canceled_retrospective"
RunFinished timelineEventType = "run_finished"
RunRestored timelineEventType = "run_restored"
StatusUpdateSnoozed timelineEventType = "status_update_snoozed"
StatusUpdatesEnabled timelineEventType = "status_updates_enabled"
StatusUpdatesDisabled timelineEventType = "status_updates_disabled"
)
type TimelineEvent struct {
// ID is the identifier of this event.
ID string `json:"id"`
// PlaybookRunID is the identifier of the playbook run this event lives in.
PlaybookRunID string `json:"playbook_run_id"`
// CreateAt is the timestamp, in milliseconds since epoch, of the time this event was created.
CreateAt int64 `json:"create_at"`
// DeleteAt is the timestamp, in milliseconds since epoch, of the time this event was deleted.
// 0 if it was never deleted.
DeleteAt int64 `json:"delete_at"`
// EventAt is the timestamp, in milliseconds since epoch, of the actual situation this event is
// describing.
EventAt int64 `json:"event_at"`
// EventType is the type of this event. It can be "incident_created", "task_state_modified",
// "status_updated", "owner_changed", "assignee_changed", "ran_slash_command",
// "event_from_post", "user_joined_left", "published_retrospective", "canceled_retrospective" or "status_update_snoozed".
EventType timelineEventType `json:"event_type"`
// Summary is a short description of the event.
Summary string `json:"summary"`
// Details is the longer description of the event.
Details string `json:"details"`
// PostID, if not empty, is the identifier of the post announcing in the channel this event
// happened. If the event is of type "event_from_post", this is the identifier of that post.
PostID string `json:"post_id"`
// SubjectUserID is the identifier of the user involved in the event. For example, if the event
// is of type "owner_changed", this is the identifier of the new owner.
SubjectUserID string `json:"subject_user_id"`
// CreatorUserID is the identifier of the user that created the event.
CreatorUserID string `json:"creator_user_id"`
}
// GetPlaybookRunsResults collects the results of the GetPlaybookRuns call: the list of PlaybookRuns matching
// the HeaderFilterOptions, and the TotalCount of the matching playbook runs before paging was applied.
type GetPlaybookRunsResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
PerPage int `json:"per_page"`
HasMore bool `json:"has_more"`
Items []PlaybookRun `json:"items"`
}
type SQLStatusPost struct {
PlaybookRunID string
PostID string
EndAt int64
}
type RunMetricData struct {
MetricConfigID string `json:"metric_config_id"`
Value null.Int `json:"value"`
}
type RetrospectiveUpdate struct {
Text string `json:"retrospective"`
Metrics []RunMetricData `json:"metrics"`
}
func (r GetPlaybookRunsResults) Clone() GetPlaybookRunsResults {
newGetPlaybookRunsResults := r
newGetPlaybookRunsResults.Items = make([]PlaybookRun, 0, len(r.Items))
for _, i := range r.Items {
newGetPlaybookRunsResults.Items = append(newGetPlaybookRunsResults.Items, *i.Clone())
}
return newGetPlaybookRunsResults
}
func (r GetPlaybookRunsResults) MarshalJSON() ([]byte, error) {
type Alias GetPlaybookRunsResults
old := Alias(r.Clone())
// replace nils with empty slices for the frontend
if old.Items == nil {
old.Items = []PlaybookRun{}
}
return json.Marshal(old)
}
// OwnerInfo holds the summary information of a owner.
type OwnerInfo struct {
UserID string `json:"user_id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Nickname string `json:"nickname"`
}
// DialogState holds the start playbook run interactive dialog's state as it appears in the client
// and is submitted back to the server.
type DialogState struct {
PostID string `json:"post_id"`
ClientID string `json:"client_id"`
PromptPostID string `json:"prompt_post_id"`
}
type DialogStateAddToTimeline struct {
PostID string `json:"post_id"`
}
// RunLink represents the info needed to display and link to a run
type RunLink struct {
PlaybookRunID string
Name string
}
// AssignedRun represents all the info needed to display a Run & ChecklistItem to a user
type AssignedRun struct {
RunLink
Tasks []AssignedTask
}
// AssignedTask represents a ChecklistItem + extra info needed to display to a user
type AssignedTask struct {
// ID is the identifier of the containing checklist.
ChecklistID string
// Title is the name of the containing checklist.
ChecklistTitle string
ChecklistItem
}
// RunAction represents the run action settings. Frontend passes this struct to update settings.
type RunAction struct {
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
}
type RunMetadata struct {
ID string
Name string
TeamID string
}
type TopicMetadata struct {
ID string
RunID string
TeamID string
}
const (
ActionTypeBroadcastChannels = "broadcast_to_channels"
ActionTypeBroadcastWebhooks = "broadcast_to_webhooks"
TriggerTypeStatusUpdatePosted = "status_update_posted"
)
// PlaybookRunService is the playbook run service interface.
type PlaybookRunService interface {
// GetPlaybookRuns returns filtered playbook runs and the total count before paging.
GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error)
// CreatePlaybookRun creates a new playbook run. userID is the user who initiated the CreatePlaybookRun.
CreatePlaybookRun(playbookRun *PlaybookRun, playbook *Playbook, userID string, public bool) (*PlaybookRun, error)
// OpenCreatePlaybookRunDialog opens an interactive dialog to start a new playbook run.
OpenCreatePlaybookRunDialog(teamID, ownerID, triggerID, postID, clientID string, playbooks []Playbook) error
// OpenUpdateStatusDialog opens an interactive dialog so the user can update the playbook run's status.
OpenUpdateStatusDialog(playbookRunID, userID, triggerID string) error
// OpenAddToTimelineDialog opens an interactive dialog so the user can add a post to the playbook run timeline.
OpenAddToTimelineDialog(requesterInfo RequesterInfo, postID, teamID, triggerID string) error
// OpenAddChecklistItemDialog opens an interactive dialog so the user can add a post to the playbook run timeline.
OpenAddChecklistItemDialog(triggerID, userID, playbookRunID string, checklist int) error
// AddPostToTimeline adds an event based on a post to a playbook run's timeline.
AddPostToTimeline(playbookRunID, userID, postID, summary string) error
// RemoveTimelineEvent removes the timeline event (sets the DeleteAt to the current time).
RemoveTimelineEvent(playbookRunID, userID, eventID string) error
// UpdateStatus updates a playbook run's status.
UpdateStatus(playbookRunID, userID string, options StatusUpdateOptions) error
// OpenFinishPlaybookRunDialog opens the dialog to confirm the run should be finished.
OpenFinishPlaybookRunDialog(playbookRunID, userID, triggerID string) error
// FinishPlaybookRun changes a run's state to Finished. If run is already in Finished state, the call is a noop.
FinishPlaybookRun(playbookRunID, userID string) error
// ToggleStatusUpdates enables or disables status update for the run
ToggleStatusUpdates(playbookRunID, userID string, enable bool) error
// GetPlaybookRun gets a playbook run by ID. Returns error if it could not be found.
GetPlaybookRun(playbookRunID string) (*PlaybookRun, error)
// GetPlaybookRunMetadata gets ancillary metadata about a playbook run.
GetPlaybookRunMetadata(playbookRunID string) (*Metadata, error)
// GetPlaybookRunsForChannelByUser get the playbookRuns associated with this channel and user.
GetPlaybookRunsForChannelByUser(channelID string, userID string) ([]PlaybookRun, error)
// GetOwners returns all the owners of playbook runs selected
GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error)
// IsOwner returns true if the userID is the owner for playbookRunID.
IsOwner(playbookRunID string, userID string) bool
// ChangeOwner processes a request from userID to change the owner for playbookRunID
// to ownerID. Changing to the same ownerID is a no-op.
ChangeOwner(playbookRunID string, userID string, ownerID string) error
// ModifyCheckedState modifies the state of the specified checklist item
// Idempotent, will not perform any actions if the checklist item is already in the specified state
ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber int, itemNumber int) error
// ToggleCheckedState checks or unchecks the specified checklist item
ToggleCheckedState(playbookRunID, userID string, checklistNumber, itemNumber int) error
// SetAssignee sets the assignee for the specified checklist item
// Idempotent, will not perform any actions if the checklist item is already assigned to assigneeID
SetAssignee(playbookRunID, userID, assigneeID string, checklistNumber, itemNumber int) error
// SetCommandToChecklistItem sets command to checklist item
SetCommandToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newCommand string) error
// SetDueDate sets absolute due date timestamp for the specified checklist item
SetDueDate(playbookRunID, userID string, duedate int64, checklistNumber, itemNumber int) error
// SetTaskActionsToChecklistItem sets Task Actions to checklist item
SetTaskActionsToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, taskActions []TaskAction) error
// RunChecklistItemSlashCommand executes the slash command associated with the specified checklist item.
RunChecklistItemSlashCommand(playbookRunID, userID string, checklistNumber, itemNumber int) (string, error)
// DuplicateChecklistItem duplicates the checklist item.
DuplicateChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error
// AddChecklistItem adds an item to the specified checklist
AddChecklistItem(playbookRunID, userID string, checklistNumber int, checklistItem ChecklistItem) error
// RemoveChecklistItem removes an item from the specified checklist
RemoveChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error
// DuplicateChecklist duplicates a checklist
DuplicateChecklist(playbookRunID, userID string, checklistNumber int) error
// SkipChecklist skips a checklist
SkipChecklist(playbookRunID, userID string, checklistNumber int) error
// RestoreChecklist restores a skipped checklist
RestoreChecklist(playbookRunID, userID string, checklistNumber int) error
// SkipChecklistItem removes an item from the specified checklist
SkipChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error
// RestoreChecklistItem restores a skipped item from the specified checklist
RestoreChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error
// EditChecklistItem changes the title, command and description of a specified checklist item.
EditChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int, newTitle, newCommand, newDescription string) error
// MoveChecklistItem moves a checklist item from one position to another.
MoveChecklist(playbookRunID, userID string, sourceChecklistIdx, destChecklistIdx int) error
// MoveChecklistItem moves a checklist item from one position to another.
MoveChecklistItem(playbookRunID, userID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error
// GetChecklistItemAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete
GetChecklistItemAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error)
// GetChecklistAutocomplete returns the list of checklists for playbookRuns to be used in autocomplete
GetChecklistAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error)
// GetRunsAutocomplete returns the list of runs to be used in autocomplete
GetRunsAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error)
// AddChecklist prepends a new checklist to the specified run
AddChecklist(playbookRunID, userID string, checklist Checklist) error
// RemoveChecklist removes the specified checklist.
RemoveChecklist(playbookRunID, userID string, checklistNumber int) error
// RenameChecklist renames the specified checklist
RenameChecklist(playbookRunID, userID string, checklistNumber int, newTitle string) error
// NukeDB removes all playbook run related data.
NukeDB() error
// SetReminder sets a reminder. After time.Now().Add(fromNow) in the future,
// the owner will be reminded to update the playbook run's status.
SetReminder(playbookRunID string, fromNow time.Duration) error
// RemoveReminder removes the pending reminder for playbookRunID (if any).
RemoveReminder(playbookRunID string)
// HandleReminder is the handler for all reminder events.
HandleReminder(key string)
// SetNewReminder sets a new reminder for playbookRunID, removes any pending reminder, removes the
// reminder post in the playbookRun's channel, and resets the PreviousReminder and
// LastStatusUpdateAt (so the countdown timer to "update due" shows the correct time)
SetNewReminder(playbookRunID string, newReminder time.Duration) error
// ResetReminder records an event for snoozing a reminder, then calls SetNewReminder to create
// the next reminder
ResetReminder(playbookRunID string, newReminder time.Duration) error
// ChangeCreationDate changes the creation date of the specified playbook run.
ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error
// UpdateRetrospective updates the retrospective for the given playbook run.
UpdateRetrospective(playbookRunID, userID string, retrospective RetrospectiveUpdate) error
// PublishRetrospective publishes the retrospective.
PublishRetrospective(playbookRunID, userID string, retrospective RetrospectiveUpdate) error
// CancelRetrospective cancels the retrospective.
CancelRetrospective(playbookRunID, userID string) error
// EphemeralPostTodoDigestToUser gathers the list of assigned tasks, participating runs, and overdue updates,
// and sends an ephemeral post to userID on channelID. Use force = true to post even if there are no items.
EphemeralPostTodoDigestToUser(userID string, channelID string, force bool, includeRunsInProgress bool) error
// DMTodoDigestToUser gathers the list of assigned tasks, participating runs, and overdue updates,
// and DMs the message to userID. Use force = true to DM even if there are no items.
DMTodoDigestToUser(userID string, force bool, includeRunsInProgress bool) error
// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID
GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error)
// GetParticipatingRuns returns the list of active runs with userID as participant
GetParticipatingRuns(userID string) ([]RunLink, error)
// GetOverdueUpdateRuns returns the list of userID's runs that have overdue updates
GetOverdueUpdateRuns(userID string) ([]RunLink, error)
// Follow method lets user follow a specific playbook run
Follow(playbookRunID, userID string) error
// UnFollow method lets user unfollow a specific playbook run
Unfollow(playbookRunID, userID string) error
// GetFollowers returns list of followers for a specific playbook run
GetFollowers(playbookRunID string) ([]string, error)
// RestorePlaybookRun reverts a run from the Finished state. If run was not in Finished state, the call is a noop.
RestorePlaybookRun(playbookRunID, userID string) error
// RequestUpdate posts a status update request message in the run's channel
RequestUpdate(playbookRunID, requesterID string) error
// RequestJoinChannel posts a channel-join request message in the run's channel
RequestJoinChannel(playbookRunID, requesterID string) error
// RemoveParticipants removes users from the run's participants
RemoveParticipants(playbookRunID string, userIDs []string, requesterUserID string) error
// AddParticipants adds users to the participants list
AddParticipants(playbookRunID string, userIDs []string, requesterUserID string, forceAddToChannel bool) error
// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following
GetPlaybookRunIDsForUser(userID string) ([]string, error)
// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs.
// Notice that order of passed ids and returned runs might not coincide
GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error)
// GetTaskMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs
GetTaskMetadataByIDs(taskIDs []string) ([]TopicMetadata, error)
// GetStatusMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs
GetStatusMetadataByIDs(statusIDs []string) ([]TopicMetadata, error)
// GraphqlUpdate taking a setmap for graphql
GraphqlUpdate(id string, setmap map[string]interface{}) error
// MessageHasBeenPosted checks posted messages for triggers that may trigger task actions
MessageHasBeenPosted(post *model.Post)
}
// PlaybookRunStore defines the methods the PlaybookRunServiceImpl needs from the interfaceStore.
type PlaybookRunStore interface {
// GetPlaybookRuns returns filtered playbook runs and the total count before paging.
GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error)
// CreatePlaybookRun creates a new playbook run. If playbook run has an ID, that ID will be used.
CreatePlaybookRun(playbookRun *PlaybookRun) (*PlaybookRun, error)
// UpdatePlaybookRun updates a playbook run.
UpdatePlaybookRun(playbookRun *PlaybookRun) (*PlaybookRun, error)
// GraphqlUpdate taking a setmap for graphql
GraphqlUpdate(id string, setmap map[string]interface{}) error
// UpdateStatus updates the status of a playbook run.
UpdateStatus(statusPost *SQLStatusPost) error
// FinishPlaybookRun finishes a run at endAt (in millis)
FinishPlaybookRun(playbookRunID string, endAt int64) error
// RestorePlaybookRun restores a run at restoreAt (in millis)
RestorePlaybookRun(playbookRunID string, restoreAt int64) error
// GetTimelineEvent returns the timeline event for playbookRunID by the timeline event ID.
GetTimelineEvent(playbookRunID, eventID string) (*TimelineEvent, error)
// CreateTimelineEvent inserts the timeline event into the DB and returns the new event ID
CreateTimelineEvent(event *TimelineEvent) (*TimelineEvent, error)
// UpdateTimelineEvent updates an existing timeline event
UpdateTimelineEvent(event *TimelineEvent) error
// GetPlaybookRun gets a playbook run by ID.
GetPlaybookRun(playbookRunID string) (*PlaybookRun, error)
// GetPlaybookRunIDsForChannel gets a playbook runs list associated with the given channel id.
GetPlaybookRunIDsForChannel(channelID string) ([]string, error)
// GetHistoricalPlaybookRunParticipantsCount returns the count of all participants of the
// playbook run associated with the given channel id since the beginning of the
// playbook run, excluding bots.
GetHistoricalPlaybookRunParticipantsCount(channelID string) (int64, error)
// GetOwners returns the owners of the playbook runs selected by options
GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error)
// NukeDB removes all playbook run related data.
NukeDB() error
// ChangeCreationDate changes the creation date of the specified playbook run.
ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error
// GetBroadcastChannelIDsToRootIDs takes a playbookRunID and returns the mapping of
// broadcastChannelID->rootID (to keep track of the status updates thread in each of the
// playbook's broadcast channels).
GetBroadcastChannelIDsToRootIDs(playbookRunID string) (map[string]string, error)
// SetBroadcastChannelIDsToRootID sets the broadcastChannelID->rootID mappings for playbookRunID
SetBroadcastChannelIDsToRootID(playbookRunID string, channelIDsToRootIDs map[string]string) error
// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID
GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error)
// GetParticipatingRuns returns the list of active runs with userID as a participant
GetParticipatingRuns(userID string) ([]RunLink, error)
// GetOverdueUpdateRuns returns the list of runs that userID is participating in that have overdue updates
GetOverdueUpdateRuns(userID string) ([]RunLink, error)
// Follow method lets user follow a specific playbook run
Follow(playbookRunID, userID string) error
// UnFollow method lets user unfollow a specific playbook run
Unfollow(playbookRunID, userID string) error
// GetFollowers returns list of followers for a specific playbook run
GetFollowers(playbookRunID string) ([]string, error)
// GetRunsActiveTotal returns number of active runs
GetRunsActiveTotal() (int64, error)
// GetOverdueUpdateRunsTotal returns number of runs that have overdue status updates
GetOverdueUpdateRunsTotal() (int64, error)
// GetOverdueRetroRunsTotal returns the number of completed runs without retro and with reminder
GetOverdueRetroRunsTotal() (int64, error)
// GetFollowersActiveTotal returns total number of active followers, including duplicates
// if a user is following more than one run, it will be counted multiple times
GetFollowersActiveTotal() (int64, error)
// GetParticipantsActiveTotal returns number of active participants
// (i.e. members of the playbook run channel when the run is active)
// if a user is member of more than one channel, it will be counted multiple times
GetParticipantsActiveTotal() (int64, error)
// AddParticipants adds particpants to the run
AddParticipants(playbookRunID string, userIDs []string) error
// RemoveParticipants removes participants from the run
RemoveParticipants(playbookRunID string, userIDs []string) error
// GetSchemeRolesForChannel scheme role ids for the channel
GetSchemeRolesForChannel(channelID string) (string, string, string, error)
// GetSchemeRolesForTeam scheme role ids for the team
GetSchemeRolesForTeam(teamID string) (string, string, string, error)
// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following
GetPlaybookRunIDsForUser(userID string) ([]string, error)
// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs.
// Notice that order of passed ids and returned runs might not coincide
GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error)
// GetTaskAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs
GetTaskAsTopicMetadataByIDs(taskIDs []string) ([]TopicMetadata, error)
// GetStatusAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs
GetStatusAsTopicMetadataByIDs(statusIDs []string) ([]TopicMetadata, error)
}
// PlaybookRunTelemetry defines the methods that the PlaybookRunServiceImpl needs from the RudderTelemetry.
// Unless otherwise noted, userID is the user initiating the event.
type PlaybookRunTelemetry interface {
// CreatePlaybookRun tracks the creation of a new playbook run.
CreatePlaybookRun(playbookRun *PlaybookRun, userID string, public bool)
// FinishPlaybookRun tracks the end of a playbook run.
FinishPlaybookRun(playbookRun *PlaybookRun, userID string)
// RestorePlaybookRun tracks the restoration of a playbook run.
RestorePlaybookRun(playbookRun *PlaybookRun, userID string)
// RestartPlaybookRun tracks the restart of a playbook run.
RestartPlaybookRun(playbookRun *PlaybookRun, userID string)
// ChangeOwner tracks changes in owner.
ChangeOwner(playbookRun *PlaybookRun, userID string)
// UpdateStatus tracks when a playbook run's status has been updated
UpdateStatus(playbookRun *PlaybookRun, userID string)
// FrontendTelemetryForPlaybookRun tracks an event originating from the frontend
FrontendTelemetryForPlaybookRun(playbookRun *PlaybookRun, userID, action string)
// AddPostToTimeline tracks userID creating a timeline event from a post.
AddPostToTimeline(playbookRun *PlaybookRun, userID string)
// RemoveTimelineEvent tracks userID removing a timeline event.
RemoveTimelineEvent(playbookRun *PlaybookRun, userID string)
// ModifyCheckedState tracks the checking and unchecking of items.
ModifyCheckedState(playbookRunID, userID string, task ChecklistItem, wasOwner bool)
// SetAssignee tracks the changing of an assignee on an item.
SetAssignee(playbookRunID, userID string, task ChecklistItem)
// AddTask tracks the creation of a new checklist item.
AddTask(playbookRunID, userID string, task ChecklistItem)
// RemoveTask tracks the removal of a checklist item.
RemoveTask(playbookRunID, userID string, task ChecklistItem)
// SkipChecklist tracks the skipping of a checklist.
SkipChecklist(playbookRunID, userID string, checklist Checklist)
// RestoreChecklist tracks the restoring of a checklist.
RestoreChecklist(playbookRunID, userID string, checklist Checklist)
// SkipTask tracks the skipping of a checklist item.
SkipTask(playbookRunID, userID string, task ChecklistItem)
// RestoreTask tracks the restoring of a checklist item.
RestoreTask(playbookRunID, userID string, task ChecklistItem)
// RenameTask tracks the update of a checklist item.
RenameTask(playbookRunID, userID string, task ChecklistItem)
// MoveChecklist tracks the movement of a checklist
MoveChecklist(playbookRunID, userID string, task Checklist)
// MoveTask tracks the movement of a checklist item
MoveTask(playbookRunID, userID string, task ChecklistItem)
// RunTaskSlashCommand tracks the execution of a slash command attached to
// a checklist item.
RunTaskSlashCommand(playbookRunID, userID string, task ChecklistItem)
// AddChecklsit tracks the creation of a new checklist.
AddChecklist(playbookRunID, userID string, checklist Checklist)
// RemoveChecklist tracks the removal of a checklist.
RemoveChecklist(playbookRunID, userID string, checklist Checklist)
// RenameChecklsit tracks the creation of a new checklist.
RenameChecklist(playbookRunID, userID string, checklist Checklist)
// UpdateRetrospective event
UpdateRetrospective(playbookRun *PlaybookRun, userID string)
// PublishRetrospective event
PublishRetrospective(playbookRun *PlaybookRun, userID string)
// Follow tracks userID following a playbook run.
Follow(playbookRun *PlaybookRun, userID string)
// Unfollow tracks userID following a playbook run.
Unfollow(playbookRun *PlaybookRun, userID string)
// RunAction tracks the run actions, i.e., status broadcast action
RunAction(playbookRun *PlaybookRun, userID, triggerType, actionType string, numBroadcasts int)
}
type JobOnceScheduler interface {
Start() error
SetCallback(callback func(string)) error
ListScheduledJobs() ([]cluster.JobOnceMetadata, error)
ScheduleOnce(key string, runAt time.Time) (*cluster.JobOnce, error)
Cancel(key string)
}
const PerPageDefault = 1000
// PlaybookRunFilterOptions specifies the optional parameters when getting playbook runs.
type PlaybookRunFilterOptions struct {
// Gets all the headers with this TeamID.
TeamID string `url:"team_id,omitempty"`
// Pagination options.
Page int `url:"page,omitempty"`
PerPage int `url:"per_page,omitempty"`
// Sort sorts by this header field in json format (eg, "create_at", "end_at", "name", etc.);
// defaults to "create_at".
Sort SortField `url:"sort,omitempty"`
// Direction orders by ascending or descending, defaulting to ascending.
Direction SortDirection `url:"direction,omitempty"`
// Statuses filters by all statuses in the list (inclusive)
Statuses []string
// OwnerID filters by owner's Mattermost user ID. Defaults to blank (no filter).
OwnerID string `url:"owner_user_id,omitempty"`
// ParticipantID filters playbook runs that have this member. Defaults to blank (no filter).
ParticipantID string `url:"participant_id,omitempty"`
// ParticipantOrFollowerID filters playbook runs that have this user as member or as follower. Defaults to blank (no filter).
ParticipantOrFollowerID string `url:"participant_or_follower,omitempty"`
// IncludeFavorites filters playbook runs that ParticipantOrFollowerID has marked as favorite.
// There's no impact if ParticipantOrFollowerID is empty.
IncludeFavorites bool `url:"include_favorites,omitempty"`
// SearchTerm returns results of the search term and respecting the other header filter options.
// The search term acts as a filter and respects the Sort and Direction fields (i.e., results are
// not returned in relevance order).
SearchTerm string `url:"search_term,omitempty"`
// PlaybookID filters playbook runs that are derived from this playbook id.
// Defaults to blank (no filter).
PlaybookID string `url:"playbook_id,omitempty"`
// ActiveGTE filters playbook runs that were active after (or equal) to the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
ActiveGTE int64 `url:"active_gte,omitempty"`
// ActiveLT filters playbook runs that were active before the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
ActiveLT int64 `url:"active_lt,omitempty"`
// StartedGTE filters playbook runs that were started after (or equal) to the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
StartedGTE int64 `url:"started_gte,omitempty"`
// StartedLT filters playbook runs that were started before the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
StartedLT int64 `url:"started_lt,omitempty"`
// ChannelID filters to playbook runs that are associated with the given channel ID
ChannelID string `url:"channel_id,omitempty"`
// Types filters by all run types in the list (inclusive)
Types []string
}
// Clone duplicates the given options.
func (o *PlaybookRunFilterOptions) Clone() PlaybookRunFilterOptions {
newPlaybookRunFilterOptions := *o
if len(o.Statuses) > 0 {
newPlaybookRunFilterOptions.Statuses = append([]string{}, o.Statuses...)
}
if len(o.Types) > 0 {
newPlaybookRunFilterOptions.Types = append([]string{}, o.Types...)
}
return newPlaybookRunFilterOptions
}
// Validate returns a new, validated filter options or returns an error if invalid.
func (o PlaybookRunFilterOptions) Validate() (PlaybookRunFilterOptions, error) {
options := o.Clone()
if options.PerPage <= 0 {
options.PerPage = PerPageDefault
}
options.Sort = SortField(strings.ToLower(string(options.Sort)))
switch options.Sort {
case SortByCreateAt:
case SortByID:
case SortByName:
case SortByOwnerUserID:
case SortByTeamID:
case SortByEndAt:
case SortByStatus:
case SortByLastStatusUpdateAt:
case SortByMetric0, SortByMetric1, SortByMetric2, SortByMetric3:
case "": // default
options.Sort = SortByCreateAt
default:
return PlaybookRunFilterOptions{}, errors.Errorf("unsupported sort '%s'", options.Sort)
}
options.Direction = SortDirection(strings.ToUpper(string(options.Direction)))
switch options.Direction {
case DirectionAsc:
case DirectionDesc:
case "": //default
options.Direction = DirectionAsc
default:
return PlaybookRunFilterOptions{}, errors.Errorf("unsupported direction '%s'", options.Direction)
}
if options.TeamID != "" && !model.IsValidId(options.TeamID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'team_id': must be 26 characters or blank")
}
if options.OwnerID != "" && !model.IsValidId(options.OwnerID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'owner_id': must be 26 characters or blank")
}
if options.ParticipantID != "" && !model.IsValidId(options.ParticipantID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'participant_id': must be 26 characters or blank")
}
if options.ParticipantOrFollowerID != "" && !model.IsValidId(options.ParticipantOrFollowerID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'participant_or_follower_id': must be 26 characters or blank")
}
if options.PlaybookID != "" && !model.IsValidId(options.PlaybookID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'playbook_id': must be 26 characters or blank")
}
if options.ActiveGTE < 0 {
options.ActiveGTE = 0
}
if options.ActiveLT < 0 {
options.ActiveLT = 0
}
if options.StartedGTE < 0 {
options.StartedGTE = 0
}
if options.StartedLT < 0 {
options.StartedLT = 0
}
if options.ChannelID != "" && !model.IsValidId(options.ChannelID) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter 'channel_id': must be 26 characters or blank")
}
for _, s := range options.Statuses {
if !validStatus(s) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter in 'statuses': must be InProgress or Finished")
}
}
for _, t := range options.Types {
if !validType(t) {
return PlaybookRunFilterOptions{}, errors.New("bad parameter in 'types': must be playbook or channel")
}
}
return options, nil
}
func validStatus(status string) bool {
return status == "" || status == StatusInProgress || status == StatusFinished
}
func validType(runType string) bool {
return runType == RunTypePlaybook || runType == RunTypeChannelChecklist
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
stripmd "github.com/writeas/go-strip-markdown"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/httptools"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/metrics"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/timeutils"
)
const checklistItemDescriptionCharLimit = 4000
const (
// PlaybookRunCreatedWSEvent is for playbook run creation.
PlaybookRunCreatedWSEvent = "playbook_run_created"
playbookRunUpdatedWSEvent = "playbook_run_updated"
noAssigneeName = "No Assignee"
)
// PlaybookRunServiceImpl holds the information needed by the PlaybookRunService's methods to complete their functions.
type PlaybookRunServiceImpl struct {
httpClient *http.Client
configService config.Service
store PlaybookRunStore
poster bot.Poster
scheduler JobOnceScheduler
telemetry PlaybookRunTelemetry
genericTelemetry GenericTelemetry
api playbooks.ServicesAPI
playbookService PlaybookService
actionService ChannelActionService
permissions *PermissionsService
licenseChecker LicenseChecker
metricsService *metrics.Metrics
}
var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`)
// DialogFieldPlaybookIDKey is the key for the playbook ID field used in OpenCreatePlaybookRunDialog.
const DialogFieldPlaybookIDKey = "playbookID"
// DialogFieldNameKey is the key for the playbook run name field used in OpenCreatePlaybookRunDialog.
const DialogFieldNameKey = "playbookRunName"
// DialogFieldDescriptionKey is the key for the description textarea field used in UpdatePlaybookRunDialog
const DialogFieldDescriptionKey = "description"
// DialogFieldMessageKey is the key for the message textarea field used in UpdatePlaybookRunDialog
const DialogFieldMessageKey = "message"
// DialogFieldReminderInSecondsKey is the key for the reminder select field used in UpdatePlaybookRunDialog
const DialogFieldReminderInSecondsKey = "reminder"
// DialogFieldFinishRun is the key for the "Finish run" bool field used in UpdatePlaybookRunDialog
const DialogFieldFinishRun = "finish_run"
// DialogFieldPlaybookRunKey is the key for the playbook run chosen in AddToTimelineDialog
const DialogFieldPlaybookRunKey = "playbook_run"
// DialogFieldSummary is the key for the summary in AddToTimelineDialog
const DialogFieldSummary = "summary"
// DialogFieldItemName is the key for the playbook run name in AddChecklistItemDialog
const DialogFieldItemNameKey = "name"
// DialogFieldDescriptionKey is the key for the description in AddChecklistItemDialog
const DialogFieldItemDescriptionKey = "description"
// DialogFieldCommandKey is the key for the command in AddChecklistItemDialog
const DialogFieldItemCommandKey = "command"
// NewPlaybookRunService creates a new PlaybookRunServiceImpl.
func NewPlaybookRunService(
store PlaybookRunStore,
poster bot.Poster,
configService config.Service,
scheduler JobOnceScheduler,
telemetry PlaybookRunTelemetry,
genericTelemetry GenericTelemetry,
api playbooks.ServicesAPI,
playbookService PlaybookService,
channelActionService ChannelActionService,
licenseChecker LicenseChecker,
metricsService *metrics.Metrics,
) *PlaybookRunServiceImpl {
service := &PlaybookRunServiceImpl{
store: store,
poster: poster,
configService: configService,
scheduler: scheduler,
telemetry: telemetry,
genericTelemetry: genericTelemetry,
httpClient: httptools.MakeClient(api),
api: api,
playbookService: playbookService,
actionService: channelActionService,
licenseChecker: licenseChecker,
metricsService: metricsService,
}
service.permissions = NewPermissionsService(service.playbookService, service, api, service.configService, service.licenseChecker)
return service
}
// GetPlaybookRuns returns filtered playbook runs and the total count before paging.
func (s *PlaybookRunServiceImpl) GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error) {
return s.store.GetPlaybookRuns(requesterInfo, options)
}
func (s *PlaybookRunServiceImpl) buildPlaybookRunCreationMessageTemplate(playbookTitle, playbookID string, playbookRun *PlaybookRun, reporter *model.User) (string, error) {
return fmt.Sprintf(
"##### [%s](%s%s)\n@%s ran the [%s](%s) playbook.",
playbookRun.Name,
GetRunDetailsRelativeURL(playbookRun.ID),
"%s", // for the telemetry data injection
reporter.Username,
playbookTitle,
GetPlaybookDetailsRelativeURL(playbookID),
), nil
}
// PlaybookRunWebhookPayload is the body of the payload sent via playbook run webhooks.
type PlaybookRunWebhookPayload struct {
PlaybookRun
// ChannelURL is the absolute URL of the playbook run channel.
ChannelURL string `json:"channel_url"`
// DetailsURL is the absolute URL of the playbook run overview page.
DetailsURL string `json:"details_url"`
// Event is metadata concerning the event that triggered this webhook.
Event PlaybookRunWebhookEvent `json:"event"`
}
type PlaybookRunWebhookEvent struct {
// Type is the type of event emitted.
Type timelineEventType `json:"type"`
// At is the time when the event occurred.
At int64 `json:"at"`
// UserId is the user who triggered the event.
UserID string `json:"user_id"`
// Payload is optional, event-specific metadata.
Payload interface{} `json:"payload"`
}
// sendWebhooksOnCreation sends a POST request to the creation webhook URL.
// It blocks until a response is received.
func (s *PlaybookRunServiceImpl) sendWebhooksOnCreation(playbookRun PlaybookRun) {
siteURL := s.api.GetConfig().ServiceSettings.SiteURL
if siteURL == nil {
logrus.Error("cannot send webhook on creation, please set siteURL")
return
}
team, err := s.api.GetTeam(playbookRun.TeamID)
if err != nil {
logrus.WithError(err).Error("cannot send webhook on creation, not able to get playbookRun.TeamID")
return
}
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
logrus.WithError(err).Error("cannot send webhook on creation, not able to get playbookRun.ChannelID")
return
}
channelURL := getChannelURL(*siteURL, team.Name, channel.Name)
detailsURL := getRunDetailsURL(*siteURL, playbookRun.ID)
event := PlaybookRunWebhookEvent{
Type: PlaybookRunCreated,
At: playbookRun.CreateAt,
UserID: playbookRun.ReporterUserID,
}
payload := PlaybookRunWebhookPayload{
PlaybookRun: playbookRun,
ChannelURL: channelURL,
DetailsURL: detailsURL,
Event: event,
}
body, err := json.Marshal(payload)
if err != nil {
logrus.WithError(err).Error("cannot send webhook on creation, unable to marshal payload")
return
}
triggerWebhooks(s, playbookRun.WebhookOnCreationURLs, body)
}
// CreatePlaybookRun creates a new playbook run. userID is the user who initiated the CreatePlaybookRun.
func (s *PlaybookRunServiceImpl) CreatePlaybookRun(playbookRun *PlaybookRun, pb *Playbook, userID string, public bool) (*PlaybookRun, error) {
if playbookRun.DefaultOwnerID != "" {
// Check if the user is a member of the team to which the playbook run belongs.
if !IsMemberOfTeam(playbookRun.DefaultOwnerID, playbookRun.TeamID, s.api) {
logrus.WithFields(logrus.Fields{
"user_id": playbookRun.DefaultOwnerID,
"team_id": playbookRun.TeamID,
}).Warn("default owner specified, but it is not a member of the playbook run's team")
} else {
playbookRun.OwnerUserID = playbookRun.DefaultOwnerID
}
}
playbookRun.ReporterUserID = userID
playbookRun.ID = model.NewId()
logger := logrus.WithField("playbook_run_id", playbookRun.ID)
var err error
var channel *model.Channel
if playbookRun.ChannelID == "" {
header := "This channel was created as part of a playbook run. To view more information, select the shield icon then select *Tasks* or *Overview*."
if pb != nil {
overviewURL := GetRunDetailsRelativeURL(playbookRun.ID)
playbookURL := GetPlaybookDetailsRelativeURL(pb.ID)
header = fmt.Sprintf("This channel was created as part of the [%s](%s) playbook. Visit [the overview page](%s) for more information.",
pb.Title, playbookURL, overviewURL)
}
channel, err = s.createPlaybookRunChannel(playbookRun, header, public)
if err != nil {
return nil, err
}
playbookRun.ChannelID = channel.Id
} else {
channel, err = s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
return nil, err
}
}
if pb != nil && pb.ChannelMode == PlaybookRunCreateNewChannel && playbookRun.Name == "" {
playbookRun.Name = pb.ChannelNameTemplate
}
if pb != nil && pb.MessageOnJoinEnabled && pb.MessageOnJoin != "" {
welcomeAction := GenericChannelAction{
GenericChannelActionWithoutPayload: GenericChannelActionWithoutPayload{
ChannelID: playbookRun.ChannelID,
Enabled: true,
ActionType: ActionTypeWelcomeMessage,
TriggerType: TriggerTypeNewMemberJoins,
},
Payload: WelcomeMessagePayload{
Message: pb.MessageOnJoin,
},
}
if _, err = s.actionService.Create(welcomeAction); err != nil {
logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("unable to create welcome action for new run in channel")
}
}
if pb != nil && pb.CategorizeChannelEnabled && pb.CategoryName != "" {
categorizeChannelAction := GenericChannelAction{
GenericChannelActionWithoutPayload: GenericChannelActionWithoutPayload{
ChannelID: playbookRun.ChannelID,
Enabled: true,
ActionType: ActionTypeCategorizeChannel,
TriggerType: TriggerTypeNewMemberJoins,
},
Payload: CategorizeChannelPayload{
CategoryName: pb.CategoryName,
},
}
if _, err = s.actionService.Create(categorizeChannelAction); err != nil {
logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("unable to create welcome action for new run in channel")
}
}
now := model.GetMillis()
playbookRun.CreateAt = now
playbookRun.LastStatusUpdateAt = now
playbookRun.CurrentStatus = StatusInProgress
// Start with a blank playbook with one empty checklist if one isn't provided
if playbookRun.PlaybookID == "" {
playbookRun.Checklists = []Checklist{
{
Title: "Checklist",
Items: []ChecklistItem{},
},
}
}
playbookRun, err = s.store.CreatePlaybookRun(playbookRun)
if err != nil {
return nil, errors.Wrap(err, "failed to create playbook run")
}
s.telemetry.CreatePlaybookRun(playbookRun, userID, public)
s.metricsService.IncrementRunsCreatedCount(1)
err = s.addPlaybookRunInitialMemberships(playbookRun, channel)
if err != nil {
return nil, errors.Wrap(err, "failed to setup core memberships at run/channel")
}
invitedUserIDs := playbookRun.InvitedUserIDs
for _, groupID := range playbookRun.InvitedGroupIDs {
groupLogger := logger.WithField("group_id", groupID)
var group *model.Group
group, err = s.api.GetGroup(groupID)
if err != nil {
groupLogger.WithError(err).Error("failed to query group")
continue
}
if !group.AllowReference {
groupLogger.Warn("group that does not allow references")
continue
}
perPage := 1000
for page := 0; ; page++ {
var users []*model.User
users, err = s.api.GetGroupMemberUsers(groupID, page, perPage)
if err != nil {
groupLogger.WithError(err).Error("failed to query group")
break
}
for _, user := range users {
invitedUserIDs = append(invitedUserIDs, user.Id)
}
if len(users) < perPage {
break
}
}
}
err = s.AddParticipants(playbookRun.ID, invitedUserIDs, s.configService.GetConfiguration().BotUserID, false)
if err != nil {
logrus.WithError(err).WithFields(map[string]any{
"playbookRunId": playbookRun.ID,
"invitedUserIDs": invitedUserIDs,
}).Warn("failed to add invited users on playbook run creation")
}
if len(invitedUserIDs) > 0 {
s.genericTelemetry.Track(
telemetryRunParticipate,
map[string]any{
"count": len(invitedUserIDs),
"trigger": "invite_on_create",
"playbookrun_id": playbookRun.ID,
},
)
}
var reporter *model.User
reporter, err = s.api.GetUserByID(playbookRun.ReporterUserID)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve user %s", playbookRun.ReporterUserID)
}
// Do we send a DM to the new owner?
if playbookRun.OwnerUserID != playbookRun.ReporterUserID {
startMessage := fmt.Sprintf("You have been assigned ownership of the run: [%s](%s), reported by @%s.",
playbookRun.Name, GetRunDetailsRelativeURL(playbookRun.ID), reporter.Username)
if err = s.poster.DM(playbookRun.OwnerUserID, &model.Post{Message: startMessage}); err != nil {
return nil, errors.Wrapf(err, "failed to send DM on CreatePlaybookRun")
}
}
if pb != nil {
var messageTemplate string
messageTemplate, err = s.buildPlaybookRunCreationMessageTemplate(pb.Title, pb.ID, playbookRun, reporter)
if err != nil {
return nil, errors.Wrapf(err, "failed to build the playbook run creation message")
}
if playbookRun.StatusUpdateBroadcastChannelsEnabled {
s.broadcastPlaybookRunMessageToChannels(playbookRun.BroadcastChannelIDs, &model.Post{Message: fmt.Sprintf(messageTemplate, "")}, creationMessage, playbookRun, logger)
s.telemetry.RunAction(playbookRun, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRun.BroadcastChannelIDs))
}
// dm to users who are auto-following the playbook
telemetryString := fmt.Sprintf("?telem_action=follower_clicked_run_started_dm&telem_run_id=%s", playbookRun.ID)
err = s.dmPostToAutoFollows(&model.Post{Message: fmt.Sprintf(messageTemplate, telemetryString)}, pb.ID, playbookRun.ID, userID)
if err != nil {
logger.WithError(err).Error("failed to dm post to auto follows")
}
}
event := &TimelineEvent{
PlaybookRunID: playbookRun.ID,
CreateAt: playbookRun.CreateAt,
EventAt: playbookRun.CreateAt,
EventType: PlaybookRunCreated,
SubjectUserID: playbookRun.ReporterUserID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return playbookRun, errors.Wrap(err, "failed to create timeline event")
}
playbookRun.TimelineEvents = append(playbookRun.TimelineEvents, *event)
//auto-follow playbook run
if pb != nil {
var autoFollows []string
autoFollows, err = s.playbookService.GetAutoFollows(pb.ID)
if err != nil {
return playbookRun, errors.Wrapf(err, "failed to get autoFollows of the playbook `%s`", pb.ID)
}
for _, autoFollow := range autoFollows {
if err = s.Follow(playbookRun.ID, autoFollow); err != nil {
logger.WithError(err).WithFields(logrus.Fields{
"playbook_run_id": playbookRun.ID,
"auto_follow": autoFollow,
}).Warn("failed to follow the playbook run")
}
}
}
if len(playbookRun.WebhookOnCreationURLs) != 0 {
s.sendWebhooksOnCreation(*playbookRun)
}
if playbookRun.PostID == "" {
return playbookRun, nil
}
// Post the content and link of the original post
post, err := s.api.GetPost(playbookRun.PostID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get original post")
}
postURL := fmt.Sprintf("/_redirect/pl/%s", playbookRun.PostID)
postMessage := fmt.Sprintf("[Original Post](%s)\n > %s", postURL, post.Message)
_, err = s.poster.PostMessage(channel.Id, postMessage)
if err != nil {
return nil, errors.Wrapf(err, "failed to post to channel")
}
return playbookRun, nil
}
func (s *PlaybookRunServiceImpl) failedInvitedUserActions(usersFailedToInvite []string, channel *model.Channel) {
if len(usersFailedToInvite) == 0 {
return
}
usernames := make([]string, 0, len(usersFailedToInvite))
numDeletedUsers := 0
for _, userID := range usersFailedToInvite {
user, userErr := s.api.GetUserByID(userID)
if userErr != nil {
// User does not exist anymore
numDeletedUsers++
continue
}
usernames = append(usernames, "@"+user.Username)
}
deletedUsersMsg := ""
if numDeletedUsers > 0 {
deletedUsersMsg = fmt.Sprintf(" %d users from the original list have been deleted since the creation of the playbook.", numDeletedUsers)
}
if _, err := s.poster.PostMessage(channel.Id, "Failed to invite the following users: %s. %s", strings.Join(usernames, ", "), deletedUsersMsg); err != nil {
logrus.WithError(err).Error("failedInvitedUserActions: failed to post to channel")
}
}
// OpenCreatePlaybookRunDialog opens a interactive dialog to start a new playbook run.
func (s *PlaybookRunServiceImpl) OpenCreatePlaybookRunDialog(teamID, requesterID, triggerID, postID, clientID string, playbooks []Playbook) error {
filteredPlaybooks := make([]Playbook, 0, len(playbooks))
for _, playbook := range playbooks {
if err := s.permissions.RunCreate(requesterID, playbook); err == nil {
filteredPlaybooks = append(filteredPlaybooks, playbook)
}
}
dialog, err := s.newPlaybookRunDialog(teamID, requesterID, postID, clientID, filteredPlaybooks)
if err != nil {
return errors.Wrapf(err, "failed to create new playbook run dialog")
}
dialogRequest := model.OpenDialogRequest{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/dialog",
"playbooks"),
Dialog: *dialog,
TriggerId: triggerID,
}
if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil {
return errors.Wrapf(err, "failed to open new playbook run dialog")
}
return nil
}
func (s *PlaybookRunServiceImpl) OpenUpdateStatusDialog(playbookRunID, userID, triggerID string) error {
currentPlaybookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
user, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
message := ""
newestPostID := findNewestNonDeletedPostID(currentPlaybookRun.StatusPosts)
if newestPostID != "" {
var post *model.Post
post, err = s.api.GetPost(newestPostID)
if err != nil {
return errors.Wrap(err, "failed to find newest post")
}
message = post.Message
} else {
message = currentPlaybookRun.ReminderMessageTemplate
}
dialog, err := s.newUpdatePlaybookRunDialog(currentPlaybookRun.Summary, message, len(currentPlaybookRun.BroadcastChannelIDs), currentPlaybookRun.PreviousReminder, user.Locale)
if err != nil {
return errors.Wrap(err, "failed to create update status dialog")
}
dialogRequest := model.OpenDialogRequest{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/update-status-dialog",
"playbooks",
playbookRunID),
Dialog: *dialog,
TriggerId: triggerID,
}
if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil {
return errors.Wrap(err, "failed to open update status dialog")
}
return nil
}
func (s *PlaybookRunServiceImpl) OpenAddToTimelineDialog(requesterInfo RequesterInfo, postID, teamID, triggerID string) error {
options := PlaybookRunFilterOptions{
TeamID: teamID,
ParticipantID: requesterInfo.UserID,
Sort: SortByCreateAt,
Direction: DirectionDesc,
Types: []string{RunTypePlaybook},
Page: 0,
PerPage: PerPageDefault,
}
result, err := s.GetPlaybookRuns(requesterInfo, options)
if err != nil {
return errors.Wrap(err, "Error retrieving the playbook runs: %v")
}
dialog, err := s.newAddToTimelineDialog(result.Items, postID, requesterInfo.UserID)
if err != nil {
return errors.Wrap(err, "failed to create add to timeline dialog")
}
dialogRequest := model.OpenDialogRequest{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/add-to-timeline-dialog",
"playbooks"),
Dialog: *dialog,
TriggerId: triggerID,
}
if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil {
return errors.Wrap(err, "failed to open update status dialog")
}
return nil
}
func (s *PlaybookRunServiceImpl) OpenAddChecklistItemDialog(triggerID, userID, playbookRunID string, checklist int) error {
user, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
T := i18n.GetUserTranslations(user.Locale)
dialog := &model.Dialog{
Title: T("app.user.run.add_checklist_item.title"),
Elements: []model.DialogElement{
{
DisplayName: T("app.user.run.add_checklist_item.name"),
Name: DialogFieldItemNameKey,
Type: "text",
Default: "",
},
{
DisplayName: T("app.user.run.add_checklist_item.description"),
Name: DialogFieldItemDescriptionKey,
Type: "textarea",
Default: "",
Optional: true,
MaxLength: checklistItemDescriptionCharLimit,
},
},
SubmitLabel: T("app.user.run.add_checklist_item.submit_label"),
NotifyOnCancel: false,
}
dialogRequest := model.OpenDialogRequest{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/checklists/%v/add-dialog",
"playbooks", playbookRunID, checklist),
Dialog: *dialog,
TriggerId: triggerID,
}
if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil {
return errors.Wrap(err, "failed to open update status dialog")
}
return nil
}
func (s *PlaybookRunServiceImpl) AddPostToTimeline(playbookRunID, userID, postID, summary string) error {
post, err := s.api.GetPost(postID)
if err != nil {
return errors.Wrap(err, "failed to find post")
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: model.GetMillis(),
DeleteAt: 0,
EventAt: post.CreateAt,
EventType: EventFromPost,
Summary: summary,
Details: "",
PostID: postID,
SubjectUserID: post.UserId,
CreatorUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
playbookRunModified, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.AddPostToTimeline(playbookRunModified, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// RemoveTimelineEvent removes the timeline event (sets the DeleteAt to the current time).
func (s *PlaybookRunServiceImpl) RemoveTimelineEvent(playbookRunID, userID, eventID string) error {
event, err := s.store.GetTimelineEvent(playbookRunID, eventID)
if err != nil {
return err
}
event.DeleteAt = model.GetMillis()
if err = s.store.UpdateTimelineEvent(event); err != nil {
return err
}
playbookRunModified, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.RemoveTimelineEvent(playbookRunModified, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
func (s *PlaybookRunServiceImpl) buildStatusUpdatePost(statusUpdate, playbookRunID, authorID string) (*model.Post, error) {
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve playbook run for id '%s'", playbookRunID)
}
authorUser, err := s.api.GetUserByID(authorID)
if err != nil {
return nil, errors.Wrapf(err, "error when trying to get the author user with ID '%s'", authorID)
}
numTasks := 0
numTasksChecked := 0
for _, checklist := range playbookRun.Checklists {
numTasks += len(checklist.Items)
for _, task := range checklist.Items {
if task.State == ChecklistItemStateClosed {
numTasksChecked++
}
}
}
return &model.Post{
Message: statusUpdate,
Type: "custom_run_update",
Props: map[string]interface{}{
"numTasksChecked": numTasksChecked,
"numTasks": numTasks,
"participantIds": playbookRun.ParticipantIDs,
"authorUsername": authorUser.Username,
"playbookRunId": playbookRun.ID,
"runName": playbookRun.Name,
},
}, nil
}
// sendWebhooksOnUpdateStatus sends a POST request to the status update webhook URL.
// It blocks until a response is received.
func (s *PlaybookRunServiceImpl) sendWebhooksOnUpdateStatus(playbookRunID string, event *PlaybookRunWebhookEvent) {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
logger.WithError(err).Error("cannot send webhook on update, not able to get playbookRun")
return
}
siteURL := s.api.GetConfig().ServiceSettings.SiteURL
if siteURL == nil {
logger.Error("cannot send webhook on update, please set siteURL")
return
}
team, err := s.api.GetTeam(playbookRun.TeamID)
if err != nil {
logger.WithField("team_id", playbookRun.TeamID).Error("cannot send webhook on update, not able to get playbookRun.TeamID")
return
}
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
logger.WithField("channel_id", playbookRun.ChannelID).Error("cannot send webhook on update, not able to get playbookRun.ChannelID")
return
}
channelURL := getChannelURL(*siteURL, team.Name, channel.Name)
detailsURL := getRunDetailsURL(*siteURL, playbookRun.ID)
payload := PlaybookRunWebhookPayload{
PlaybookRun: *playbookRun,
ChannelURL: channelURL,
DetailsURL: detailsURL,
Event: *event,
}
body, err := json.Marshal(payload)
if err != nil {
logger.WithError(err).Error("cannot send webhook on update, unable to marshal payload")
return
}
triggerWebhooks(s, playbookRun.WebhookOnStatusUpdateURLs, body)
}
// UpdateStatus updates a playbook run's status.
func (s *PlaybookRunServiceImpl) UpdateStatus(playbookRunID, userID string, options StatusUpdateOptions) error {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
originalPost, err := s.buildStatusUpdatePost(options.Message, playbookRunID, userID)
if err != nil {
return err
}
originalPost.ChannelId = playbookRunToModify.ChannelID
channelPost := originalPost.Clone()
if err = s.poster.Post(channelPost); err != nil {
return errors.Wrap(err, "failed to post update status message")
}
// Add the status manually for the broadcasts
playbookRunToModify.StatusPosts = append(playbookRunToModify.StatusPosts,
StatusPost{
ID: channelPost.Id,
CreateAt: channelPost.CreateAt,
DeleteAt: channelPost.DeleteAt,
})
if err = s.store.UpdateStatus(&SQLStatusPost{
PlaybookRunID: playbookRunID,
PostID: channelPost.Id,
}); err != nil {
return errors.Wrap(err, "failed to write status post to store. there is now inconsistent state")
}
if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled {
s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, originalPost.Clone(), statusUpdateMessage, playbookRunToModify, logger)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs))
}
err = s.dmPostToRunFollowers(originalPost.Clone(), statusUpdateMessage, playbookRunID, userID)
if err != nil {
logger.WithError(err).Error("failed to dm post to run followers")
}
// Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes)
if err = s.SetNewReminder(playbookRunID, options.Reminder); err != nil {
return errors.Wrapf(err, "failed to set new reminder")
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: channelPost.CreateAt,
EventAt: channelPost.CreateAt,
EventType: StatusUpdated,
PostID: channelPost.Id,
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.telemetry.UpdateStatus(playbookRunToModify, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID)
if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled {
webhookEvent := PlaybookRunWebhookEvent{
Type: StatusUpdated,
At: channelPost.CreateAt,
UserID: userID,
Payload: options,
}
s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs))
}
return nil
}
func (s *PlaybookRunServiceImpl) OpenFinishPlaybookRunDialog(playbookRunID, userID, triggerID string) error {
currentPlaybookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
user, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
numOutstanding := 0
for _, c := range currentPlaybookRun.Checklists {
for _, item := range c.Items {
if item.State == ChecklistItemStateOpen || item.State == ChecklistItemStateInProgress {
numOutstanding++
}
}
}
dialogRequest := model.OpenDialogRequest{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/finish-dialog",
"playbooks",
playbookRunID),
Dialog: *s.newFinishPlaybookRunDialog(currentPlaybookRun, numOutstanding, user.Locale),
TriggerId: triggerID,
}
if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil {
return errors.Wrap(err, "failed to open finish run dialog")
}
return nil
}
func (s *PlaybookRunServiceImpl) buildRunFinishedMessage(playbookRun *PlaybookRun, userName string) string {
telemetryString := fmt.Sprintf("?telem_action=follower_clicked_run_finished_dm&telem_run_id=%s", playbookRun.ID)
announcementMsg := fmt.Sprintf(
"### Run finished: [%s](%s%s)\n",
playbookRun.Name,
GetRunDetailsRelativeURL(playbookRun.ID),
telemetryString,
)
announcementMsg += fmt.Sprintf(
"@%s just marked [%s](%s%s) as finished. Visit the link above for more information.",
userName,
playbookRun.Name,
GetRunDetailsRelativeURL(playbookRun.ID),
telemetryString,
)
return announcementMsg
}
func (s *PlaybookRunServiceImpl) buildStatusUpdateMessage(playbookRun *PlaybookRun, userName string, status string) string {
telemetryString := fmt.Sprintf("?telem_run_id=%s", playbookRun.ID)
announcementMsg := fmt.Sprintf(
"### Run status update %s : [%s](%s%s)\n",
status,
playbookRun.Name,
GetRunDetailsRelativeURL(playbookRun.ID),
telemetryString,
)
announcementMsg += fmt.Sprintf(
"@%s %s status update for [%s](%s%s). Visit the link above for more information.",
userName,
status,
playbookRun.Name,
GetRunDetailsRelativeURL(playbookRun.ID),
telemetryString,
)
return announcementMsg
}
// FinishPlaybookRun changes a run's state to Finished. If run is already in Finished state, the call is a noop.
func (s *PlaybookRunServiceImpl) FinishPlaybookRun(playbookRunID, userID string) error {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
if playbookRunToModify.CurrentStatus == StatusFinished {
return nil
}
endAt := model.GetMillis()
if err = s.store.FinishPlaybookRun(playbookRunID, endAt); err != nil {
return err
}
user, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
message := fmt.Sprintf("@%s marked [%s](%s) as finished.", user.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID))
postID := ""
post, err := s.poster.PostMessage(playbookRunToModify.ChannelID, message)
if err != nil {
logger.WithError(err).WithField("channel_id", playbookRunToModify.ChannelID).Error("failed to post the status update to channel")
} else {
postID = post.Id
}
if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled {
s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, &model.Post{Message: message}, finishMessage, playbookRunToModify, logger)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs))
}
runFinishedMessage := s.buildRunFinishedMessage(playbookRunToModify, user.Username)
err = s.dmPostToRunFollowers(&model.Post{Message: runFinishedMessage}, finishMessage, playbookRunToModify.ID, userID)
if err != nil {
logger.WithError(err).Error("failed to dm post to run followers")
}
// Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes)
s.RemoveReminder(playbookRunID)
err = s.resetReminderTimer(playbookRunID)
if err != nil {
logger.WithError(err).Error("failed to reset the reminder timer when updating status to Archived")
}
// We are resolving the playbook run. Send the reminder to fill out the retrospective
// Also start the recurring reminder if enabled.
if s.licenseChecker.RetrospectiveAllowed() {
if playbookRunToModify.RetrospectiveEnabled && playbookRunToModify.RetrospectivePublishedAt == 0 {
if err = s.postRetrospectiveReminder(playbookRunToModify, true); err != nil {
return errors.Wrap(err, "couldn't post retrospective reminder")
}
s.scheduler.Cancel(RetrospectivePrefix + playbookRunID)
if playbookRunToModify.RetrospectiveReminderIntervalSeconds != 0 {
if err = s.SetReminder(RetrospectivePrefix+playbookRunID, time.Duration(playbookRunToModify.RetrospectiveReminderIntervalSeconds)*time.Second); err != nil {
return errors.Wrap(err, "failed to set the retrospective reminder for playbook run")
}
}
}
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: endAt,
EventAt: endAt,
EventType: RunFinished,
PostID: postID,
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.telemetry.FinishPlaybookRun(playbookRunToModify, userID)
s.metricsService.IncrementRunsFinishedCount(1)
s.sendPlaybookRunUpdatedWS(playbookRunID)
if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled {
webhookEvent := PlaybookRunWebhookEvent{
Type: RunFinished,
At: endAt,
UserID: userID,
}
s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs))
}
return nil
}
func (s *PlaybookRunServiceImpl) ToggleStatusUpdates(playbookRunID, userID string, enable bool) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
logger := logrus.WithField("playbook_run_id", playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
updateAt := model.GetMillis()
playbookRunToModify.StatusUpdateEnabled = enable
if playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil {
return err
}
user, err := s.api.GetUserByID(userID)
T := i18n.GetUserTranslations(user.Locale)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
statusUpdate := "enabled"
eventType := StatusUpdatesEnabled
if !enable {
statusUpdate = "disabled"
eventType = StatusUpdatesDisabled
}
data := map[string]interface{}{
"RunName": playbookRunToModify.Name,
"RunURL": GetRunDetailsRelativeURL(playbookRunID),
"Username": user.Username,
}
message := T("app.user.run.status_disable", data)
if enable {
message = T("app.user.run.status_enable", data)
}
postID := ""
post, err := s.poster.PostMessage(playbookRunToModify.ChannelID, message)
if err != nil {
logger.WithError(err).WithField("channel_id", playbookRunToModify.ChannelID).Error("failed to post the status update to channel")
} else {
postID = post.Id
}
if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled {
s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, &model.Post{Message: message}, statusUpdateMessage, playbookRunToModify, logger)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs))
}
runStatusUpdateMessage := s.buildStatusUpdateMessage(playbookRunToModify, user.Username, statusUpdate)
if err = s.dmPostToRunFollowers(&model.Post{Message: runStatusUpdateMessage}, statusUpdateMessage, playbookRunToModify.ID, userID); err != nil {
logger.WithError(err).Error("failed to dm post toggle-run-status-updates to run followers")
}
// Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes)
if !enable {
s.RemoveReminder(playbookRunID)
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: updateAt,
EventAt: updateAt,
EventType: eventType,
PostID: postID,
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled {
webhookEvent := PlaybookRunWebhookEvent{
Type: eventType,
At: updateAt,
UserID: userID,
}
s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent)
s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs))
}
return nil
}
// RestorePlaybookRun reverts a run from the Finished state. If run was not in Finished state, the call is a noop.
func (s *PlaybookRunServiceImpl) RestorePlaybookRun(playbookRunID, userID string) error {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToRestore, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
if playbookRunToRestore.CurrentStatus != StatusFinished {
return nil
}
restoreAt := model.GetMillis()
if err = s.store.RestorePlaybookRun(playbookRunID, restoreAt); err != nil {
return err
}
user, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
message := fmt.Sprintf("@%s changed the status of [%s](%s) from Finished to In Progress.", user.Username, playbookRunToRestore.Name, GetRunDetailsRelativeURL(playbookRunID))
postID := ""
post, err := s.poster.PostMessage(playbookRunToRestore.ChannelID, message)
if err != nil {
logger.WithField("channel_id", playbookRunToRestore.ChannelID).Error("failed to post the status update to channel")
} else {
postID = post.Id
}
if playbookRunToRestore.StatusUpdateBroadcastChannelsEnabled {
s.broadcastPlaybookRunMessageToChannels(playbookRunToRestore.BroadcastChannelIDs, &model.Post{Message: message}, restoreMessage, playbookRunToRestore, logger)
s.telemetry.RunAction(playbookRunToRestore, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToRestore.BroadcastChannelIDs))
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: restoreAt,
EventAt: restoreAt,
EventType: RunRestored,
PostID: postID,
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.telemetry.RestorePlaybookRun(playbookRunToRestore, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID)
if playbookRunToRestore.StatusUpdateBroadcastWebhooksEnabled {
webhookEvent := PlaybookRunWebhookEvent{
Type: RunRestored,
At: restoreAt,
UserID: userID,
}
s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent)
s.telemetry.RunAction(playbookRunToRestore, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToRestore.WebhookOnStatusUpdateURLs))
}
return nil
}
// GraphqlUpdate updates fields based on a setmap
func (s *PlaybookRunServiceImpl) GraphqlUpdate(id string, setmap map[string]interface{}) error {
if len(setmap) == 0 {
return nil
}
if err := s.store.GraphqlUpdate(id, setmap); err != nil {
return err
}
s.sendPlaybookRunUpdatedWS(id)
return nil
}
func (s *PlaybookRunServiceImpl) postRetrospectiveReminder(playbookRun *PlaybookRun, isInitial bool) error {
retrospectiveURL := getRunRetrospectiveURL("", playbookRun.ID)
attachments := []*model.SlackAttachment{
{
Actions: []*model.PostAction{
{
Type: "button",
Name: "No Retrospective",
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/no-retrospective-button",
"playbooks",
playbookRun.ID),
},
},
},
},
}
customPostType := "custom_retro_rem"
if isInitial {
customPostType = "custom_retro_rem_first"
}
if _, err := s.poster.PostCustomMessageWithAttachments(playbookRun.ChannelID, customPostType, attachments, "@channel Reminder to [fill out the retrospective](%s).", retrospectiveURL); err != nil {
return errors.Wrap(err, "failed to post retro reminder to channel")
}
return nil
}
// GetPlaybookRun gets a playbook run by ID. Returns error if it could not be found.
func (s *PlaybookRunServiceImpl) GetPlaybookRun(playbookRunID string) (*PlaybookRun, error) {
return s.store.GetPlaybookRun(playbookRunID)
}
// GetPlaybookRunMetadata gets ancillary metadata about a playbook run.
func (s *PlaybookRunServiceImpl) GetPlaybookRunMetadata(playbookRunID string) (*Metadata, error) {
playbookRun, err := s.GetPlaybookRun(playbookRunID)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve playbook run '%s'", playbookRunID)
}
// Get main channel details
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve channel id '%s'", playbookRun.ChannelID)
}
team, err := s.api.GetTeam(channel.TeamId)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve team id '%s'", channel.TeamId)
}
numParticipants, err := s.store.GetHistoricalPlaybookRunParticipantsCount(playbookRun.ChannelID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get the count of playbook run members for channel id '%s'", playbookRun.ChannelID)
}
followers, err := s.GetFollowers(playbookRunID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get followers of playbook run %s", playbookRunID)
}
return &Metadata{
ChannelName: channel.Name,
ChannelDisplayName: channel.DisplayName,
TeamName: team.Name,
TotalPosts: channel.TotalMsgCount,
NumParticipants: numParticipants,
Followers: followers,
}, nil
}
// GetPlaybookRunsForChannelByUser get the playbookRuns list associated with this channel and user.
func (s *PlaybookRunServiceImpl) GetPlaybookRunsForChannelByUser(channelID string, userID string) ([]PlaybookRun, error) {
result, err := s.store.GetPlaybookRuns(
RequesterInfo{
UserID: userID,
},
PlaybookRunFilterOptions{
ChannelID: channelID,
Statuses: []string{StatusInProgress},
Page: 0,
PerPage: 1000,
Sort: SortByCreateAt,
Direction: DirectionDesc,
Types: []string{RunTypePlaybook},
},
)
if err != nil {
return nil, err
}
return result.Items, nil
}
// GetOwners returns all the owners of the playbook runs selected by options
func (s *PlaybookRunServiceImpl) GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error) {
owners, err := s.store.GetOwners(requesterInfo, options)
if err != nil {
return nil, errors.Wrap(err, "can't get owners from the store")
}
// System admin can see fullname no matter the settings
if IsSystemAdmin(requesterInfo.UserID, s.api) {
return owners, nil
}
// If ShowFullName is true return owners info unedited
showFullName := s.api.GetConfig().PrivacySettings.ShowFullName
if showFullName != nil && *showFullName {
return owners, nil
}
// Remove names otherwise
for k, o := range owners {
o.FirstName = ""
o.LastName = ""
owners[k] = o
}
return owners, nil
}
// IsOwner returns true if the userID is the owner for playbookRunID.
func (s *PlaybookRunServiceImpl) IsOwner(playbookRunID, userID string) bool {
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return false
}
return playbookRun.OwnerUserID == userID
}
// ChangeOwner processes a request from userID to change the owner for playbookRunID
// to ownerID. Changing to the same ownerID is a no-op.
func (s *PlaybookRunServiceImpl) ChangeOwner(playbookRunID, userID, ownerID string) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return err
}
if playbookRunToModify.OwnerUserID == ownerID {
return nil
}
oldOwner, err := s.api.GetUserByID(playbookRunToModify.OwnerUserID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", playbookRunToModify.OwnerUserID)
}
newOwner, err := s.api.GetUserByID(ownerID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", ownerID)
}
subjectUser, err := s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", userID)
}
// add owner as user
err = s.AddParticipants(playbookRunID, []string{ownerID}, userID, false)
if err != nil {
return errors.Wrap(err, "failed to add owner as a participant")
}
playbookRunToModify.OwnerUserID = ownerID
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
// Do we send a DM to the new owner?
if ownerID != userID {
msg := fmt.Sprintf("@%s changed the owner for run: [%s](%s) from **@%s** to **@%s**",
subjectUser.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunToModify.ID),
oldOwner.Username, newOwner.Username)
if err = s.poster.DM(ownerID, &model.Post{Message: msg}); err != nil {
return errors.Wrapf(err, "failed to send DM in ChangeOwner")
}
}
eventTime := model.GetMillis()
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: eventTime,
EventAt: eventTime,
EventType: OwnerChanged,
Summary: fmt.Sprintf("@%s to @%s", oldOwner.Username, newOwner.Username),
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.telemetry.ChangeOwner(playbookRunToModify, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// ModifyCheckedState checks or unchecks the specified checklist item. Idempotent, will not perform
// any action if the checklist item is already in the given checked state
func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber, itemNumber int) error {
type Details struct {
Action string `json:"action,omitempty"`
Task string `json:"task,omitempty"`
}
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indicies")
}
itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
if newState == itemToCheck.State {
return nil
}
details := Details{
Action: "check",
Task: stripmd.Strip(itemToCheck.Title),
}
modifyMessage := fmt.Sprintf("checked off checklist item **%v**", stripmd.Strip(itemToCheck.Title))
if newState == ChecklistItemStateOpen {
details.Action = "uncheck"
modifyMessage = fmt.Sprintf("unchecked checklist item **%v**", stripmd.Strip(itemToCheck.Title))
}
if newState == ChecklistItemStateSkipped {
details.Action = "skip"
modifyMessage = fmt.Sprintf("skipped checklist item **%v**", stripmd.Strip(itemToCheck.Title))
}
if itemToCheck.State == ChecklistItemStateSkipped && newState == ChecklistItemStateOpen {
details.Action = "restore"
modifyMessage = fmt.Sprintf("restored checklist item **%v**", stripmd.Strip(itemToCheck.Title))
}
itemToCheck.State = newState
itemToCheck.StateModified = model.GetMillis()
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run, is now in inconsistent state")
}
s.telemetry.ModifyCheckedState(playbookRunID, userID, itemToCheck, playbookRunToModify.OwnerUserID == userID)
detailsJSON, err := json.Marshal(details)
if err != nil {
return errors.Wrap(err, "failed to encode timeline event details")
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: itemToCheck.StateModified,
EventAt: itemToCheck.StateModified,
EventType: TaskStateModified,
Summary: modifyMessage,
SubjectUserID: userID,
Details: string(detailsJSON),
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// ToggleCheckedState checks or unchecks the specified checklist item
func (s *PlaybookRunServiceImpl) ToggleCheckedState(playbookRunID, userID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indices")
}
isOpen := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State == ChecklistItemStateOpen
newState := ChecklistItemStateOpen
if isOpen {
newState = ChecklistItemStateClosed
}
return s.ModifyCheckedState(playbookRunID, userID, newState, checklistNumber, itemNumber)
}
// SetAssignee sets the assignee for the specified checklist item
// Idempotent, will not perform any actions if the checklist item is already assigned to assigneeID
func (s *PlaybookRunServiceImpl) SetAssignee(playbookRunID, userID, assigneeID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indices")
}
itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
if assigneeID == itemToCheck.AssigneeID {
return nil
}
newAssigneeUserAtMention := noAssigneeName
if assigneeID != "" {
var newUser *model.User
newUser, err = s.api.GetUserByID(assigneeID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", assigneeID)
}
newAssigneeUserAtMention = "@" + newUser.Username
}
oldAssigneeUserAtMention := noAssigneeName
if itemToCheck.AssigneeID != "" {
var oldUser *model.User
oldUser, err = s.api.GetUserByID(itemToCheck.AssigneeID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", assigneeID)
}
oldAssigneeUserAtMention = "@" + oldUser.Username
}
itemToCheck.AssigneeID = assigneeID
itemToCheck.AssigneeModified = model.GetMillis()
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run; it is now in an inconsistent state")
}
// add the user as run participant if they was not already
if assigneeID != "" && assigneeID != playbookRunToModify.OwnerUserID {
var isParticipant bool
for _, participantID := range playbookRunToModify.ParticipantIDs {
if participantID == assigneeID {
isParticipant = true
break
}
}
if !isParticipant {
err = s.AddParticipants(playbookRunID, []string{assigneeID}, userID, false)
if err != nil {
return errors.Wrapf(err, "failed to add assignee to run")
}
}
}
// Do we send a DM to the new assignee?
if itemToCheck.AssigneeID != "" && itemToCheck.AssigneeID != userID {
var subjectUser *model.User
subjectUser, err = s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to to resolve user %s", assigneeID)
}
runURL := fmt.Sprintf("[%s](%s?from=dm_assignedtask)\n", playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID))
modifyMessage := fmt.Sprintf("@%s assigned you the task **%s** (previously assigned to %s) for the run: %s #taskassigned",
subjectUser.Username, stripmd.Strip(itemToCheck.Title), oldAssigneeUserAtMention, runURL)
if err = s.poster.DM(itemToCheck.AssigneeID, &model.Post{Message: modifyMessage}); err != nil {
return errors.Wrapf(err, "failed to send DM in SetAssignee")
}
}
s.telemetry.SetAssignee(playbookRunID, userID, itemToCheck)
modifyMessage := fmt.Sprintf("changed assignee of checklist item **%s** from **%s** to **%s**",
stripmd.Strip(itemToCheck.Title), oldAssigneeUserAtMention, newAssigneeUserAtMention)
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: itemToCheck.AssigneeModified,
EventAt: itemToCheck.AssigneeModified,
EventType: AssigneeChanged,
Summary: modifyMessage,
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// SetCommandToChecklistItem sets command to checklist item
func (s *PlaybookRunServiceImpl) SetCommandToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newCommand string) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indices")
}
// CommandLastRun is reset to avoid misunderstandings when the command is changed but the date
// of the previous run is set (and show rerun in the UI)
if playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command != newCommand {
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].CommandLastRun = 0
}
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command = newCommand
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
return nil
}
func (s *PlaybookRunServiceImpl) SetTaskActionsToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, taskActions []TaskAction) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indices")
}
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].TaskActions = taskActions
if playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
return nil
}
// SetDueDate sets absolute due date timestamp for the specified checklist item
func (s *PlaybookRunServiceImpl) SetDueDate(playbookRunID, userID string, duedate int64, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indices")
}
itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
itemToCheck.DueDate = duedate
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck
_, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run; it is now in an inconsistent state")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// RunChecklistItemSlashCommand executes the slash command associated with the specified checklist
// item.
func (s *PlaybookRunServiceImpl) RunChecklistItemSlashCommand(playbookRunID, userID string, checklistNumber, itemNumber int) (string, error) {
playbookRun, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return "", err
}
if !IsValidChecklistItemIndex(playbookRun.Checklists, checklistNumber, itemNumber) {
return "", errors.New("invalid checklist item indices")
}
itemToRun := playbookRun.Checklists[checklistNumber].Items[itemNumber]
if strings.TrimSpace(itemToRun.Command) == "" {
return "", errors.New("no slash command associated with this checklist item")
}
// parse playbook summary for variables and values
varsAndVals := parseVariablesAndValues(playbookRun.Summary)
// parse slash command for variables
varsInCmd := parseVariables(itemToRun.Command)
command := itemToRun.Command
for _, v := range varsInCmd {
if val, ok := varsAndVals[v]; !ok || val == "" {
s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Found undefined or empty variable in slash command: %s", v)})
return "", errors.Errorf("Found undefined or empty variable in slash command: %s", v)
}
command = strings.ReplaceAll(command, v, varsAndVals[v])
}
cmdResponse, err := s.api.Execute(&model.CommandArgs{
Command: command,
UserId: userID,
TeamId: playbookRun.TeamID,
ChannelId: playbookRun.ChannelID,
})
if err == ErrNotFound {
trigger := strings.Fields(command)[0]
s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Failed to find slash command **%s**", trigger)})
return "", errors.Wrap(err, "failed to find slash command")
} else if err != nil {
s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Failed to execute slash command **%s**", command)})
return "", errors.Wrap(err, "failed to run slash command")
}
// Fetch the playbook run again, in case the slash command actually changed the run
// (e.g. `/playbook owner`).
playbookRun, err = s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return "", errors.Wrapf(err, "failed to retrieve playbook run after running slash command")
}
// Record the last (successful) run time.
playbookRun.Checklists[checklistNumber].Items[itemNumber].CommandLastRun = model.GetMillis()
_, err = s.store.UpdatePlaybookRun(playbookRun)
if err != nil {
return "", errors.Wrapf(err, "failed to update playbook run recording run of slash command")
}
s.telemetry.RunTaskSlashCommand(playbookRunID, userID, itemToRun)
eventTime := model.GetMillis()
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: eventTime,
EventAt: eventTime,
EventType: RanSlashCommand,
Summary: fmt.Sprintf("ran the slash command: `%s`", command),
SubjectUserID: userID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return "", errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
return cmdResponse.TriggerId, nil
}
func (s *PlaybookRunServiceImpl) DuplicateChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) {
return errors.New("invalid checklist item indicies")
}
checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
checklistItem.ID = ""
playbookRunToModify.Checklists[checklistNumber].Items = append(
playbookRunToModify.Checklists[checklistNumber].Items[:itemNumber+1],
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber:]...)
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber+1] = checklistItem
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.AddTask(playbookRunID, userID, checklistItem)
return nil
}
// AddChecklist adds a checklist to the specified run
func (s *PlaybookRunServiceImpl) AddChecklist(playbookRunID, userID string, checklist Checklist) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrapf(err, "failed to retrieve playbook run")
}
playbookRunToModify.Checklists = append(playbookRunToModify.Checklists, checklist)
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.AddChecklist(playbookRunID, userID, checklist)
return nil
}
// DuplicateChecklist duplicates a checklist
func (s *PlaybookRunServiceImpl) DuplicateChecklist(playbookRunID, userID string, checklistNumber int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
duplicate := playbookRunToModify.Checklists[checklistNumber].Clone()
playbookRunToModify.Checklists = append(playbookRunToModify.Checklists, duplicate)
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.AddChecklist(playbookRunID, userID, duplicate)
return nil
}
// RemoveChecklist removes the specified checklist
func (s *PlaybookRunServiceImpl) RemoveChecklist(playbookRunID, userID string, checklistNumber int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
oldChecklist := playbookRunToModify.Checklists[checklistNumber]
playbookRunToModify.Checklists = append(playbookRunToModify.Checklists[:checklistNumber], playbookRunToModify.Checklists[checklistNumber+1:]...)
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.RemoveChecklist(playbookRunID, userID, oldChecklist)
return nil
}
// RenameChecklist adds a checklist to the specified run
func (s *PlaybookRunServiceImpl) RenameChecklist(playbookRunID, userID string, checklistNumber int, newTitle string) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
playbookRunToModify.Checklists[checklistNumber].Title = newTitle
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
s.telemetry.RenameChecklist(playbookRunID, userID, playbookRunToModify.Checklists[checklistNumber])
return nil
}
// AddChecklistItem adds an item to the specified checklist
func (s *PlaybookRunServiceImpl) AddChecklistItem(playbookRunID, userID string, checklistNumber int, checklistItem ChecklistItem) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
playbookRunToModify.Checklists[checklistNumber].Items = append(playbookRunToModify.Checklists[checklistNumber].Items, checklistItem)
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.AddTask(playbookRunID, userID, checklistItem)
return nil
}
// RemoveChecklistItem removes the item at the given index from the given checklist
func (s *PlaybookRunServiceImpl) RemoveChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
playbookRunToModify.Checklists[checklistNumber].Items = append(
playbookRunToModify.Checklists[checklistNumber].Items[:itemNumber],
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber+1:]...,
)
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.RemoveTask(playbookRunID, userID, checklistItem)
return nil
}
// SkipChecklist skips the checklist
func (s *PlaybookRunServiceImpl) SkipChecklist(playbookRunID, userID string, checklistNumber int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
for itemNumber := 0; itemNumber < len(playbookRunToModify.Checklists[checklistNumber].Items); itemNumber++ {
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].LastSkipped = model.GetMillis()
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateSkipped
}
checklist := playbookRunToModify.Checklists[checklistNumber]
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.SkipChecklist(playbookRunID, userID, checklist)
return nil
}
// RestoreChecklist restores the skipped checklist
func (s *PlaybookRunServiceImpl) RestoreChecklist(playbookRunID, userID string, checklistNumber int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return err
}
for itemNumber := 0; itemNumber < len(playbookRunToModify.Checklists[checklistNumber].Items); itemNumber++ {
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateOpen
}
checklist := playbookRunToModify.Checklists[checklistNumber]
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.RestoreChecklist(playbookRunID, userID, checklist)
return nil
}
// SkipChecklistItem skips the item at the given index from the given checklist
func (s *PlaybookRunServiceImpl) SkipChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].LastSkipped = model.GetMillis()
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateSkipped
checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.SkipTask(playbookRunID, userID, checklistItem)
return nil
}
// RestoreChecklistItem restores the item at the given index from the given checklist
func (s *PlaybookRunServiceImpl) RestoreChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateOpen
checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.RestoreTask(playbookRunID, userID, checklistItem)
return nil
}
// EditChecklistItem changes the title of a specified checklist item
func (s *PlaybookRunServiceImpl) EditChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newTitle, newCommand, newDescription string) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber)
if err != nil {
return err
}
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Title = newTitle
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command = newCommand
playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Description = newDescription
checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber]
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.RenameTask(playbookRunID, userID, checklistItem)
return nil
}
// MoveChecklist moves a checklist to a new location
func (s *PlaybookRunServiceImpl) MoveChecklist(playbookRunID, userID string, sourceChecklistIdx, destChecklistIdx int) error {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, sourceChecklistIdx)
if err != nil {
return err
}
if destChecklistIdx < 0 || destChecklistIdx >= len(playbookRunToModify.Checklists) {
return errors.New("invalid destChecklist")
}
// Get checklist to move
checklistMoved := playbookRunToModify.Checklists[sourceChecklistIdx]
// Delete checklist to move
copy(playbookRunToModify.Checklists[sourceChecklistIdx:], playbookRunToModify.Checklists[sourceChecklistIdx+1:])
playbookRunToModify.Checklists[len(playbookRunToModify.Checklists)-1] = Checklist{}
// Insert checklist in new location
copy(playbookRunToModify.Checklists[destChecklistIdx+1:], playbookRunToModify.Checklists[destChecklistIdx:])
playbookRunToModify.Checklists[destChecklistIdx] = checklistMoved
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.MoveChecklist(playbookRunID, userID, checklistMoved)
return nil
}
// MoveChecklistItem moves a checklist item to a new location
func (s *PlaybookRunServiceImpl) MoveChecklistItem(playbookRunID, userID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error {
playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, sourceChecklistIdx, sourceItemIdx)
if err != nil {
return err
}
if destChecklistIdx < 0 || destChecklistIdx >= len(playbookRunToModify.Checklists) {
return errors.New("invalid destChecklist")
}
lenDestItems := len(playbookRunToModify.Checklists[destChecklistIdx].Items)
if (destItemIdx < 0) || (sourceChecklistIdx == destChecklistIdx && destItemIdx >= lenDestItems) || (destItemIdx > lenDestItems) {
return errors.New("invalid destItem")
}
// Moved item
sourceChecklist := playbookRunToModify.Checklists[sourceChecklistIdx].Items
itemMoved := sourceChecklist[sourceItemIdx]
// Delete item to move
sourceChecklist = append(sourceChecklist[:sourceItemIdx], sourceChecklist[sourceItemIdx+1:]...)
// Insert item in new location
destChecklist := playbookRunToModify.Checklists[destChecklistIdx].Items
if sourceChecklistIdx == destChecklistIdx {
destChecklist = sourceChecklist
}
destChecklist = append(destChecklist, ChecklistItem{})
copy(destChecklist[destItemIdx+1:], destChecklist[destItemIdx:])
destChecklist[destItemIdx] = itemMoved
// Update the playbookRunToModify checklists. If the source and destination indices
// are the same, we only need to update the checklist to its final state (destChecklist)
if sourceChecklistIdx == destChecklistIdx {
playbookRunToModify.Checklists[sourceChecklistIdx].Items = destChecklist
} else {
playbookRunToModify.Checklists[sourceChecklistIdx].Items = sourceChecklist
playbookRunToModify.Checklists[destChecklistIdx].Items = destChecklist
}
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify))
s.telemetry.MoveTask(playbookRunID, userID, itemMoved)
return nil
}
// GetChecklistAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete
func (s *PlaybookRunServiceImpl) GetChecklistAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) {
ret := make([]model.AutocompleteListItem, 0)
multipleRuns := len(playbookRuns) > 1
for j, playbookRun := range playbookRuns {
runIndex := ""
runName := ""
// include run number and name only if there are multiple runs
if multipleRuns {
runIndex = fmt.Sprintf("%d ", j)
runName = fmt.Sprintf("\"%s\" - ", playbookRun.Name)
}
for i, checklist := range playbookRun.Checklists {
ret = append(ret, model.AutocompleteListItem{
Item: fmt.Sprintf("%s%d", runIndex, i),
Hint: fmt.Sprintf("%s\"%s\"", runName, stripmd.Strip(checklist.Title)),
})
}
}
return ret, nil
}
// GetChecklistItemAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete
func (s *PlaybookRunServiceImpl) GetChecklistItemAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) {
ret := make([]model.AutocompleteListItem, 0)
multipleRuns := len(playbookRuns) > 1
for k, playbookRun := range playbookRuns {
runIndex := ""
runName := ""
// include run number and name only if there are multiple runs
if multipleRuns {
runIndex = fmt.Sprintf("%d ", k)
runName = fmt.Sprintf("\"%s\" - ", playbookRun.Name)
}
for i, checklist := range playbookRun.Checklists {
for j, item := range checklist.Items {
ret = append(ret, model.AutocompleteListItem{
Item: fmt.Sprintf("%s%d %d", runIndex, i, j),
Hint: fmt.Sprintf("%s\"%s\"", runName, stripmd.Strip(item.Title)),
})
}
}
}
return ret, nil
}
// GetRunsAutocomplete returns the list of runs to be used in autocomplete
func (s *PlaybookRunServiceImpl) GetRunsAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) {
if len(playbookRuns) <= 1 {
return nil, nil
}
ret := make([]model.AutocompleteListItem, 0)
for i, playbookRun := range playbookRuns {
ret = append(ret, model.AutocompleteListItem{
Item: fmt.Sprintf("%d", i),
Hint: fmt.Sprintf("\"%s\"", playbookRun.Name),
})
}
return ret, nil
}
type TodoDigestMessageItems struct {
overdueRuns []RunLink
assignedRuns []AssignedRun
inProgressRuns []RunLink
}
func (s *PlaybookRunServiceImpl) getTodoDigestMessageItems(userID string) (*TodoDigestMessageItems, error) {
runsOverdue, err := s.GetOverdueUpdateRuns(userID)
if err != nil {
return nil, err
}
runsAssigned, err := s.GetRunsWithAssignedTasks(userID)
if err != nil {
return nil, err
}
runsInProgress, err := s.GetParticipatingRuns(userID)
if err != nil {
return nil, err
}
return &TodoDigestMessageItems{
overdueRuns: runsOverdue,
assignedRuns: runsAssigned,
inProgressRuns: runsInProgress,
}, nil
}
// buildTodoDigestMessage
// gathers the list of assigned tasks, participating runs, and overdue updates and builds a combined message with them
func (s *PlaybookRunServiceImpl) buildTodoDigestMessage(userID string, force bool, shouldSendFullData bool) (*model.Post, error) {
digestMessageItems, err := s.getTodoDigestMessageItems(userID)
if err != nil {
return nil, err
}
// if we have no items to send and we're not forced to, return early
if len(digestMessageItems.assignedRuns) == 0 &&
len(digestMessageItems.overdueRuns) == 0 &&
len(digestMessageItems.inProgressRuns) == 0 &&
!force {
return nil, nil
}
user, err := s.api.GetUserByID(userID)
if err != nil {
return nil, err
}
part1 := buildRunsOverdueMessage(digestMessageItems.overdueRuns, user.Locale)
timezone, err := timeutils.GetUserTimezone(user)
if err != nil {
return nil, err
}
part2 := buildAssignedTaskMessageSummary(digestMessageItems.assignedRuns, user.Locale, timezone, !force)
part3 := buildRunsInProgressMessage(digestMessageItems.inProgressRuns, user.Locale)
var message string
if shouldSendFullData || len(digestMessageItems.overdueRuns) > 0 {
message += part1
}
if shouldSendFullData || len(digestMessageItems.assignedRuns) > 0 {
message += part2
}
if shouldSendFullData || len(digestMessageItems.inProgressRuns) > 0 {
message += part3
}
return &model.Post{Message: message}, nil
}
// EphemeralPostTodoDigestToUser
// builds todo digest message and sends an ephemeral post to userID, channelID. Use force = true to send post even if there are no items.
func (s *PlaybookRunServiceImpl) EphemeralPostTodoDigestToUser(userID string, channelID string, force bool, shouldSendFullData bool) error {
todoDigestMessage, err := s.buildTodoDigestMessage(userID, force, shouldSendFullData)
if err != nil {
return err
}
if todoDigestMessage != nil {
s.poster.EphemeralPost(userID, channelID, todoDigestMessage)
return nil
}
return nil
}
// DMTodoDigestToUser
// DMs the message to userID. Use force = true to DM even if there are no items.
func (s *PlaybookRunServiceImpl) DMTodoDigestToUser(userID string, force bool, shouldSendFullData bool) error {
todoDigestMessage, err := s.buildTodoDigestMessage(userID, force, shouldSendFullData)
if err != nil {
return err
}
if todoDigestMessage != nil {
return s.poster.DM(userID, todoDigestMessage)
}
return nil
}
// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID
func (s *PlaybookRunServiceImpl) GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error) {
return s.store.GetRunsWithAssignedTasks(userID)
}
// GetParticipatingRuns returns the list of active runs with userID as a participant
func (s *PlaybookRunServiceImpl) GetParticipatingRuns(userID string) ([]RunLink, error) {
return s.store.GetParticipatingRuns(userID)
}
// GetOverdueUpdateRuns returns the list of userID's runs that have overdue updates
func (s *PlaybookRunServiceImpl) GetOverdueUpdateRuns(userID string) ([]RunLink, error) {
return s.store.GetOverdueUpdateRuns(userID)
}
func (s *PlaybookRunServiceImpl) checklistParamsVerify(playbookRunID, userID string, checklistNumber int) (*PlaybookRun, error) {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve playbook run")
}
if checklistNumber < 0 || checklistNumber >= len(playbookRunToModify.Checklists) {
return nil, errors.New("invalid checklist number")
}
return playbookRunToModify, nil
}
func (s *PlaybookRunServiceImpl) checklistItemParamsVerify(playbookRunID, userID string, checklistNumber, itemNumber int) (*PlaybookRun, error) {
playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber)
if err != nil {
return nil, err
}
if itemNumber < 0 || itemNumber >= len(playbookRunToModify.Checklists[checklistNumber].Items) {
return nil, errors.New("invalid item number")
}
return playbookRunToModify, nil
}
// NukeDB removes all playbook run related data.
func (s *PlaybookRunServiceImpl) NukeDB() error {
return s.store.NukeDB()
}
// ChangeCreationDate changes the creation date of the playbook run.
func (s *PlaybookRunServiceImpl) ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error {
return s.store.ChangeCreationDate(playbookRunID, creationTimestamp)
}
func (s *PlaybookRunServiceImpl) createPlaybookRunChannel(playbookRun *PlaybookRun, header string, public bool) (*model.Channel, error) {
channelType := model.ChannelTypePrivate
if public {
channelType = model.ChannelTypeOpen
}
channel := &model.Channel{
TeamId: playbookRun.TeamID,
Type: channelType,
DisplayName: playbookRun.Name,
Name: cleanChannelName(playbookRun.Name),
Header: header,
}
if channel.Name == "" {
channel.Name = model.NewId()
}
// Prefer the channel name the user chose. But if it already exists, add some random bits
// and try exactly once more.
err := s.api.CreateChannel(channel)
if err != nil {
if appErr, ok := err.(*model.AppError); ok {
// Let the user correct display name errors:
if appErr.Id == "model.channel.is_valid.display_name.app_error" ||
appErr.Id == "model.channel.is_valid.1_or_more.app_error" {
return nil, ErrChannelDisplayNameInvalid
}
// We can fix channel Name errors:
if appErr.Id == "store.sql_channel.save_channel.exists.app_error" {
channel.Name = addRandomBits(channel.Name)
// clean channel id
channel.Id = ""
err = s.api.CreateChannel(channel)
}
}
if err != nil {
return nil, errors.Wrapf(err, "failed to create channel")
}
}
return channel, nil
}
// addPlaybookRunInitialMemberships creates the memberships in run and channels for the most core users: playbooksbot, reporter and owner
func (s *PlaybookRunServiceImpl) addPlaybookRunInitialMemberships(playbookRun *PlaybookRun, channel *model.Channel) error {
if _, err := s.api.CreateMember(channel.TeamId, s.configService.GetConfiguration().BotUserID); err != nil {
return errors.Wrapf(err, "failed to add bot to the team")
}
// channel related
if _, err := s.api.AddMemberToChannel(channel.Id, s.configService.GetConfiguration().BotUserID); err != nil {
return errors.Wrapf(err, "failed to add bot to the channel")
}
if _, err := s.api.AddUserToChannel(channel.Id, playbookRun.ReporterUserID, s.configService.GetConfiguration().BotUserID); err != nil {
return errors.Wrapf(err, "failed to add reporter to the channel")
}
if playbookRun.OwnerUserID != playbookRun.ReporterUserID {
if _, err := s.api.AddUserToChannel(channel.Id, playbookRun.OwnerUserID, s.configService.GetConfiguration().BotUserID); err != nil {
return errors.Wrapf(err, "failed to add owner to channel")
}
}
_, userRoleID, adminRoleID := s.GetSchemeRolesForChannel(channel)
if _, err := s.api.UpdateChannelMemberRoles(channel.Id, playbookRun.OwnerUserID, fmt.Sprintf("%s %s", userRoleID, adminRoleID)); err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"channel_id": channel.Id,
"owner_user_id": playbookRun.OwnerUserID,
}).Warn("failed to promote owner to admin")
}
// run related
participants := []string{playbookRun.OwnerUserID}
if playbookRun.OwnerUserID != playbookRun.ReporterUserID {
participants = append(participants, playbookRun.ReporterUserID)
}
err := s.AddParticipants(playbookRun.ID, participants, playbookRun.ReporterUserID, false)
if err != nil {
return errors.Wrap(err, "failed to add owner/reporter as a participant")
}
return nil
}
func (s *PlaybookRunServiceImpl) GetSchemeRolesForChannel(channel *model.Channel) (string, string, string) {
// get channel roles
if guestRole, userRole, adminRole, err := s.store.GetSchemeRolesForChannel(channel.Id); err == nil {
return guestRole, userRole, adminRole
}
// get team roles if channel roles are not available
if guestRole, userRole, adminRole, err := s.store.GetSchemeRolesForTeam(channel.TeamId); err == nil {
return guestRole, userRole, adminRole
}
// return default roles
return model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId
}
func (s *PlaybookRunServiceImpl) newFinishPlaybookRunDialog(playbookRun *PlaybookRun, outstanding int, locale string) *model.Dialog {
T := i18n.GetUserTranslations(locale)
data := map[string]interface{}{
"RunName": playbookRun.Name,
"Count": outstanding,
}
message := T("app.user.run.confirm_finish.num_outstanding", data)
return &model.Dialog{
Title: T("app.user.run.confirm_finish.title"),
IntroductionText: message,
SubmitLabel: T("app.user.run.confirm_finish.submit_label"),
NotifyOnCancel: false,
}
}
func (s *PlaybookRunServiceImpl) newPlaybookRunDialog(teamID, requesterID, postID, clientID string, playbooks []Playbook) (*model.Dialog, error) {
user, err := s.api.GetUserByID(requesterID)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch owner user")
}
T := i18n.GetUserTranslations(user.Locale)
state, err := json.Marshal(DialogState{
PostID: postID,
ClientID: clientID,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal DialogState")
}
var options []*model.PostActionOptions
for _, playbook := range playbooks {
options = append(options, &model.PostActionOptions{
Text: playbook.Title,
Value: playbook.ID,
})
}
data := map[string]interface{}{
"Username": getUserDisplayName(user),
}
introText := T("app.user.new_run.intro", data)
defaultPlaybookID := ""
defaultChannelNameTemplate := ""
if len(playbooks) == 1 {
defaultPlaybookID = playbooks[0].ID
defaultChannelNameTemplate = playbooks[0].ChannelNameTemplate
}
return &model.Dialog{
Title: T("app.user.new_run.title"),
IntroductionText: introText,
Elements: []model.DialogElement{
{
DisplayName: T("app.user.new_run.playbook"),
Name: DialogFieldPlaybookIDKey,
Type: "select",
Options: options,
Default: defaultPlaybookID,
},
{
DisplayName: T("app.user.new_run.run_name"),
Name: DialogFieldNameKey,
Type: "text",
MinLength: 1,
MaxLength: 64,
Default: defaultChannelNameTemplate,
},
},
SubmitLabel: T("app.user.new_run.submit_label"),
NotifyOnCancel: false,
State: string(state),
}, nil
}
func (s *PlaybookRunServiceImpl) newUpdatePlaybookRunDialog(description, message string, broadcastChannelNum int, reminderTimer time.Duration, locale string) (*model.Dialog, error) {
T := i18n.GetUserTranslations(locale)
data := map[string]interface{}{
"Count": broadcastChannelNum,
}
introductionText := T("app.user.run.update_status.num_channel", data)
reminderOptions := []*model.PostActionOptions{
{
Text: "15min",
Value: "900",
},
{
Text: "30min",
Value: "1800",
},
{
Text: "60min",
Value: "3600",
},
{
Text: "4hr",
Value: "14400",
},
{
Text: "24hr",
Value: "86400",
},
{
Text: "1Week",
Value: "604800",
},
}
if s.configService.IsConfiguredForDevelopmentAndTesting() {
reminderOptions = append(reminderOptions, nil)
copy(reminderOptions[2:], reminderOptions[1:])
reminderOptions[1] = &model.PostActionOptions{
Text: "10sec",
Value: "10",
}
}
return &model.Dialog{
Title: T("app.user.run.update_status.title"),
IntroductionText: introductionText,
Elements: []model.DialogElement{
{
DisplayName: T("app.user.run.update_status.change_since_last_update"),
Name: DialogFieldMessageKey,
Type: "textarea",
Default: message,
},
{
DisplayName: T("app.user.run.update_status.reminder_for_next_update"),
Name: DialogFieldReminderInSecondsKey,
Type: "select",
Options: reminderOptions,
Optional: true,
Default: fmt.Sprintf("%d", reminderTimer/time.Second),
},
{
DisplayName: T("app.user.run.update_status.finish_run"),
Name: DialogFieldFinishRun,
Placeholder: T("app.user.run.update_status.finish_run.placeholder"),
Type: "bool",
Optional: true,
},
},
SubmitLabel: T("app.user.run.update_status.submit_label"),
NotifyOnCancel: false,
}, nil
}
func (s *PlaybookRunServiceImpl) newAddToTimelineDialog(playbookRuns []PlaybookRun, postID, userID string) (*model.Dialog, error) {
user, err := s.api.GetUserByID(userID)
if err != nil {
return nil, errors.Wrapf(err, "failed to to resolve user %s", userID)
}
T := i18n.GetUserTranslations(user.Locale)
var options []*model.PostActionOptions
for _, i := range playbookRuns {
options = append(options, &model.PostActionOptions{
Text: i.Name,
Value: i.ID,
})
}
state, err := json.Marshal(DialogStateAddToTimeline{
PostID: postID,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal DialogState")
}
post, err := s.api.GetPost(postID)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal DialogState")
}
defaultSummary := ""
if post.Message != "" {
end := min(40, len(post.Message))
defaultSummary = post.Message[:end]
if len(post.Message) > end {
defaultSummary += "..."
}
}
defaultPlaybookRuns, err := s.GetPlaybookRunsForChannelByUser(post.ChannelId, userID)
if err != nil && !errors.Is(err, ErrNotFound) {
return nil, errors.Wrapf(err, "failed to get playbookRunID for channel")
}
defaultRunID := ""
if len(defaultPlaybookRuns) == 1 {
defaultRunID = defaultPlaybookRuns[0].ID
}
return &model.Dialog{
Title: T("app.user.run.add_to_timeline.title"),
Elements: []model.DialogElement{
{
DisplayName: T("app.user.run.add_to_timeline.playbook_run"),
Name: DialogFieldPlaybookRunKey,
Type: "select",
Options: options,
Default: defaultRunID,
},
{
DisplayName: T("app.user.run.add_to_timeline.summary"),
Name: DialogFieldSummary,
Type: "text",
MaxLength: 64,
Placeholder: T("app.user.run.add_to_timeline.summary.placeholder"),
Default: defaultSummary,
HelpText: T("app.user.run.add_to_timeline.summary.help"),
},
},
SubmitLabel: T("app.user.run.add_to_timeline.submit_label"),
NotifyOnCancel: false,
State: string(state),
}, nil
}
// structure to handle optional parameters for sendPlaybookRunUpdatedWS
type RunWSOptions struct {
AdditionalUserIDs []string
PlaybookRun *PlaybookRun
}
type RunWSOption func(options *RunWSOptions)
func withAdditionalUserIDs(additionalUserIDs []string) RunWSOption {
return func(options *RunWSOptions) {
options.AdditionalUserIDs = append(options.AdditionalUserIDs, additionalUserIDs...)
}
}
func withPlaybookRun(playbookRun *PlaybookRun) RunWSOption {
return func(options *RunWSOptions) {
options.PlaybookRun = playbookRun
}
}
// sendPlaybookRunUpdatedWS send run updates to users via websocket
// Individual Websocket messages will be sent to the owner/participants and users
// (optionally passed as parameter)
func (s *PlaybookRunServiceImpl) sendPlaybookRunUpdatedWS(playbookRunID string, options ...RunWSOption) {
var err error
sendWSOptions := RunWSOptions{}
for _, option := range options {
option(&sendWSOptions)
}
// Get playbookRun if not provided
playbookRun := sendWSOptions.PlaybookRun
if playbookRun == nil {
playbookRun, err = s.store.GetPlaybookRun(playbookRunID)
if err != nil {
logrus.WithError(err).WithField("playbookRunID", playbookRunID).Error("failed to retrieve playbook run when sending websocket")
return
}
}
// create a unique list of user ids to send the message to
uniqueUserIDs := make(map[string]bool, len(sendWSOptions.AdditionalUserIDs)+len(playbookRun.ParticipantIDs)+1)
uniqueUserIDs[playbookRun.OwnerUserID] = true
for _, u := range sendWSOptions.AdditionalUserIDs {
uniqueUserIDs[u] = true
}
for _, u := range playbookRun.ParticipantIDs {
uniqueUserIDs[u] = true
}
// send the websocket message
for userID := range uniqueUserIDs {
s.poster.PublishWebsocketEventToUser(playbookRunUpdatedWSEvent, playbookRun, userID)
}
}
func (s *PlaybookRunServiceImpl) UpdateRetrospective(playbookRunID, updaterID string, newRetrospective RetrospectiveUpdate) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
playbookRunToModify.Retrospective = newRetrospective.Text
playbookRunToModify.MetricsData = newRetrospective.Metrics
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrap(err, "failed to update playbook run")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
s.telemetry.UpdateRetrospective(playbookRunToModify, updaterID)
return nil
}
func (s *PlaybookRunServiceImpl) PublishRetrospective(playbookRunID, publisherID string, retrospective RetrospectiveUpdate) error {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToPublish, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
now := model.GetMillis()
// Update the text to keep syncronized
playbookRunToPublish.Retrospective = retrospective.Text
playbookRunToPublish.MetricsData = retrospective.Metrics
playbookRunToPublish.RetrospectivePublishedAt = now
playbookRunToPublish.RetrospectiveWasCanceled = false
playbookRunToPublish, err = s.store.UpdatePlaybookRun(playbookRunToPublish)
if err != nil {
return errors.Wrap(err, "failed to update playbook run")
}
publisherUser, err := s.api.GetUserByID(publisherID)
if err != nil {
return errors.Wrap(err, "failed to get publisher user")
}
retrospectiveURL := getRunRetrospectiveURL("", playbookRunToPublish.ID)
post, err := s.buildRetrospectivePost(playbookRunToPublish, publisherUser, retrospectiveURL)
if err != nil {
return err
}
if err = s.poster.Post(post); err != nil {
return errors.Wrap(err, "failed to post to channel")
}
telemetryString := fmt.Sprintf("?telem_action=follower_clicked_retrospective_dm&telem_run_id=%s", playbookRunToPublish.ID)
retrospectivePublishedMessage := fmt.Sprintf("@%s published the retrospective report for [%s](%s%s).\n%s", publisherUser.Username, playbookRunToPublish.Name, retrospectiveURL, telemetryString, retrospective.Text)
err = s.dmPostToRunFollowers(&model.Post{Message: retrospectivePublishedMessage}, retroMessage, playbookRunToPublish.ID, publisherID)
if err != nil {
logger.WithError(err).Error("failed to dm post to run followers")
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: now,
EventAt: now,
EventType: PublishedRetrospective,
SubjectUserID: publisherID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
s.telemetry.PublishRetrospective(playbookRunToPublish, publisherID)
return nil
}
func (s *PlaybookRunServiceImpl) buildRetrospectivePost(playbookRunToPublish *PlaybookRun, publisherUser *model.User, retrospectiveURL string) (*model.Post, error) {
props := map[string]interface{}{
"metricsData": "null",
"metricsConfigs": "null",
"retrospectiveText": playbookRunToPublish.Retrospective,
}
// If run has metrics data, get playbooks metrics configs and include them in custom post
if len(playbookRunToPublish.MetricsData) > 0 {
playbook, err := s.playbookService.Get(playbookRunToPublish.PlaybookID)
if err != nil {
return nil, errors.Wrap(err, "failed to get playbook")
}
metricsConfigs, err := json.Marshal(playbook.Metrics)
if err != nil {
return nil, errors.Wrap(err, "unable to marshal metrics configs")
}
metricsData, err := json.Marshal(playbookRunToPublish.MetricsData)
if err != nil {
return nil, errors.Wrap(err, "cannot post retro, unable to marshal metrics data")
}
props["metricsData"] = string(metricsData)
props["metricsConfigs"] = string(metricsConfigs)
}
return &model.Post{
Message: fmt.Sprintf("@channel Retrospective for [%s](%s) has been published by @%s\n[See the full retrospective](%s)\n", playbookRunToPublish.Name, GetRunDetailsRelativeURL(playbookRunToPublish.ID), publisherUser.Username, retrospectiveURL),
Type: "custom_retro",
ChannelId: playbookRunToPublish.ChannelID,
Props: props,
}, nil
}
func (s *PlaybookRunServiceImpl) CancelRetrospective(playbookRunID, cancelerID string) error {
playbookRunToCancel, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
now := model.GetMillis()
// Update the text to keep syncronized
playbookRunToCancel.Retrospective = "No retrospective for this run."
playbookRunToCancel.RetrospectivePublishedAt = now
playbookRunToCancel.RetrospectiveWasCanceled = true
playbookRunToCancel, err = s.store.UpdatePlaybookRun(playbookRunToCancel)
if err != nil {
return errors.Wrap(err, "failed to update playbook run")
}
cancelerUser, err := s.api.GetUserByID(cancelerID)
if err != nil {
return errors.Wrap(err, "failed to get canceler user")
}
if _, err = s.poster.PostMessage(playbookRunToCancel.ChannelID, "@channel Retrospective for [%s](%s) has been canceled by @%s\n", playbookRunToCancel.Name, GetRunDetailsRelativeURL(playbookRunID), cancelerUser.Username); err != nil {
return errors.Wrap(err, "failed to post to channel")
}
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: now,
EventAt: now,
EventType: CanceledRetrospective,
SubjectUserID: cancelerID,
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// RequestJoinChannel posts a channel-join request message in the run's channel
func (s *PlaybookRunServiceImpl) RequestJoinChannel(playbookRunID, requesterID string) error {
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
// avoid sending request if user is already a member of the channel
if s.api.HasPermissionToChannel(requesterID, playbookRun.ChannelID, model.PermissionReadChannel) {
return fmt.Errorf("user %s is already a member of the channel %s", requesterID, playbookRunID)
}
requesterUser, err := s.api.GetUserByID(requesterID)
if err != nil {
return errors.Wrap(err, "failed to get requester user")
}
T := i18n.GetUserTranslations(requesterUser.Locale)
data := map[string]interface{}{
"Name": requesterUser.Username,
}
_, err = s.poster.PostMessage(playbookRun.ChannelID, T("app.user.run.request_join_channel", data))
if err != nil {
return errors.Wrap(err, "failed to post to channel")
}
return nil
}
// RequestUpdate posts a status update request message in the run's channel
func (s *PlaybookRunServiceImpl) RequestUpdate(playbookRunID, requesterID string) error {
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
requesterUser, err := s.api.GetUserByID(requesterID)
if err != nil {
return errors.Wrap(err, "failed to get requester user")
}
T := i18n.GetUserTranslations(requesterUser.Locale)
data := map[string]interface{}{
"RunName": playbookRun.Name,
"RunURL": GetRunDetailsRelativeURL(playbookRunID),
"Name": requesterUser.Username,
}
post, err := s.poster.PostMessage(playbookRun.ChannelID, T("app.user.run.request_update", data))
if err != nil {
return errors.Wrap(err, "failed to post to channel")
}
// create timeline event
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: post.CreateAt,
EventAt: post.CreateAt,
EventType: StatusUpdateRequested,
PostID: post.Id,
SubjectUserID: requesterID,
CreatorUserID: requesterID,
Summary: fmt.Sprintf("@%s requested a status update", requesterUser.Username),
}
if _, err = s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
// send updated run through websocket
s.sendPlaybookRunUpdatedWS(playbookRunID)
return nil
}
// Leave removes user from the run's participants
func (s *PlaybookRunServiceImpl) RemoveParticipants(playbookRunID string, userIDs []string, requesterUserID string) error {
if len(userIDs) == 0 {
return nil
}
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
// Check if any user is the owner
for _, userID := range userIDs {
if playbookRun.OwnerUserID == userID {
return errors.New("owner user can't leave the run")
}
}
if err = s.store.RemoveParticipants(playbookRunID, userIDs); err != nil {
return errors.Wrapf(err, "users `%+v` failed to remove participation in run `%s`", userIDs, playbookRunID)
}
requesterUser, err := s.api.GetUserByID(requesterUserID)
if err != nil {
return errors.Wrap(err, "failed to get requester user")
}
users := make([]*model.User, 0)
for _, userID := range userIDs {
user := requesterUser
if userID != requesterUserID {
user, err = s.api.GetUserByID(userID)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
}
users = append(users, user)
s.leaveActions(playbookRun, userID)
}
err = s.changeParticipantsTimeline(playbookRunID, requesterUser, users, "left")
if err != nil {
return err
}
// ws send run
userIDs = append(userIDs, requesterUserID)
s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs(userIDs))
return nil
}
func (s *PlaybookRunServiceImpl) leaveActions(playbookRun *PlaybookRun, userID string) {
if !playbookRun.RemoveChannelMemberOnRemovedParticipant {
return
}
// Don't do anything if the user not a channel member
member, _ := s.api.GetChannelMember(playbookRun.ChannelID, userID)
if member == nil {
return
}
// To be added to the UI as an optional action
if err := s.api.DeleteChannelMember(playbookRun.ChannelID, userID); err != nil {
logrus.WithError(err).WithField("user_id", userID).Error("failed to remove user from linked channel")
}
}
func (s *PlaybookRunServiceImpl) AddParticipants(playbookRunID string, userIDs []string, requesterUserID string, forceAddToChannel bool) error {
usersFailedToInvite := make([]string, 0)
usersToInvite := make([]string, 0)
if len(userIDs) == 0 {
return nil
}
playbookRun, err := s.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrapf(err, "failed to get run %s", playbookRunID)
}
// Ensure new participants are team members
for _, userID := range userIDs {
var member *model.TeamMember
member, err = s.api.GetTeamMember(playbookRun.TeamID, userID)
if err != nil || member.DeleteAt != 0 {
usersFailedToInvite = append(usersFailedToInvite, userID)
continue
}
usersToInvite = append(usersToInvite, userID)
}
if err = s.store.AddParticipants(playbookRun.ID, usersToInvite); err != nil {
return errors.Wrapf(err, "users `%+v` failed to participate the run `%s`", usersToInvite, playbookRun.ID)
}
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
logrus.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("failed to get channel")
}
s.failedInvitedUserActions(usersFailedToInvite, channel)
requesterUser, err := s.api.GetUserByID(requesterUserID)
if err != nil {
return errors.Wrap(err, "failed to get requester user")
}
users := make([]*model.User, 0)
for _, userID := range usersToInvite {
user := requesterUser
if userID != requesterUserID {
user, err = s.api.GetUserByID(userID)
if err != nil {
return errors.Wrapf(err, "failed to get user %s", userID)
}
}
users = append(users, user)
// Configured actions
s.participateActions(playbookRun, channel, user, requesterUser, forceAddToChannel)
// Participate implies following the run
if err = s.Follow(playbookRunID, userID); err != nil {
return errors.Wrap(err, "failed to make participant to follow run")
}
}
err = s.changeParticipantsTimeline(playbookRun.ID, requesterUser, users, "joined")
if err != nil {
return err
}
// ws send run
if len(usersToInvite) > 0 {
s.sendPlaybookRunUpdatedWS(playbookRun.ID, withAdditionalUserIDs(usersToInvite), withAdditionalUserIDs([]string{requesterUserID}))
}
return nil
}
// changeParticipantsTimeline handles timeline event creation for run participation change triggers:
// participate/leave events and add/remove participants (multiple allowed)
func (s *PlaybookRunServiceImpl) changeParticipantsTimeline(playbookRunID string, requesterUser *model.User, users []*model.User, action string) error {
type Details struct {
Action string `json:"action,omitempty"`
Requester string `json:"requester,omitempty"`
Users []string `json:"users,omitempty"`
}
var details Details
if len(users) == 0 {
return nil
}
now := model.GetMillis()
event := &TimelineEvent{
PlaybookRunID: playbookRunID,
CreateAt: now,
EventAt: now,
Summary: "", // copies managed in webapp using the injected data
CreatorUserID: requesterUser.Id,
SubjectUserID: requesterUser.Id,
}
event.EventType = ParticipantsChanged
if len(users) == 1 && users[0].Id == requesterUser.Id {
event.EventType = UserJoinedLeft
}
if len(users) == 1 {
event.SubjectUserID = users[0].Id
}
details.Action = action
details.Requester = requesterUser.Username
details.Users = make([]string, 0)
for _, u := range users {
details.Users = append(details.Users, u.Username)
}
detailsJSON, err := json.Marshal(details)
if err != nil {
return errors.Wrap(err, "failed to encode timeline event details")
}
event.Details = string(detailsJSON)
if _, err := s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrap(err, "failed to create timeline event")
}
return nil
}
func (s *PlaybookRunServiceImpl) participateActions(playbookRun *PlaybookRun, channel *model.Channel, user *model.User, requesterUser *model.User, forceAddToChannel bool) {
if !playbookRun.CreateChannelMemberOnNewParticipant && !forceAddToChannel {
return
}
// Don't do anything if the user is a channel member
member, _ := s.api.GetChannelMember(playbookRun.ChannelID, user.Id)
if member != nil {
return
}
// Add user to the channel
if _, err := s.api.AddChannelMember(playbookRun.ChannelID, user.Id); err != nil {
logrus.WithError(err).WithField("user_id", user.Id).Error("participateActions: failed to add user to linked channel")
}
}
func (s *PlaybookRunServiceImpl) postMessageToThreadAndSaveRootID(playbookRunID, channelID string, post *model.Post) error {
channelIDsToRootIDs, err := s.store.GetBroadcastChannelIDsToRootIDs(playbookRunID)
if err != nil {
return errors.Wrapf(err, "error when trying to retrieve ChannelIDsToRootIDs map for playbookRunId '%s'", playbookRunID)
}
err = s.poster.PostMessageToThread(channelIDsToRootIDs[channelID], post)
if err != nil {
return errors.Wrapf(err, "failed to PostMessageToThread for channelID '%s'", channelID)
}
newRootID := post.RootId
if newRootID == "" {
newRootID = post.Id
}
if newRootID != channelIDsToRootIDs[channelID] {
channelIDsToRootIDs[channelID] = newRootID
if err = s.store.SetBroadcastChannelIDsToRootID(playbookRunID, channelIDsToRootIDs); err != nil {
return errors.Wrapf(err, "failed to SetBroadcastChannelIDsToRootID for playbookID '%s'", playbookRunID)
}
}
return nil
}
// Follow method lets user follow a specific playbook run
func (s *PlaybookRunServiceImpl) Follow(playbookRunID, userID string) error {
if err := s.store.Follow(playbookRunID, userID); err != nil {
return errors.Wrapf(err, "user `%s` failed to follow the run `%s`", userID, playbookRunID)
}
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.Follow(playbookRun, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs([]string{userID}))
return nil
}
// UnFollow method lets user unfollow a specific playbook run
func (s *PlaybookRunServiceImpl) Unfollow(playbookRunID, userID string) error {
if err := s.store.Unfollow(playbookRunID, userID); err != nil {
return errors.Wrapf(err, "user `%s` failed to unfollow the run `%s`", userID, playbookRunID)
}
playbookRun, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.Unfollow(playbookRun, userID)
s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs([]string{userID}))
return nil
}
// GetFollowers returns list of followers for a specific playbook run
func (s *PlaybookRunServiceImpl) GetFollowers(playbookRunID string) ([]string, error) {
var followers []string
var err error
if followers, err = s.store.GetFollowers(playbookRunID); err != nil {
return nil, errors.Wrapf(err, "failed to get followers for the run `%s`", playbookRunID)
}
return followers, nil
}
func getUserDisplayName(user *model.User) string {
if user == nil {
return ""
}
if user.FirstName != "" && user.LastName != "" {
return fmt.Sprintf("%s %s", user.FirstName, user.LastName)
}
return fmt.Sprintf("@%s", user.Username)
}
func cleanChannelName(channelName string) string {
// Lower case only
channelName = strings.ToLower(channelName)
// Trim spaces
channelName = strings.TrimSpace(channelName)
// Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes
channelName = strings.ReplaceAll(channelName, "-", " ")
channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "")
channelName = strings.ReplaceAll(channelName, " ", "-")
// Remove all leading and trailing dashes
channelName = strings.Trim(channelName, "-")
return channelName
}
func addRandomBits(name string) string {
// Fix too long names (we're adding 5 chars):
if len(name) > 59 {
name = name[:59]
}
randBits := model.NewId()
return fmt.Sprintf("%s-%s", name, randBits[:4])
}
func findNewestNonDeletedStatusPost(posts []StatusPost) *StatusPost {
var newest *StatusPost
for i, p := range posts {
if p.DeleteAt == 0 && (newest == nil || p.CreateAt > newest.CreateAt) {
newest = &posts[i]
}
}
return newest
}
func findNewestNonDeletedPostID(posts []StatusPost) string {
newest := findNewestNonDeletedStatusPost(posts)
if newest == nil {
return ""
}
return newest.ID
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Helper function to Trigger webhooks
func triggerWebhooks(s *PlaybookRunServiceImpl, webhooks []string, body []byte) {
for i := range webhooks {
url := webhooks[i]
go func() {
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
logrus.WithError(err).WithField("webhook_url", url).Error("failed to create a POST request to webhook URL")
return
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
logrus.WithError(err).WithField("webhook_url", url).Warn("failed to send a POST request to webhook URL")
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
err := errors.Errorf("response code is %d; expected a status code in the 2xx range", resp.StatusCode)
logrus.WithError(err).WithField("webhook_url", url).Warn("failed to finish a POST request to webhook URL")
}
}()
}
}
func buildAssignedTaskMessageSummary(runs []AssignedRun, locale string, timezone *time.Location, onlyTasksDueUntilToday bool) string {
var msg strings.Builder
T := i18n.GetUserTranslations(locale)
total := 0
for _, run := range runs {
total += len(run.Tasks)
}
msg.WriteString("##### ")
msg.WriteString(T("app.user.digest.tasks.heading"))
msg.WriteString("\n")
if total == 0 {
msg.WriteString(T("app.user.digest.tasks.zero_assigned"))
msg.WriteString("\n")
return msg.String()
}
var tasksNoDueDate, tasksDoAfterToday int
currentTime := timeutils.GetTimeForMillis(model.GetMillis()).In(timezone)
yesterday := currentTime.Add(-24 * time.Hour)
var runsInfo strings.Builder
for _, run := range runs {
var tasksInfo strings.Builder
for _, task := range run.Tasks {
// no due date
if task.ChecklistItem.DueDate == 0 {
// add information about tasks without due date only if the full list was requested
if !onlyTasksDueUntilToday {
tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s\n", task.ChecklistTitle, task.Title))
}
tasksNoDueDate++
continue
}
dueTime := time.Unix(task.ChecklistItem.DueDate/1000, 0).In(timezone)
// due today
if timeutils.IsSameDay(dueTime, currentTime) {
tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_today")))
continue
}
// due yesterday
if timeutils.IsSameDay(dueTime, yesterday) {
tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_yesterday")))
continue
}
// due before yesterday
if dueTime.Before(currentTime) {
days := timeutils.GetDaysDiff(dueTime, currentTime)
tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_x_days_ago", days)))
continue
}
// due after today
if !onlyTasksDueUntilToday {
days := timeutils.GetDaysDiff(currentTime, dueTime)
tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s `%s`\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_in_x_days", days)))
}
tasksDoAfterToday++
}
// omit run's title if tasks info is empty
if tasksInfo.String() != "" {
runsInfo.WriteString(fmt.Sprintf("[%s](%s?from=digest_assignedtask)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID)))
runsInfo.WriteString(tasksInfo.String())
}
}
// if we need tasks due now and there are only tasks that are due after today or without due date, skip a message
if onlyTasksDueUntilToday && tasksDoAfterToday+tasksNoDueDate == total {
return ""
}
// add title
if onlyTasksDueUntilToday {
msg.WriteString(T("app.user.digest.tasks.num_assigned_due_until_today", total-tasksDoAfterToday))
} else {
msg.WriteString(T("app.user.digest.tasks.num_assigned", total))
}
// add info about tasks
msg.WriteString("\n\n")
msg.WriteString(runsInfo.String())
// add summary info for tasks without a due date or due date after today
if tasksDoAfterToday > 0 && onlyTasksDueUntilToday {
msg.WriteString(":information_source: ")
msg.WriteString(T("app.user.digest.tasks.due_after_today", tasksDoAfterToday))
msg.WriteString(" ")
msg.WriteString(T("app.user.digest.tasks.all_tasks_command"))
}
return msg.String()
}
func buildRunsInProgressMessage(runs []RunLink, locale string) string {
T := i18n.GetUserTranslations(locale)
total := len(runs)
msg := "\n"
msg += "##### " + T("app.user.digest.runs_in_progress.heading") + "\n"
if total == 0 {
return msg + T("app.user.digest.runs_in_progress.zero_in_progress") + "\n"
}
msg += T("app.user.digest.runs_in_progress.num_in_progress", total) + "\n"
for _, run := range runs {
msg += fmt.Sprintf("- [%s](%s?from=digest_runsinprogress)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID))
}
return msg
}
func buildRunsOverdueMessage(runs []RunLink, locale string) string {
T := i18n.GetUserTranslations(locale)
total := len(runs)
msg := "\n"
msg += "##### " + T("app.user.digest.overdue_status_updates.heading") + "\n"
if total == 0 {
return msg + T("app.user.digest.overdue_status_updates.zero_overdue") + "\n"
}
msg += T("app.user.digest.overdue_status_updates.num_overdue", total) + "\n"
for _, run := range runs {
msg += fmt.Sprintf("- [%s](%s?from=digest_overduestatus)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID))
}
return msg
}
type messageType string
const (
creationMessage messageType = "creation"
finishMessage messageType = "finish"
overdueStatusUpdateMessage messageType = "overdue status update"
restoreMessage messageType = "restore"
retroMessage messageType = "retrospective"
statusUpdateMessage messageType = "status update"
)
// broadcasting to channels
func (s *PlaybookRunServiceImpl) broadcastPlaybookRunMessageToChannels(channelIDs []string, post *model.Post, mType messageType, playbookRun *PlaybookRun, logger logrus.FieldLogger) {
logger = logger.WithField("message_type", mType)
for _, broadcastChannelID := range channelIDs {
post.Id = "" // Reset the ID so we avoid cloning the whole object
if err := s.broadcastPlaybookRunMessage(broadcastChannelID, post, mType, playbookRun); err != nil {
logger.WithError(err).Error("failed to broadcast run to channel")
if _, err = s.poster.PostMessage(playbookRun.ChannelID, fmt.Sprintf("Failed to broadcast run %s to the configured channel.", mType)); err != nil {
logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("failed to post failure message to the channel")
}
}
}
}
func (s *PlaybookRunServiceImpl) broadcastPlaybookRunMessage(broadcastChannelID string, post *model.Post, mType messageType, playbookRun *PlaybookRun) error {
post.ChannelId = broadcastChannelID
if err := IsChannelActiveInTeam(post.ChannelId, playbookRun.TeamID, s.api); err != nil {
return errors.Wrap(err, "announcement channel is not active")
}
if err := s.postMessageToThreadAndSaveRootID(playbookRun.ID, post.ChannelId, post); err != nil {
return errors.Wrapf(err, "error posting '%s' message, for playbook '%s', to channelID '%s'", mType, playbookRun.ID, post.ChannelId)
}
return nil
}
// dm to users who follow
func (s *PlaybookRunServiceImpl) dmPostToRunFollowers(post *model.Post, mType messageType, playbookRunID, authorID string) error {
followers, err := s.GetFollowers(playbookRunID)
if err != nil {
return errors.Wrap(err, "failed to get followers")
}
s.dmPostToUsersWithPermission(followers, post, playbookRunID, authorID)
return nil
}
func (s *PlaybookRunServiceImpl) dmPostToAutoFollows(post *model.Post, playbookID, playbookRunID, authorID string) error {
autoFollows, err := s.playbookService.GetAutoFollows(playbookID)
if err != nil {
return errors.Wrap(err, "failed to get auto follows")
}
s.dmPostToUsersWithPermission(autoFollows, post, playbookRunID, authorID)
return nil
}
func (s *PlaybookRunServiceImpl) dmPostToUsersWithPermission(users []string, post *model.Post, playbookRunID, authorID string) {
logger := logrus.WithFields(logrus.Fields{"playbook_run_id": playbookRunID})
for _, user := range users {
// Do not send update to the author
if user == authorID {
continue
}
// Check for access permissions
if err := s.permissions.RunView(user, playbookRunID); err != nil {
continue
}
post.Id = "" // Reset the ID so we avoid cloning the whole object
post.RootId = ""
if err := s.poster.DM(user, post); err != nil {
logger.WithError(err).WithField("user_id", user).Warn("failed to broadcast post to the user")
}
}
}
func (s *PlaybookRunServiceImpl) MessageHasBeenPosted(post *model.Post) {
runIDs, err := s.store.GetPlaybookRunIDsForChannel(post.ChannelId)
if err != nil {
if errors.Is(err, ErrNotFound) {
return
}
logrus.WithError(err).WithFields(logrus.Fields{
"post_id": post.Id,
"channel_id": post.ChannelId,
}).Error("unable retrieve run ID from post")
return
}
for _, runID := range runIDs {
// Get run
run, err := s.GetPlaybookRun(runID)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"run_id": runID,
}).Error("unable retrieve run from ID")
return
}
for checklistNum, checklist := range run.Checklists {
for itemNum, item := range checklist.Items {
for _, ta := range item.TaskActions {
if ta.Trigger.Type == KeywordsByUsersTriggerType {
t, err := NewKeywordsByUsersTrigger(ta.Trigger)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"type": ta.Trigger.Type,
"checklistNum": checklistNum,
"itemNum": itemNum,
}).Error("unable to decode trigger")
return
}
if t.IsTriggered(post) {
s.genericTelemetry.Track(
telemetryTaskActionsTriggered,
map[string]any{
"trigger": ta.Trigger.Type,
"playbookrun_id": runID,
},
)
err := s.doActions(ta.Actions, runID, post.UserId, ChecklistItemStateClosed, checklistNum, itemNum)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"checklistNum": checklistNum,
"itemNum": itemNum,
}).Error("can't process task actions")
return
}
}
}
}
}
}
}
}
func (s *PlaybookRunServiceImpl) doActions(taskActions []Action, runID string, userID string, ChecklistItemStateClosed string, checklistNum int, itemNum int) error {
for _, action := range taskActions {
if action.Type == MarkItemAsDoneActionType {
a, err := NewMarkItemAsDoneAction(action)
if err != nil {
return errors.Wrapf(err, "unable to decode action")
}
if a.Payload.Enabled {
if err := s.ModifyCheckedState(runID, userID, ChecklistItemStateClosed, checklistNum, itemNum); err != nil {
return errors.Wrapf(err, "can't mark item as done")
}
}
}
s.genericTelemetry.Track(
telemetryTaskActionsActionExecuted,
map[string]any{
"action": action.Type,
"playbookrun_id": runID,
},
)
}
return nil
}
// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following
func (s *PlaybookRunServiceImpl) GetPlaybookRunIDsForUser(userID string) ([]string, error) {
return s.store.GetPlaybookRunIDsForUser(userID)
}
// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs.
func (s *PlaybookRunServiceImpl) GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error) {
return s.store.GetRunMetadataByIDs(runIDs)
}
// GetTaskMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs
func (s *PlaybookRunServiceImpl) GetTaskMetadataByIDs(taskIDs []string) ([]TopicMetadata, error) {
return s.store.GetTaskAsTopicMetadataByIDs(taskIDs)
}
// GetStatusMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs
func (s *PlaybookRunServiceImpl) GetStatusMetadataByIDs(statusIDs []string) ([]TopicMetadata, error) {
return s.store.GetStatusAsTopicMetadataByIDs(statusIDs)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/metrics"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
const (
playbookCreatedWSEvent = "playbook_created"
playbookArchivedWSEvent = "playbook_archived"
playbookRestoredWSEvent = "playbook_restored"
)
type playbookService struct {
store PlaybookStore
poster bot.Poster
telemetry PlaybookTelemetry
api playbooks.ServicesAPI
metricsService *metrics.Metrics
}
// NewPlaybookService returns a new playbook service
func NewPlaybookService(store PlaybookStore, poster bot.Poster, telemetry PlaybookTelemetry, api playbooks.ServicesAPI, metricsService *metrics.Metrics) PlaybookService {
return &playbookService{
store: store,
poster: poster,
telemetry: telemetry,
api: api,
metricsService: metricsService,
}
}
func (s *playbookService) Create(playbook Playbook, userID string) (string, error) {
playbook.CreateAt = model.GetMillis()
playbook.UpdateAt = playbook.CreateAt
newID, err := s.store.Create(playbook)
if err != nil {
return "", err
}
playbook.ID = newID
s.telemetry.CreatePlaybook(playbook, userID)
s.poster.PublishWebsocketEventToTeam(playbookCreatedWSEvent, map[string]interface{}{
"teamID": playbook.TeamID,
}, playbook.TeamID)
s.metricsService.IncrementPlaybookCreatedCount(1)
return newID, nil
}
func (s *playbookService) Import(playbook Playbook, userID string) (string, error) {
newID, err := s.Create(playbook, userID)
if err != nil {
return "", err
}
playbook.ID = newID
s.telemetry.ImportPlaybook(playbook, userID)
return newID, nil
}
func (s *playbookService) Get(id string) (Playbook, error) {
return s.store.Get(id)
}
func (s *playbookService) GetPlaybooks() ([]Playbook, error) {
return s.store.GetPlaybooks()
}
func (s *playbookService) GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error) {
return s.store.GetPlaybooksForTeam(requesterInfo, teamID, opts)
}
func (s *playbookService) Update(playbook Playbook, userID string) error {
if playbook.DeleteAt != 0 {
return errors.New("cannot update a playbook that is archived")
}
playbook.UpdateAt = model.GetMillis()
if err := s.store.Update(playbook); err != nil {
return err
}
s.telemetry.UpdatePlaybook(playbook, userID)
return nil
}
func (s *playbookService) Archive(playbook Playbook, userID string) error {
if playbook.ID == "" {
return errors.New("can't archive a playbook without an ID")
}
if err := s.store.Archive(playbook.ID); err != nil {
return err
}
s.telemetry.DeletePlaybook(playbook, userID)
s.metricsService.IncrementPlaybookArchivedCount(1)
s.poster.PublishWebsocketEventToTeam(playbookArchivedWSEvent, map[string]interface{}{
"teamID": playbook.TeamID,
}, playbook.TeamID)
return nil
}
func (s *playbookService) Restore(playbook Playbook, userID string) error {
if playbook.ID == "" {
return errors.New("can't restore a playbook without an ID")
}
if playbook.DeleteAt == 0 {
return nil
}
if err := s.store.Restore(playbook.ID); err != nil {
return err
}
s.telemetry.RestorePlaybook(playbook, userID)
s.metricsService.IncrementPlaybookRestoredCount(1)
s.poster.PublishWebsocketEventToTeam(playbookRestoredWSEvent, map[string]interface{}{
"teamID": playbook.TeamID,
}, playbook.TeamID)
return nil
}
// AutoFollow method lets user to auto-follow all runs of a specific playbook
func (s *playbookService) AutoFollow(playbookID, userID string) error {
if err := s.store.AutoFollow(playbookID, userID); err != nil {
return errors.Wrapf(err, "user `%s` failed to auto-follow the playbook `%s`", userID, playbookID)
}
playbook, err := s.store.Get(playbookID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.AutoFollowPlaybook(playbook, userID)
return nil
}
// AutoUnfollow method lets user to not auto-follow the newly created playbook runs
func (s *playbookService) AutoUnfollow(playbookID, userID string) error {
if err := s.store.AutoUnfollow(playbookID, userID); err != nil {
return errors.Wrapf(err, "user `%s` failed to auto-unfollow the playbook `%s`", userID, playbookID)
}
playbook, err := s.store.Get(playbookID)
if err != nil {
return errors.Wrap(err, "failed to retrieve playbook run")
}
s.telemetry.AutoUnfollowPlaybook(playbook, userID)
return nil
}
// GetAutoFollows returns list of users who auto-follow a playbook
func (s *playbookService) GetAutoFollows(playbookID string) ([]string, error) {
autoFollows, err := s.store.GetAutoFollows(playbookID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get auto-follows for the playbook `%s`", playbookID)
}
return autoFollows, nil
}
// Duplicate duplicates a playbook
func (s *playbookService) Duplicate(playbook Playbook, userID string) (string, error) {
logger := logrus.WithFields(logrus.Fields{
"original_playbook_id": playbook.ID,
"user_id": userID,
})
newPlaybook := playbook.Clone()
newPlaybook.ID = ""
// Empty metric IDs if there are such. Otherwise, metrics will not be saved in the database.
for i := range newPlaybook.Metrics {
newPlaybook.Metrics[i].ID = ""
}
newPlaybook.Title = "Copy of " + playbook.Title
// On duplicating, make the current user the administrator.
newPlaybook.Members = []PlaybookMember{{
UserID: userID,
Roles: []string{PlaybookRoleMember, PlaybookRoleAdmin},
}}
playbookID, err := s.Create(newPlaybook, userID)
if err != nil {
return "", err
}
logger.WithField("playbook_id", playbookID).Debug("Duplicated playbook")
return playbookID, nil
}
// get top playbooks for teams
func (s *playbookService) GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) {
permissionFlag, err := licenseAndGuestCheck(s, userID, false)
if err != nil {
return nil, err
}
if !permissionFlag {
return nil, errors.New("User cannot access playbooks insights")
}
return s.store.GetTopPlaybooksForTeam(teamID, userID, opts)
}
// get top playbooks for users
func (s *playbookService) GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) {
permissionFlag, err := licenseAndGuestCheck(s, userID, true)
if err != nil {
return nil, err
}
if !permissionFlag {
return nil, errors.New("User cannot access playbooks insights")
}
return s.store.GetTopPlaybooksForUser(teamID, userID, opts)
}
func licenseAndGuestCheck(s *playbookService, userID string, isMyInsights bool) (bool, error) {
licenseError := errors.New("invalid license/authorization to use insights API")
guestError := errors.New("Guests aren't authorized to use insights API")
lic := s.api.GetLicense()
user, err := s.api.GetUserByID(userID)
if err != nil {
return false, err
}
if user.IsGuest() {
return false, guestError
}
if lic == nil && !isMyInsights {
return false, licenseError
}
if !isMyInsights && (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
return false, licenseError
}
return true, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/pkg/errors"
)
var (
ErrChannelNotFound = errors.Errorf("channel not found")
ErrChannelDeleted = errors.Errorf("channel deleted")
ErrChannelNotInExpectedTeam = errors.Errorf("channel in different team")
)
func IsChannelActiveInTeam(channelID string, expectedTeamID string, api playbooks.ServicesAPI) error {
channel, err := api.GetChannelByID(channelID)
if err != nil {
return errors.Wrapf(ErrChannelNotFound, "channel with ID %s does not exist", channelID)
}
if channel.DeleteAt != 0 {
return errors.Wrapf(ErrChannelDeleted, "channel with ID %s is archived", channelID)
}
if channel.TeamId != expectedTeamID {
return errors.Wrapf(ErrChannelNotInExpectedTeam,
"channel with ID %s is on team with ID %s; expected team ID is %s",
channelID,
channel.TeamId,
expectedTeamID,
)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"time"
)
func ShouldSendWeeklyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool {
if userInfo.DigestNotificationSettings.DisableWeeklyDigest {
return false
}
lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone)
currentYear, currentWeek := currentTime.ISOWeek()
lastSentYear, lastSentWeek := lastSentTime.ISOWeek()
isFirstLoginOfTheWeek := currentYear != lastSentYear || currentWeek != lastSentWeek
return isFirstLoginOfTheWeek
}
func ShouldSendDailyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool {
if userInfo.DigestNotificationSettings.DisableDailyDigest {
return false
}
// DM message if it's the next day and been more than an hour since the last post
// Hat tip to Github plugin for the logic.
lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone)
isMoreThanOneHourPassed := currentTime.Sub(lastSentTime).Hours() >= 1
isDifferentDay := currentTime.Day() != lastSentTime.Day() ||
currentTime.Month() != lastSentTime.Month() ||
currentTime.Year() != lastSentTime.Year()
return isMoreThanOneHourPassed && isDifferentDay
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const RetrospectivePrefix = "retro_"
// HandleReminder is the handler for all reminder events.
func (s *PlaybookRunServiceImpl) HandleReminder(key string) {
if strings.HasPrefix(key, RetrospectivePrefix) {
s.handleReminderToFillRetro(strings.TrimPrefix(key, RetrospectivePrefix))
} else {
s.handleStatusUpdateReminder(key)
}
}
func (s *PlaybookRunServiceImpl) handleReminderToFillRetro(playbookRunID string) {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToRemind, err := s.GetPlaybookRun(playbookRunID)
if err != nil {
logger.WithError(err).Errorf("handleReminderToFillRetro failed to get playbook run")
return
}
// In the meantime we did publish a retrospective, so no reminder.
if playbookRunToRemind.RetrospectivePublishedAt != 0 {
return
}
// If we are not in the finished state then don't remind
if playbookRunToRemind.CurrentStatus != StatusFinished {
return
}
if err = s.postRetrospectiveReminder(playbookRunToRemind, false); err != nil {
logger.WithError(err).Errorf("couldn't post reminder")
return
}
// Jobs can't be rescheduled within themselves with the same key. As a temporary workaround do it in a delayed goroutine
go func() {
time.Sleep(time.Second * 2)
if err = s.SetReminder(RetrospectivePrefix+playbookRunID, time.Duration(playbookRunToRemind.RetrospectiveReminderIntervalSeconds)*time.Second); err != nil {
logger.WithError(err).Errorf("failed to reocurr retrospective reminder")
return
}
}()
}
func (s *PlaybookRunServiceImpl) handleStatusUpdateReminder(playbookRunID string) {
logger := logrus.WithField("playbook_run_id", playbookRunID)
playbookRunToModify, err := s.GetPlaybookRun(playbookRunID)
if err != nil {
logger.WithError(err).Error("HandleReminder failed to get playbook run")
return
}
owner, err := s.api.GetUserByID(playbookRunToModify.OwnerUserID)
if err != nil {
logger.WithError(err).WithField("user_id", playbookRunToModify.OwnerUserID).Error("HandleReminder failed to get owner")
return
}
attachments := []*model.SlackAttachment{
{
Actions: []*model.PostAction{
{
Type: "button",
Name: "Update status",
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/reminder/button-update",
"playbooks",
playbookRunToModify.ID),
},
},
},
},
}
post := &model.Post{
Message: fmt.Sprintf("@%s, please provide a status update for [%s](%s).", owner.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID)),
ChannelId: playbookRunToModify.ChannelID,
Type: "custom_update_status",
Props: map[string]any{
"targetUsername": owner.Username,
"playbookRunId": playbookRunToModify.ID,
},
}
model.ParseSlackAttachment(post, attachments)
if err = s.poster.PostMessageToThread("", post); err != nil {
logger.WithError(err).Errorf("HandleReminder error posting reminder message")
return
}
// broadcast to followers
message, err := s.buildOverdueStatusUpdateMessage(playbookRunToModify, owner.Username)
if err != nil {
logger.WithError(err).Error("failed to build overdue status update message")
} else {
err = s.dmPostToRunFollowers(&model.Post{Message: message}, overdueStatusUpdateMessage, playbookRunToModify.ID, "")
if err != nil {
logger.WithError(err).Error("failed to dm post to run followers")
}
}
playbookRunToModify.ReminderPostID = post.Id
if _, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil {
logger.WithError(err).Error("error updating with reminder post id")
}
}
func (s *PlaybookRunServiceImpl) buildOverdueStatusUpdateMessage(playbookRun *PlaybookRun, ownerUserName string) (string, error) {
channel, err := s.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
return "", errors.Wrapf(err, "can't get channel - %s", playbookRun.ChannelID)
}
team, err := s.api.GetTeam(channel.TeamId)
if err != nil {
return "", errors.Wrapf(err, "can't get team - %s", channel.TeamId)
}
message := fmt.Sprintf("Status update is overdue for [%s](/%s/channels/%s?telem_action=todo_overduestatus_clicked&telem_run_id=%s&forceRHSOpen) (Owner: @%s)\n",
channel.DisplayName, team.Name, channel.Name, playbookRun.ID, ownerUserName)
return message, nil
}
// SetReminder sets a reminder. After timeInMinutes in the future, the owner will be
// reminded to update the playbook run's status.
func (s *PlaybookRunServiceImpl) SetReminder(playbookRunID string, fromNow time.Duration) error {
if _, err := s.scheduler.ScheduleOnce(playbookRunID, time.Now().Add(fromNow)); err != nil {
return errors.Wrap(err, "unable to schedule reminder")
}
return nil
}
// RemoveReminder removes the pending reminder for the given playbook run, if any.
func (s *PlaybookRunServiceImpl) RemoveReminder(playbookRunID string) {
s.scheduler.Cancel(playbookRunID)
}
// resetReminderTimer sets the previous reminder timer to 0.
func (s *PlaybookRunServiceImpl) resetReminderTimer(playbookRunID string) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrapf(err, "failed to retrieve playbook run")
}
playbookRunToModify.PreviousReminder = 0
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer")
}
s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID)
return nil
}
// ResetReminder creates a timeline event for a reminder being reset and then creates a new reminder
func (s *PlaybookRunServiceImpl) ResetReminder(playbookRunID string, newReminder time.Duration) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrapf(err, "failed to retrieve playbook run")
}
eventTime := model.GetMillis()
event := &TimelineEvent{
PlaybookRunID: playbookRunToModify.ID,
CreateAt: eventTime,
EventAt: eventTime,
EventType: StatusUpdateSnoozed,
SubjectUserID: playbookRunToModify.ReporterUserID,
}
if _, err := s.store.CreateTimelineEvent(event); err != nil {
return errors.Wrapf(err, "failed to create timeline event after resetting reminder timer")
}
return s.SetNewReminder(playbookRunID, newReminder)
}
// SetNewReminder sets a new reminder for playbookRunID, removes any pending reminder, removes the
// reminder post in the playbookRun's channel, and resets the PreviousReminder and
// LastStatusUpdateAt (so the countdown timer to "update due" shows the correct time)
func (s *PlaybookRunServiceImpl) SetNewReminder(playbookRunID string, newReminder time.Duration) error {
playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID)
if err != nil {
return errors.Wrapf(err, "failed to retrieve playbook run")
}
// Remove pending reminder (if any)
s.RemoveReminder(playbookRunID)
// Remove reminder post (if any)
if playbookRunToModify.ReminderPostID != "" {
if err = s.removePost(playbookRunToModify.ReminderPostID); err != nil {
return err
}
playbookRunToModify.ReminderPostID = ""
}
playbookRunToModify.PreviousReminder = newReminder
playbookRunToModify.LastStatusUpdateAt = model.GetMillis()
playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify)
if err != nil {
return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer")
}
if newReminder != 0 {
if err = s.SetReminder(playbookRunID, newReminder); err != nil {
return errors.Wrap(err, "failed to set the reminder for playbook run")
}
}
s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID)
return nil
}
func (s *PlaybookRunServiceImpl) removePost(postID string) error {
post, err := s.api.GetPost(postID)
if err != nil {
return errors.Wrapf(err, "failed to retrieve reminder post %s", postID)
}
if post.DeleteAt != 0 {
return nil
}
if _, err = s.api.DeletePost(postID); err != nil {
return errors.Wrapf(err, "failed to delete reminder post %s", postID)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
// SortField enumerates the available fields we can sort on.
type SortField string
const (
// SortByTitle sorts by the title field of a playbook.
SortByTitle SortField = "title"
// SortByStages sorts by the number of checklists in a playbook.
SortByStages SortField = "stages"
// SortBySteps sorts by the number of steps in a playbook.
SortBySteps SortField = "steps"
// SortByRuns sorts by the number of times a playbook has been run.
SortByRuns SortField = "runs"
// SortByCreateAt sorts by the created time of a playbook or playbook run.
SortByCreateAt SortField = "create_at"
// SortByID sorts by the primary key of a playbook or playbook run.
SortByID SortField = "id"
// SortByName sorts by the name of a playbook run.
SortByName SortField = "name"
// SortByOwnerUserID sorts by the user id of the owner of a playbook run.
SortByOwnerUserID SortField = "owner_user_id"
// SortByTeamID sorts by the team id of a playbook or playbook run.
SortByTeamID SortField = "team_id"
// SortByEndAt sorts by the end time of a playbook run.
SortByEndAt SortField = "end_at"
// SortByStatus sorts by the status of a playbook run.
SortByStatus SortField = "status"
// SortByLastStatusUpdateAt sorts by when the playbook run was last updated.
SortByLastStatusUpdateAt SortField = "last_status_update_at"
// SortByLastStatusUpdateAt sorts by when the playbook was last run.
SortByLastRunAt SortField = "last_run_at"
// SortByActiveRuns sorts by number of active runs in the playbook.
SortByActiveRuns SortField = "active_runs"
// SortByMetric0 ..3 sorts by the playbook's metric index
SortByMetric0 SortField = "metric0"
SortByMetric1 SortField = "metric1"
SortByMetric2 SortField = "metric2"
SortByMetric3 SortField = "metric3"
)
// SortDirection is the type used to specify the ascending or descending order of returned results.
type SortDirection string
const (
// DirectionDesc is descending order.
DirectionDesc SortDirection = "DESC"
// DirectionAsc is ascending order.
DirectionAsc SortDirection = "ASC"
)
func IsValidDirection(direction SortDirection) bool {
return direction == DirectionAsc || direction == DirectionDesc
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type TaskAction struct {
Trigger Trigger `json:"trigger"`
Actions []Action `json:"actions"`
}
type TaskActionType string
type TaskTriggerType string
type Trigger struct {
Type TaskTriggerType `json:"type"`
// Payload is the json payload that stores trigger specific settings or config.
// This should be unmarshalled into a concrete type during usage
Payload string `json:"payload"`
}
type Action struct {
Type TaskActionType `json:"type"`
// Payload is the json payload that stores action specific settings or config.
// This should be unmarshalled into a concrete type during usage
Payload string `json:"payload"`
}
// Known Types
const (
KeywordsByUsersTriggerType TaskTriggerType = "keywords_by_users"
MarkItemAsDoneActionType TaskActionType = "mark_item_as_done"
)
var (
ValidTaskActionTypes = []TaskActionType{
MarkItemAsDoneActionType,
}
)
// Triggers
type KeywordsByUsersTrigger struct {
typ TaskTriggerType
Payload KeywordsByUsersTriggerPayload
}
type KeywordsByUsersTriggerPayload struct {
Keywords []string `json:"keywords" mapstructure:"keywords"`
UserIDs []string `json:"user_ids" mapstructure:"user_ids"`
}
func NewKeywordsByUsersTrigger(trigger Trigger) (*KeywordsByUsersTrigger, error) {
if trigger.Type != KeywordsByUsersTriggerType {
return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", trigger.Type, KeywordsByUsersTriggerType)
}
var t KeywordsByUsersTrigger
t.typ = KeywordsByUsersTriggerType
if err := json.Unmarshal([]byte(trigger.Payload), &t.Payload); err != nil {
return nil, errors.New("unable to decode payload from trigger")
}
return &t, nil
}
func (t *KeywordsByUsersTrigger) IsValid() error {
return nil
}
func (t *KeywordsByUsersTrigger) IsTriggered(post *model.Post) bool {
foundUser := false
if len(t.Payload.UserIDs) > 0 {
for _, userID := range t.Payload.UserIDs {
if post.UserId == userID {
foundUser = true
break
}
}
} else {
foundUser = true
}
if foundUser {
for _, keyword := range t.Payload.Keywords {
if strings.Contains(post.Message, keyword) {
logrus.WithField("keyword", keyword)
return true
}
}
}
return false
}
// Actions
type MarkItemAsDoneAction struct {
typ TaskActionType
Payload MarkItemAsDoneActionPayload
}
type MarkItemAsDoneActionPayload struct {
Enabled bool `json:"enabled"`
}
func NewMarkItemAsDoneAction(action Action) (*MarkItemAsDoneAction, error) {
if action.Type != MarkItemAsDoneActionType {
return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", action.Type, MarkItemAsDoneActionType)
}
var a MarkItemAsDoneAction
a.typ = MarkItemAsDoneActionType
if err := json.Unmarshal([]byte(action.Payload), &a.Payload); err != nil {
return nil, errors.New("unable to decode payload from trigger")
}
return &a, nil
}
func (a *MarkItemAsDoneAction) IsValid() error {
return nil
}
// Validators
func ValidateTrigger(t Trigger) error {
switch t.Type {
case KeywordsByUsersTriggerType:
trigger, err := NewKeywordsByUsersTrigger(t)
if err != nil {
return err
}
return trigger.IsValid()
default:
return errors.Errorf("Unknown task trigger type: %s", t.Type)
}
}
func ValidateAction(a Action) error {
switch a.Type {
case MarkItemAsDoneActionType:
action, err := NewMarkItemAsDoneAction(a)
if err != nil {
return err
}
return action.IsValid()
default:
return errors.Errorf("Unknown task action type: %s", a.Type)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "fmt"
// GenericTelemetry is the generic interface for telemetry.
type GenericTelemetry interface {
Page(name TelemetryPage, properties map[string]interface{})
Track(name TelemetryTrack, properties map[string]interface{})
}
// TelemetryType is the type for the different kinds of tracking we have
type TelemetryType string
const (
// TelemetryTypeTrack is for tracking events (click, submit, etc..)
TelemetryTypeTrack TelemetryType = "track"
// TelemetryTypePage is for tracking page views
TelemetryTypePage TelemetryType = "page"
)
// TelemetryTrack is a type alias to hold all possible
// event tracking names in an enum-like
//
// Contained names should match the ones that are at webapp/src/types/telemetry.ts
// when they use generic tracking
type TelemetryTrack int
const (
telemetryRunFollow TelemetryTrack = iota
telemetryRunUnfollow
telemetryRunCreate
telemetryRunParticipate
telemetryRunLeave
telemetryRunUpdateActions
telemetryTaskActionsTriggered
telemetryTaskActionsActionExecuted
telemetryTaskActionsUpdated
)
var trackTypes = [...]string{
telemetryRunFollow: "playbookrun_follow",
telemetryRunUnfollow: "playbookrun_unfollow",
telemetryRunCreate: "playbookrun_create",
telemetryRunParticipate: "playbookrun_participate",
telemetryRunLeave: "playbookrun_leave",
telemetryRunUpdateActions: "playbookrun_update_actions",
telemetryTaskActionsUpdated: "taskactions_updated",
telemetryTaskActionsTriggered: "taskactions_triggered",
telemetryTaskActionsActionExecuted: "taskactions_action_executed",
}
// String creates the string version of the TelemetryTrack
func (tt TelemetryTrack) String() string {
return trackTypes[tt]
}
// TelemetryPage is a type alias to hold all possible
// page tracking names in an enum-like
//
// Contained names should match the ones that are at webapp/src/types/telemetry.ts
// when they use generic tracking
type TelemetryPage int
const (
telemetryRunStatusUpdate TelemetryPage = iota
telemetryRunDetails
telemetryTaskInbox
telemetryChannelsRunDetails
telemetryChannelsHome
telemetryChannelsRunList
)
var pageTypes = [...]string{
telemetryRunStatusUpdate: "run_status_update",
telemetryTaskInbox: "task_inbox",
telemetryRunDetails: "run_details", // Backstage RDP
telemetryChannelsRunDetails: "channels_rhs_rundetails", // RHS details
telemetryChannelsHome: "channels_rhs_home", // RHS templates list
telemetryChannelsRunList: "channels_rhs_runlist", // RHS runs list
}
// String creates the string version of the Telemetrypage
func (tp TelemetryPage) String() string {
return pageTypes[tp]
}
// NewTelemetryPage creates an instance of TelemetryPage from a string.
// It's useful to validate that the arbitrary string has a equivalent constant
// for what pages we want to track (and avoid typos).
func NewTelemetryPage(name string) (*TelemetryPage, error) {
for i, ct := range pageTypes {
if ct == name {
tp := TelemetryPage(i)
return &tp, nil
}
}
return nil, fmt.Errorf("unknown page type: %s", name)
}
// NewTelemetryTrack creates an instance of TelemetryTrack from a string.
// It's useful to validate that the arbitrary string has a equivalent constant
// for what events we want to track (and avoid typos).
func NewTelemetryTrack(name string) (*TelemetryTrack, error) {
for i, ct := range trackTypes {
if ct == name {
tt := TelemetryTrack(i)
return &tt, nil
}
}
return nil, fmt.Errorf("unknown track type: %s", name)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "fmt"
const (
PlaybooksPath = "/playbooks/playbooks"
RunsPath = "/playbooks/runs"
)
// relative urls
func GetRunDetailsRelativeURL(playbookRunID string) string {
return fmt.Sprintf("%s/%s", RunsPath, playbookRunID)
}
func GetPlaybookDetailsRelativeURL(playbookID string) string {
return fmt.Sprintf("%s/%s", PlaybooksPath, playbookID)
}
// absolute urls
func getRunDetailsURL(siteURL string, playbookRunID string) string {
return fmt.Sprintf("%s%s", siteURL, GetRunDetailsRelativeURL(playbookRunID))
}
func getRunRetrospectiveURL(siteURL string, playbookRunID string) string {
return fmt.Sprintf("%s/retrospective", getRunDetailsURL(siteURL, playbookRunID))
}
func getPlaybooksURL(siteURL string) string {
return fmt.Sprintf("%s%s", siteURL, PlaybooksPath)
}
func getPlaybooksNewURL(siteURL string) string {
return fmt.Sprintf("%s/new", getPlaybooksURL(siteURL))
}
func getPlaybookDetailsURL(siteURL string, playbookID string) string {
return fmt.Sprintf("%s%s", siteURL, GetPlaybookDetailsRelativeURL(playbookID))
}
func getChannelURL(siteURL string, teamName string, channelName string) string {
return fmt.Sprintf("%s/%s/channels/%s",
siteURL,
teamName,
channelName,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"regexp"
"strings"
)
var varsReStr = `(\$[a-zA-Z0-9_]+)`
var reVars = regexp.MustCompile(varsReStr)
// reVarsAndVals is the regex use to match variables and their values.
var reVarsAndVals = regexp.MustCompile(`^\s*` + varsReStr + `=(.+)\s*$`)
// parseVariables returns the variables parsed from the given text.
// Each variable must be defined on a separate line, and must match
// the `reVar` regex.
func parseVariablesAndValues(input string) map[string]string {
lines := strings.Split(input, "\n")
vars := make(map[string]string)
for _, line := range lines {
if !reVarsAndVals.MatchString(line) {
continue
}
match := reVarsAndVals.FindStringSubmatch(line)
vars[match[1]] = match[2]
}
return vars
}
// parseVariables returns the variable names in the given input string.
func parseVariables(input string) []string {
return reVars.FindAllString(input, -1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bot
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// Bot stores the information for the plugin configuration, and implements the Poster interfaces.
type Bot struct {
configService config.Service
serviceAdapter playbooks.ServicesAPI
botUserID string
telemetry Telemetry
}
// Poster interface - a small subset of the plugin posting API.
type Poster interface {
// Post posts a custom post, which should provide the Message and ChannelId fields
Post(post *model.Post) error
// PostMessage posts a simple message to channelID. Returns the post id if posting was successful.
PostMessage(channelID, format string, args ...interface{}) (*model.Post, error)
// PostMessageToThread posts a message to a specified channel and thread identified by rootPostID.
// If the rootPostID is blank, or the rootPost is deleted, it will create a standalone post. The
// returned post's RootID (or ID, if there was no root post) should be used as the rootID for
// future use (i.e., save that if you want to continue the thread).
PostMessageToThread(rootPostID string, post *model.Post) error
// PostMessageWithAttachments posts a message with slack attachments to channelID. Returns the post id if
// posting was successful. Often used to include post actions.
PostMessageWithAttachments(channelID string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error)
// PostCustomMessageWithAttachments posts a custom message with the specified type. Falling back to attachments for mobile.
PostCustomMessageWithAttachments(channelID, customType string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error)
// DM posts a DM from the plugin bot to the specified user
DM(userID string, post *model.Post) error
// EphemeralPost sends an ephemeral message to a user.
EphemeralPost(userID, channelID string, post *model.Post)
// SystemEphemeralPost sends an ephemeral message to a user authored by the System.
SystemEphemeralPost(userID, channelID string, post *model.Post)
// EphemeralPostWithAttachments sends an ephemeral message to a user with Slack attachments.
EphemeralPostWithAttachments(userID, channelID, rootPostID string, attachments []*model.SlackAttachment, format string, args ...interface{})
// PublishWebsocketEventToTeam sends a websocket event with payload to teamID.
PublishWebsocketEventToTeam(event string, payload interface{}, teamID string)
// PublishWebsocketEventToChannel sends a websocket event with payload to channelID.
PublishWebsocketEventToChannel(event string, payload interface{}, channelID string)
// PublishWebsocketEventToUser sends a websocket event with payload to userID.
PublishWebsocketEventToUser(event string, payload interface{}, userID string)
// NotifyAdmins sends a DM with the message to each admins
NotifyAdmins(message, authorUserID string, isTeamEdition bool) error
// IsFromPoster returns whether the provided post was sent by this poster
IsFromPoster(post *model.Post) bool
}
type Telemetry interface {
NotifyAdmins(userID string, action string)
StartTrial(userID string, action string)
}
// New creates a new bot poster.
func New(serviceAdapter playbooks.ServicesAPI, botUserID string, configService config.Service, telemetry Telemetry) *Bot {
return &Bot{
serviceAdapter: serviceAdapter,
botUserID: botUserID,
configService: configService,
telemetry: telemetry,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bot
import (
"encoding/json"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const maxAdminsToQueryForNotification = 1000
// PostMessage posts a message to a specified channel.
func (b *Bot) PostMessage(channelID, format string, args ...interface{}) (*model.Post, error) {
post := &model.Post{
Message: fmt.Sprintf(format, args...),
UserId: b.botUserID,
ChannelId: channelID,
}
if _, err := b.serviceAdapter.CreatePost(post); err != nil {
return nil, err
}
return post, nil
}
// Post posts a custom post. The Message and ChannelId fields should be provided in the specified
// post
func (b *Bot) Post(post *model.Post) error {
if post.Message == "" {
return fmt.Errorf("the post does not contain a message")
}
if !model.IsValidId(post.ChannelId) {
return fmt.Errorf("the post does not contain a valid ChannelId")
}
post.UserId = b.botUserID
_, err := b.serviceAdapter.CreatePost(post)
return err
}
// PostMessageToThread posts a message to a specified thread identified by rootPostID.
// If the rootPostID is blank, or the rootPost is deleted, it will create a standalone post. The
// overwritten post's RootID will be the correct rootID (save that if you want to continue the thread).
func (b *Bot) PostMessageToThread(rootPostID string, post *model.Post) error {
rootID := ""
if rootPostID != "" {
root, err := b.serviceAdapter.GetPostsByIds([]string{rootPostID})
if err == nil && len(root) > 0 && root[0].DeleteAt == 0 {
rootID = root[0].Id
}
}
post.UserId = b.botUserID
post.RootId = rootID
_, err := b.serviceAdapter.CreatePost(post)
return err
}
// PostMessageWithAttachments posts a message with slack attachments to channelID. Returns the post id if
// posting was successful. Often used to include post actions.
func (b *Bot) PostMessageWithAttachments(channelID string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) {
post := &model.Post{
Message: fmt.Sprintf(format, args...),
UserId: b.botUserID,
ChannelId: channelID,
}
model.ParseSlackAttachment(post, attachments)
if _, err := b.serviceAdapter.CreatePost(post); err != nil {
return nil, err
}
return post, nil
}
func (b *Bot) PostCustomMessageWithAttachments(channelID, customType string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) {
post := &model.Post{
Message: fmt.Sprintf(format, args...),
UserId: b.botUserID,
ChannelId: channelID,
Type: customType,
}
model.ParseSlackAttachment(post, attachments)
if _, err := b.serviceAdapter.CreatePost(post); err != nil {
return nil, err
}
return post, nil
}
// DM sends a DM from the plugin bot to the specified user
func (b *Bot) DM(userID string, post *model.Post) error {
channel, err := b.serviceAdapter.GetDirectChannelOrCreate(userID, b.botUserID)
if err != nil {
return errors.Wrapf(err, "failed to get bot DM channel with user_id %s", userID)
}
post.ChannelId = channel.Id
post.UserId = b.botUserID
_, err = b.serviceAdapter.CreatePost(post)
return err
}
// EphemeralPost sends an ephemeral message to a user
func (b *Bot) EphemeralPost(userID, channelID string, post *model.Post) {
post.UserId = b.botUserID
post.ChannelId = channelID
b.serviceAdapter.SendEphemeralPost(userID, post)
}
// SystemEphemeralPost sends an ephemeral message to a user authored by the System
func (b *Bot) SystemEphemeralPost(userID, channelID string, post *model.Post) {
post.ChannelId = channelID
b.serviceAdapter.SendEphemeralPost(userID, post)
}
// EphemeralPostWithAttachments sends an ephemeral message to a user with Slack attachments.
func (b *Bot) EphemeralPostWithAttachments(userID, channelID, postID string, attachments []*model.SlackAttachment, format string, args ...interface{}) {
post := &model.Post{
Message: fmt.Sprintf(format, args...),
UserId: b.botUserID,
ChannelId: channelID,
RootId: postID,
}
model.ParseSlackAttachment(post, attachments)
b.serviceAdapter.SendEphemeralPost(userID, post)
}
// PublishWebsocketEventToTeam sends a websocket event with payload to teamID
func (b *Bot) PublishWebsocketEventToTeam(event string, payload interface{}, teamID string) {
payloadMap := b.makePayloadMap(payload)
b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{
TeamId: teamID,
})
}
// PublishWebsocketEventToChannel sends a websocket event with payload to channelID
func (b *Bot) PublishWebsocketEventToChannel(event string, payload interface{}, channelID string) {
payloadMap := b.makePayloadMap(payload)
b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{
ChannelId: channelID,
})
}
// PublishWebsocketEventToUser sends a websocket event with payload to userID
func (b *Bot) PublishWebsocketEventToUser(event string, payload interface{}, userID string) {
payloadMap := b.makePayloadMap(payload)
b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{
UserId: userID,
})
}
func (b *Bot) NotifyAdmins(messageType, authorUserID string, isTeamEdition bool) error {
author, err := b.serviceAdapter.GetUserByID(authorUserID)
if err != nil {
return errors.Wrap(err, "unable to find author user")
}
admins, err := b.serviceAdapter.GetUsersFromProfiles(&model.UserGetOptions{
Role: string(model.SystemAdminRoleId),
Page: 0,
PerPage: maxAdminsToQueryForNotification,
})
if err != nil {
return errors.Wrap(err, "unable to find all admin users")
}
if len(admins) == 0 {
return fmt.Errorf("no admins found")
}
var postType, footer string
isCloud := b.configService.IsCloud()
separator := "\n\n---\n\n"
if isCloud {
postType = "custom_cloud_upgrade"
footer = separator + "[Upgrade now](https://customers.mattermost.com)."
} else {
footer = "[Learn more](https://mattermost.com/pricing).\n\nWhen you select **Start 30-day trial**, you agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails."
if isTeamEdition {
footer = "[Learn more](https://mattermost.com/pricing).\n\n[Convert to Mattermost Starter](https://docs.mattermost.com/install/ee-install.html#converting-team-edition-to-enterprise-edition) to unlock this feature. Then, start a trial or upgrade to Mattermost Professional or Enterprise."
}
}
var message, title, text string
switch messageType {
case "start_trial_to_add_message_to_timeline", "start_trial_to_view_timeline":
message = fmt.Sprintf("@%s requested access to the playbook run timeline.", author.Username)
title = "Keep a complete record of the playbook run timeline"
text = "The playbook run timeline automatically tracks key events and messages in chronological order so that they can be traced and reviewed afterwards. Teams use timeline to perform retrospectives and extract lessons for the next time that they run the playbook."
case "start_trial_to_access_retrospective":
message = fmt.Sprintf("@%s requested access to the retrospective.", author.Username)
title = "Publish retrospective report and access the timeline"
text = "Celebrate success and learn from mistakes with retrospective reports. Filter timeline events for process review, stakeholder engagement, and auditing purposes."
case "start_trial_to_restrict_playbook_access":
message = fmt.Sprintf("@%s requested permission to configure who can access specific playbooks.", author.Username)
title = "Control who can access your team's playbooks"
text = "Playbooks are workflows that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives. When you upgrade, you can set playbook permissions for specific users or set a global permission to control which team members can create playbooks.\n" + footer
case "start_trial_to_restrict_playbook_creation":
message = fmt.Sprintf("@%s requested permission to configure who can create playbooks.", author.Username)
title = "Control who can create playbooks"
text = "Playbooks are workflows that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives. When you upgrade, you can set playbook permissions for specific users or set a global permission to control which team members can create playbooks.\n" + footer
case "start_trial_to_export_channel":
message = fmt.Sprintf("@%s requested access to export the playbook run channel.", author.Username)
title = "Save the message history of your playbook runs"
text = "Export the channel of your playbook run and save it for later analysis. When you upgrade, you can automatically generate and download a CSV file containing all the timestamped messages sent to the channel.\n" + footer
case "start_trial_to_access_playbook_dashboard":
message = fmt.Sprintf("@%s requested access to view playbook statistics", author.Username)
title = "All the statistics you need"
text = "View trends for total runs, active runs, and participants involved in runs of this playbook."
case "start_trial_to_access_metrics":
message = fmt.Sprintf("@%s requested access to playbook key metrics feature", author.Username)
title = "Track key metrics and measure value"
text = "Use metrics to understand patterns and progress across runs, and track performance."
case "start_trial_to_request_update":
message = fmt.Sprintf("@%s requested access to ask for status updates in playbook runs", author.Username)
title = "Try request update with a free trial"
text = "Request updates for playbook runs in a single click and get notified directly when an update is posted. Start a free, 30-day trial to try it out.\n" + footer
}
actions := []*model.PostAction{
{
Id: "message",
Name: "Start 30-day trial",
Style: "primary",
Type: "button",
Integration: &model.PostActionIntegration{
URL: fmt.Sprintf("/plugins/%s/api/v0/bot/notify-admins/button-start-trial",
"playbooks"),
Context: map[string]interface{}{
"users": 100,
"termsAccepted": true,
"receiveEmailsAccepted": true,
},
},
},
}
if isTeamEdition || isCloud {
actions = []*model.PostAction{}
}
attachments := []*model.SlackAttachment{
{
Title: title,
Text: separator + text,
Actions: actions,
},
}
for _, admin := range admins {
go func(adminID string) {
channel, err := b.serviceAdapter.GetDirectChannelOrCreate(adminID, b.botUserID)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"user_id": adminID,
"bot_id": b.botUserID,
}).Warn("failed to get Direct Message channel between user and bot")
return
}
if _, err := b.PostCustomMessageWithAttachments(channel.Id, postType, attachments, message); err != nil {
logrus.WithError(err).WithField("user_id", adminID).Error("failed to send a DM to user")
}
}(admin.Id)
}
b.telemetry.NotifyAdmins(authorUserID, messageType)
return nil
}
func (b *Bot) IsFromPoster(post *model.Post) bool {
return post.UserId == b.botUserID
}
func (b *Bot) makePayloadMap(payload interface{}) map[string]interface{} {
payloadJSON, err := json.Marshal(payload)
if err != nil {
logrus.WithError(err).Error("could not marshall payload")
payloadJSON = []byte("null")
}
return map[string]interface{}{"payload": string(payloadJSON)}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package command
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/bot"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/config"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/timeutils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const helpText = "###### Mattermost Playbooks Plugin - Slash Command Help\n" +
"* `/playbook run` - Run a playbook. \n" +
"* `/playbook finish` - Finish the playbook run in this channel. \n" +
"* `/playbook update` - Provide a status update. \n" +
"* `/playbook check [checklist #] [item #]` - check/uncheck the checklist item. \n" +
"* `/playbook checkadd [checklist #] [item text]` - add a checklist item. \n" +
"* `/playbook checkremove [checklist #] [item #]` - remove a checklist item. \n" +
"* `/playbook owner [@username]` - Show or change the current owner. \n" +
"* `/playbook info` - Show a summary of the current playbook run. \n" +
"* `/playbook timeline` - Show the timeline for the current playbook run. \n" +
"* `/playbook todo` - Get a list of your assigned tasks. \n" +
"* `/playbook settings digest [on/off]` - turn daily digest on/off. \n" +
"* `/playbook settings weekly-digest [on/off]` - turn weekly digest on/off. \n" +
"\n" +
"Learn more [in our documentation](https://mattermost.com/pl/default-incident-response-app-documentation). \n" +
""
const confirmPrompt = "CONFIRM"
// Register is a function that allows the runner to register commands with the mattermost server.
type Register func(*model.Command) error
// RegisterCommands should be called by the plugin to register all necessary commands
func RegisterCommands(registerFunc Register, addTestCommands bool) error {
return registerFunc(getCommand(addTestCommands))
}
func getCommand(addTestCommands bool) *model.Command {
return &model.Command{
Trigger: "playbook",
DisplayName: "Playbook",
Description: "Playbooks",
AutoComplete: true,
AutoCompleteDesc: "Available commands: run, finish, update, check, list, owner, info, todo, settings",
AutoCompleteHint: "[command]",
AutocompleteData: getAutocompleteData(addTestCommands),
}
}
func getAutocompleteData(addTestCommands bool) *model.AutocompleteData {
command := model.NewAutocompleteData("playbook", "[command]",
"Available commands: run, finish, update, check, checkadd, checkremove, list, owner, info, timeline, todo, settings")
run := model.NewAutocompleteData("run", "", "Starts a new playbook run")
command.AddCommand(run)
finish := model.NewAutocompleteData("finish", "",
"Finishes a playbook run associated with the current channel")
finish.AddDynamicListArgument(
"List of channel runs is loading",
"api/v0/runs/runs-autocomplete", true)
command.AddCommand(finish)
update := model.NewAutocompleteData("update", "",
"Provide a status update.")
update.AddDynamicListArgument(
"List of channel runs is loading",
"api/v0/runs/runs-autocomplete", true)
command.AddCommand(update)
checklist := model.NewAutocompleteData("check", "[checklist item]",
"Checks or unchecks a checklist item.")
checklist.AddDynamicListArgument(
"List of checklist items is loading",
"api/v0/runs/checklist-autocomplete-item", true)
command.AddCommand(checklist)
itemAdd := model.NewAutocompleteData("checkadd", "[checklist]",
"Add a checklist item")
itemAdd.AddDynamicListArgument(
"List of checklist items is loading",
"api/v0/runs/checklist-autocomplete", true)
itemRemove := model.NewAutocompleteData("checkremove", "[checklist item]",
"Remove a checklist item")
itemRemove.AddDynamicListArgument(
"List of checklist items is loading",
"api/v0/runs/checklist-autocomplete-item", true)
command.AddCommand(itemAdd)
command.AddCommand(itemRemove)
owner := model.NewAutocompleteData("owner", "[@username]",
"Show or change the current owner")
owner.AddDynamicListArgument(
"List of channel runs is loading",
"api/v0/runs/runs-autocomplete", true)
owner.AddTextArgument("The desired new owner.", "[@username]", "")
command.AddCommand(owner)
info := model.NewAutocompleteData("info", "", "Shows a summary of the current playbook run")
info.AddDynamicListArgument(
"List of channel runs is loading",
"api/v0/runs/runs-autocomplete", true)
command.AddCommand(info)
timeline := model.NewAutocompleteData("timeline", "", "Shows the timeline for the current playbook run")
timeline.AddDynamicListArgument(
"List of channel runs is loading",
"api/v0/runs/runs-autocomplete", true)
command.AddCommand(timeline)
todo := model.NewAutocompleteData("todo", "", "Get a list of your assigned tasks")
command.AddCommand(todo)
settings := model.NewAutocompleteData("settings", "", "Change personal playbook settings")
display := model.NewAutocompleteData(" ", "Display current settings", "")
settings.AddCommand(display)
weeklyDigest := model.NewAutocompleteData("weekly-digest", "[on/off]", "Turn weekly digest on/off")
weeklyDigestValues := []model.AutocompleteListItem{{
HelpText: "Turn weekly digest on",
Item: "on",
}, {
HelpText: "Turn weekly digest off",
Item: "off",
}}
weeklyDigest.AddStaticListArgument("", true, weeklyDigestValues)
settings.AddCommand((weeklyDigest))
digest := model.NewAutocompleteData("digest", "[on/off]", "Turn digest on/off")
digestValue := []model.AutocompleteListItem{{
HelpText: "Turn daily digest on",
Item: "on",
}, {
HelpText: "Turn daily digest off",
Item: "off",
}}
digest.AddStaticListArgument("", true, digestValue)
settings.AddCommand(digest)
command.AddCommand(settings)
if addTestCommands {
test := model.NewAutocompleteData("test", "", "Commands for testing and debugging.")
testGeneratePlaybooks := model.NewAutocompleteData("create-playbooks", "[total playbooks]", "Create one or more playbooks based on number of playbooks defined")
testGeneratePlaybooks.AddTextArgument("An integer indicating how many playbooks will be generated (at most 5).", "Number of playbooks", "")
test.AddCommand(testGeneratePlaybooks)
testCreate := model.NewAutocompleteData("create-playbook-run", "[playbook ID] [timestamp] [name]", "Run a playbook with a specific creation date")
testCreate.AddDynamicListArgument("List of playbooks is loading", "api/v0/playbooks/autocomplete", true)
testCreate.AddTextArgument("Date in format 2020-01-31", "Creation timestamp", `/[0-9]{4}-[0-9]{2}-[0-9]{2}/`)
testCreate.AddTextArgument("Name of the playbook run", "Name", "")
test.AddCommand(testCreate)
testData := model.NewAutocompleteData("bulk-data", "[ongoing] [ended] [days] [seed]", "Generate random test data in bulk")
testData.AddTextArgument("An integer indicating how many ongoing playbook runs will be generated.", "Number of ongoing playbook runs", "")
testData.AddTextArgument("An integer indicating how many ended playbook runs will be generated.", "Number of ended playbook runs", "")
testData.AddTextArgument("An integer n. The playbook runs generated will have a start date between n days ago and today.", "Range of days for the start date", "")
testData.AddTextArgument("An integer in case you need random, but reproducible, results", "Random seed (optional)", "")
test.AddCommand(testData)
testSelf := model.NewAutocompleteData("self", "", "DESTRUCTIVE ACTION - Perform a series of self tests to ensure everything works as expected.")
test.AddCommand(testSelf)
command.AddCommand(test)
}
return command
}
// Runner handles commands.
type Runner struct {
context *plugin.Context
args *model.CommandArgs
api playbooks.ServicesAPI
poster bot.Poster
playbookRunService app.PlaybookRunService
playbookService app.PlaybookService
configService config.Service
userInfoStore app.UserInfoStore
userInfoTelemetry app.UserInfoTelemetry
permissions *app.PermissionsService
}
// NewCommandRunner creates a command runner.
func NewCommandRunner(ctx *plugin.Context,
args *model.CommandArgs,
api playbooks.ServicesAPI,
poster bot.Poster,
playbookRunService app.PlaybookRunService,
playbookService app.PlaybookService,
configService config.Service,
userInfoStore app.UserInfoStore,
userInfoTelemetry app.UserInfoTelemetry,
permissions *app.PermissionsService,
) *Runner {
return &Runner{
context: ctx,
args: args,
api: api,
poster: poster,
playbookRunService: playbookRunService,
playbookService: playbookService,
configService: configService,
userInfoStore: userInfoStore,
userInfoTelemetry: userInfoTelemetry,
permissions: permissions,
}
}
func (r *Runner) isValid() error {
if r.context == nil || r.args == nil || r.api == nil {
return errors.New("invalid arguments to command.Runner")
}
return nil
}
func (r *Runner) postCommandResponse(text string) {
post := &model.Post{
Message: text,
}
r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, post)
}
func (r *Runner) warnUserAndLogErrorf(format string, args ...interface{}) {
logrus.Errorf(format, args...)
r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, &model.Post{
Message: "Your request could not be completed. Check the system logs for more information.",
})
}
func (r *Runner) actionRun(args []string) {
clientID := ""
if len(args) > 0 {
clientID = args[0]
}
postID := ""
if len(args) == 2 {
postID = args[1]
}
requesterInfo := app.RequesterInfo{
UserID: r.args.UserId,
TeamID: r.args.TeamId,
IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api),
}
playbooksResults, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId,
app.PlaybookFilterOptions{
Sort: app.SortByTitle,
Direction: app.DirectionAsc,
Page: 0,
PerPage: app.PerPageDefault,
})
if err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
if err := r.playbookRunService.OpenCreatePlaybookRunDialog(r.args.TeamId, r.args.UserId, r.args.TriggerId, postID, clientID, playbooksResults.Items); err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
}
// actionRunPlaybook is intended for scripting use, not use by the end user (they would have
// to type in the correct playbookID).
func (r *Runner) actionRunPlaybook(args []string) {
if len(args) != 2 {
r.postCommandResponse("Usage: `/playbook run-playbook <playbookID> <clientID>`")
return
}
playbookID := args[0]
clientID := args[1]
requesterInfo := app.RequesterInfo{
UserID: r.args.UserId,
TeamID: r.args.TeamId,
IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api),
}
// Using the GetPlaybooksForTeam so that requesterInfo and the expected security restrictions
// are respected.
playbooksResults, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId,
app.PlaybookFilterOptions{
Sort: app.SortByTitle,
Direction: app.DirectionAsc,
Page: 0,
PerPage: app.PerPageDefault,
})
if err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
var playbook []app.Playbook
for _, pb := range playbooksResults.Items {
if pb.ID == playbookID {
playbook = append(playbook, pb)
break
}
}
if len(playbook) == 0 {
r.postCommandResponse("Playbook not found for id: " + playbookID)
return
}
if err := r.playbookRunService.OpenCreatePlaybookRunDialog(r.args.TeamId, r.args.UserId, r.args.TriggerId, "", clientID, playbook); err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
}
func (r *Runner) actionCheck(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if !multipleRuns && len(args) != 2 {
r.postCommandResponse("Command expects two arguments: the checklist number and the item number.")
return
}
if multipleRuns && len(args) != 3 {
r.postCommandResponse("Command expects three arguments: the run number, the checklist number and the item number.")
return
}
run := 0
index := 0
if multipleRuns {
if run, err = strconv.Atoi(args[index]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
index++
}
checklist, err := strconv.Atoi(args[index])
index++
if err != nil {
r.postCommandResponse("Error parsing the argument. Must be a number.")
return
}
item, err := strconv.Atoi(args[index])
if err != nil {
r.postCommandResponse("Error parsing the argument. Must be a number.")
return
}
if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil {
r.postCommandResponse("Become a participant to interact with this run.")
return
}
err = r.playbookRunService.ToggleCheckedState(playbookRuns[run].ID, r.args.UserId, checklist, item)
if err != nil {
r.warnUserAndLogErrorf("Error checking/unchecking item: %v", err)
}
}
func (r *Runner) actionAddChecklistItem(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if !multipleRuns && len(args) < 1 {
r.postCommandResponse("Command expects one argument: the checklist number.")
return
}
if multipleRuns && len(args) < 2 {
r.postCommandResponse("Command expects two arguments: the run number and the checklist number.")
return
}
run := 0
index := 0
if multipleRuns {
if run, err = strconv.Atoi(args[index]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
index++
}
checklist, err := strconv.Atoi(args[index])
index++
if err != nil {
r.postCommandResponse("Error parsing the argument. Must be a number.")
return
}
if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil {
r.postCommandResponse("Become a participant to interact with this run.")
return
}
// If we didn't get the item's text, then use the interactive dialog
if len(args) == index {
if err := r.playbookRunService.OpenAddChecklistItemDialog(r.args.TriggerId, r.args.UserId, playbookRuns[run].ID, checklist); err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
return
}
combineargs := strings.Join(args[index:], " ")
if err := r.playbookRunService.AddChecklistItem(playbookRuns[run].ID, r.args.UserId, checklist, app.ChecklistItem{
Title: combineargs,
}); err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
}
func (r *Runner) actionRemoveChecklistItem(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if !multipleRuns && len(args) != 2 {
r.postCommandResponse("Command expects two arguments: the checklist number and the item number.")
return
}
if multipleRuns && len(args) != 3 {
r.postCommandResponse("Command expects three arguments: the run number, the checklist number and the item number.")
return
}
run := 0
index := 0
if multipleRuns {
if run, err = strconv.Atoi(args[index]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
index++
}
checklist, err := strconv.Atoi(args[index])
index++
if err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
item, err := strconv.Atoi(args[index])
if err != nil {
r.postCommandResponse("Error parsing the second argument. Must be a number.")
return
}
if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil {
r.postCommandResponse("Become a participant to interact with this run.")
return
}
err = r.playbookRunService.RemoveChecklistItem(playbookRuns[run].ID, r.args.UserId, checklist, item)
if err != nil {
r.warnUserAndLogErrorf("Error removing item: %v", err)
}
}
func (r *Runner) actionOwner(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
extraArg := 0
// if channel has multiple runs, we require additional argument: run number
if multipleRuns {
extraArg = 1
}
switch len(args) - extraArg {
case 0:
r.actionShowOwner(args, playbookRuns)
case 1:
r.actionChangeOwner(args, playbookRuns)
default:
r.postCommandResponse("/playbook owner expects at most one argument.")
}
}
func (r *Runner) actionShowOwner(args []string, playbookRuns []app.PlaybookRun) {
multipleRuns := len(playbookRuns) > 1
run := 0
if multipleRuns {
var err error
if run, err = strconv.Atoi(args[0]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
}
currentPlaybookRun := playbookRuns[run]
ownerUser, err := r.api.GetUserByID(currentPlaybookRun.OwnerUserID)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving owner user: %v", err)
return
}
r.postCommandResponse(fmt.Sprintf("**@%s** is the current owner for this playbook run.", ownerUser.Username))
}
func (r *Runner) actionChangeOwner(args []string, playbookRuns []app.PlaybookRun) {
multipleRuns := len(playbookRuns) > 1
run := 0
index := 0
if multipleRuns {
var err error
if run, err = strconv.Atoi(args[index]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
index++
}
targetOwnerUsername := strings.TrimLeft(args[index], "@")
if err := r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil {
r.postCommandResponse("Become a participant to interact with this run.")
return
}
currentPlaybookRun := playbookRuns[run]
targetOwnerUser, err := r.api.GetUserByUsername(targetOwnerUsername)
if errors.Is(err, app.ErrNotFound) {
r.postCommandResponse(fmt.Sprintf("Unable to find user @%s", targetOwnerUsername))
return
} else if err != nil {
r.warnUserAndLogErrorf("Error finding user @%s: %v", targetOwnerUsername, err)
return
}
if currentPlaybookRun.OwnerUserID == targetOwnerUser.Id {
r.postCommandResponse(fmt.Sprintf("User @%s is already owner of this playbook run.", targetOwnerUsername))
return
}
err = r.playbookRunService.ChangeOwner(currentPlaybookRun.ID, r.args.UserId, targetOwnerUser.Id)
if err != nil {
r.warnUserAndLogErrorf("Failed to change owner to @%s: %v", targetOwnerUsername, err)
return
}
}
func (r *Runner) actionInfo(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
session, err := r.api.GetSession(r.context.SessionId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving session: %v", err)
return
}
if !session.IsMobileApp() {
// The RHS was opened by the webapp, so inform the user
r.postCommandResponse("Your playbook run details are already open in the right hand side of the channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if multipleRuns && len(args) == 0 {
r.postCommandResponse("Command expects one argument: the run number.")
return
}
run := 0
if multipleRuns {
if run, err = strconv.Atoi(args[0]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
}
playbookRun := playbookRuns[run]
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err)
return
}
owner, err := r.api.GetUserByID(playbookRun.OwnerUserID)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving owner user: %v", err)
return
}
tasks := ""
for _, checklist := range playbookRun.Checklists {
for _, item := range checklist.Items {
icon := ":white_large_square: "
timestamp := ""
if item.State == app.ChecklistItemStateClosed {
icon = ":white_check_mark: "
timestamp = " (" + timeutils.GetTimeForMillis(item.StateModified).Format("15:04 PM") + ")"
}
tasks += icon + item.Title + timestamp + "\n"
}
}
attachment := &model.SlackAttachment{
Fields: []*model.SlackAttachmentField{
{Title: "Name:", Value: fmt.Sprintf("**%s**", strings.Trim(playbookRun.Name, " "))},
{Title: "Duration:", Value: timeutils.DurationString(timeutils.GetTimeForMillis(playbookRun.CreateAt), time.Now())},
{Title: "Owner:", Value: fmt.Sprintf("@%s", owner.Username)},
{Title: "Tasks:", Value: tasks},
},
}
post := &model.Post{
Props: map[string]interface{}{
"attachments": []*model.SlackAttachment{attachment},
},
}
r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, post)
}
func (r *Runner) actionFinish(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if multipleRuns && len(args) == 0 {
r.postCommandResponse("Command expects one argument: the run number.")
return
}
run := 0
if multipleRuns {
if run, err = strconv.Atoi(args[0]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
}
r.actionFinishByID([]string{playbookRuns[run].ID})
}
func (r *Runner) actionFinishByID(args []string) {
if len(args) == 0 {
r.postCommandResponse("Command expects one argument: the run ID.")
return
}
if err := r.permissions.RunManageProperties(r.args.UserId, args[0]); err != nil {
if errors.Is(err, app.ErrNoPermissions) {
r.postCommandResponse(fmt.Sprintf("userID `%s` is not an admin or channel member", r.args.UserId))
return
}
r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err)
return
}
err := r.playbookRunService.OpenFinishPlaybookRunDialog(args[0], r.args.UserId, r.args.TriggerId)
if err != nil {
r.warnUserAndLogErrorf("Error finishing the playbook run: %v", err)
return
}
}
func (r *Runner) actionUpdate(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if multipleRuns && len(args) == 0 {
r.postCommandResponse("Command expects one argument: the run number.")
return
}
run := 0
if multipleRuns {
if run, err = strconv.Atoi(args[0]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
}
if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil {
if errors.Is(err, app.ErrNoPermissions) {
r.postCommandResponse(fmt.Sprintf("userID `%s` is not an admin or channel member", r.args.UserId))
return
}
r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err)
return
}
err = r.playbookRunService.OpenUpdateStatusDialog(playbookRuns[run].ID, r.args.UserId, r.args.TriggerId)
switch {
case errors.Is(err, app.ErrPlaybookRunNotActive):
r.postCommandResponse("This playbook run has already been closed.")
return
case err != nil:
r.warnUserAndLogErrorf("Error: %v", err)
return
}
}
func (r *Runner) actionAdd(args []string) {
if len(args) != 1 {
r.postCommandResponse("Need to provide a postId")
return
}
postID := args[0]
if postID == "" {
r.postCommandResponse("Need to provide a postId")
return
}
requesterInfo, err := app.GetRequesterInfo(r.args.UserId, r.api)
if err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
if err := r.playbookRunService.OpenAddToTimelineDialog(requesterInfo, postID, r.args.TeamId, r.args.TriggerId); err != nil {
r.warnUserAndLogErrorf("Error: %v", err)
return
}
}
func (r *Runner) actionTimeline(args []string) {
playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err)
return
}
if len(playbookRuns) == 0 {
r.postCommandResponse("This command only works when run from a playbook run channel.")
return
}
multipleRuns := len(playbookRuns) > 1
if multipleRuns && len(args) == 0 {
r.postCommandResponse("Command expects one argument: the run number.")
return
}
run := 0
if multipleRuns {
if run, err = strconv.Atoi(args[0]); err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if run < 0 || run >= len(playbookRuns) {
r.postCommandResponse("Invalid run number")
return
}
}
playbookRun := playbookRuns[run]
if err != nil {
r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err)
return
}
if len(playbookRun.TimelineEvents) == 0 {
r.postCommandResponse("There are no timeline events to display.")
return
}
team, err := r.api.GetTeam(r.args.TeamId)
if err != nil {
r.warnUserAndLogErrorf("Error retrieving team: %v", err)
return
}
postURL := fmt.Sprintf("/%s/pl/", team.Name)
message := "Timeline for **" + playbookRun.Name + "**:\n\n" +
"|Event Time | Since Reported | Event |\n" +
"|:----------|:---------------|:------|\n"
var reported time.Time
for _, e := range playbookRun.TimelineEvents {
if e.EventType == app.PlaybookRunCreated {
reported = timeutils.GetTimeForMillis(e.EventAt)
break
}
}
for _, e := range playbookRun.TimelineEvents {
if e.EventType == app.AssigneeChanged ||
e.EventType == app.TaskStateModified ||
e.EventType == app.RanSlashCommand {
continue
}
timeLink := timeutils.GetTimeForMillis(e.EventAt).Format("Jan 2 15:04")
if e.PostID != "" {
timeLink = " [" + timeLink + "](" + postURL + e.PostID + ") "
}
message += "|" + timeLink + "|" + r.timeSince(e, reported) + "|" + r.summaryMessage(e) + "|\n"
}
r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, &model.Post{Message: message})
}
func (r *Runner) summaryMessage(event app.TimelineEvent) string {
var username string
user, err := r.api.GetUserByID(event.SubjectUserID)
if err == nil {
username = user.Username
}
switch event.EventType {
case app.PlaybookRunCreated:
return "Run started by @" + username
case app.StatusUpdated:
if event.Summary == "" {
return "@" + username + " posted a status update"
}
return "@" + username + " changed status from " + event.Summary
case app.OwnerChanged:
return "Owner changes from " + event.Summary
case app.TaskStateModified:
return "@" + username + " " + event.Summary
case app.AssigneeChanged:
return "@" + username + " " + event.Summary
case app.RanSlashCommand:
return "@" + username + " " + event.Summary
case app.PublishedRetrospective:
return "@" + username + " published retrospective"
case app.CanceledRetrospective:
return "@" + username + " canceled retrospective"
default:
return event.Summary
}
}
func (r *Runner) timeSince(event app.TimelineEvent, reported time.Time) string {
if event.EventType == app.PlaybookRunCreated {
return ""
}
eventAt := timeutils.GetTimeForMillis(event.EventAt)
if reported.Before(eventAt) {
return timeutils.DurationString(reported, eventAt)
}
return "-" + timeutils.DurationString(eventAt, reported)
}
func (r *Runner) actionTodo() {
if err := r.playbookRunService.EphemeralPostTodoDigestToUser(r.args.UserId, r.args.ChannelId, true, true); err != nil {
r.warnUserAndLogErrorf("Error getting tasks and runs digest: %v", err)
}
}
func (r *Runner) actionSettings(args []string) {
settingsHelpText := "###### Playbooks Personal Settings - Slash Command Help\n" +
"* `/playbook settings` - display current settings. \n" +
"* `/playbook settings digest on` - turn daily digest on. \n" +
"* `/playbook settings digest off` - turn daily digest off. \n" +
"* `/playbook settings weekly-digest on` - turn weekly digest on. \n" +
"* `/playbook settings weekly-digest off` - turn weekly digest off. \n"
if len(args) == 0 {
r.displayCurrentSettings()
return
}
isDigest := args[0] == "digest" || args[0] == "weekly-digest"
if len(args) != 2 || !isDigest || (args[1] != "on" && args[1] != "off") {
r.postCommandResponse(settingsHelpText)
return
}
info, err := r.userInfoStore.Get(r.args.UserId)
if errors.Is(err, app.ErrNotFound) {
info = app.UserInfo{
ID: r.args.UserId,
}
} else if err != nil {
r.warnUserAndLogErrorf("Error getting userInfo: %v", err)
return
}
oldInfo := info
if args[0] == "weekly-digest" && args[1] == "off" {
info.DisableWeeklyDigest = true
} else if args[0] == "weekly-digest" {
info.DisableWeeklyDigest = false
} else if args[0] == "digest" && args[1] == "off" {
info.DisableDailyDigest = true
} else {
info.DisableDailyDigest = false
}
if err = r.userInfoStore.Upsert(info); err != nil {
r.warnUserAndLogErrorf("Error updating userInfo: %v", err)
return
}
r.userInfoTelemetry.ChangeDigestSettings(r.args.UserId, oldInfo.DigestNotificationSettings, info.DigestNotificationSettings)
r.displayCurrentSettings()
}
func (r *Runner) displayCurrentSettings() {
info, err := r.userInfoStore.Get(r.args.UserId)
if err != nil {
if !errors.Is(err, app.ErrNotFound) {
r.warnUserAndLogErrorf("Error getting userInfo: %v", err)
return
}
}
dailyDigestSetting := "Daily digest: on"
if info.DisableDailyDigest {
dailyDigestSetting = "Daily digest: off"
}
weeklyDigestSetting := "Weekly digest: on"
if info.DisableWeeklyDigest {
weeklyDigestSetting = "Weekly digest: off"
}
r.postCommandResponse(fmt.Sprintf("###### Playbooks Personal Settings\n- %s, %s", dailyDigestSetting, weeklyDigestSetting))
}
func (r *Runner) actionTestSelf(args []string) {
if r.api.GetConfig().ServiceSettings.EnableTesting == nil ||
!*r.api.GetConfig().ServiceSettings.EnableTesting {
r.postCommandResponse(helpText)
return
}
if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) {
r.postCommandResponse("Running the self-test is restricted to system administrators.")
return
}
if len(args) != 3 || args[0] != confirmPrompt || args[1] != "TEST" || args[2] != "SELF" {
r.postCommandResponse("Are you sure you want to self-test (which will nuke the database and delete all data -- instances, configuration)? " +
"All data will be lost. To self-test, type `/playbook test self CONFIRM TEST SELF`")
return
}
if err := r.playbookRunService.NukeDB(); err != nil {
r.postCommandResponse("There was an error while nuking db. Err: " + err.Error())
return
}
shortDescription := "A short description."
longDescription := `A very long description describing the item in a very descriptive way. Now with Markdown syntax! We have *italics* and **bold**. We have [external](http://example.com) and [internal links](/ad-1/playbooks/playbooks). We have even links to channels: ~town-square. And links to users: @sysadmin, @user-1. We do have the usual headings and lists, of course:
## Unordered List
- One
- Two
- Three
### Ordered List
1. One
2. Two
3. Three
We also have images:

And... yes, of course, we have emojis
:muscle: :sunglasses: :tada: :confetti_ball: :balloon: :cowboy_hat_face: :nail_care:`
testPlaybook := app.Playbook{
Title: "testing playbook",
TeamID: r.args.TeamId,
Checklists: []app.Checklist{
{
Title: "Identification",
Items: []app.ChecklistItem{
{
Title: "Create Jira ticket",
Description: longDescription,
},
{
Title: "Add on-call team members",
State: app.ChecklistItemStateClosed,
},
{
Title: "Identify blast radius",
Description: shortDescription,
},
{
Title: "Identify impacted services",
},
{
Title: "Collect server data logs",
},
{
Title: "Identify blast Analyze data logs",
},
},
},
{
Title: "Resolution",
Items: []app.ChecklistItem{
{
Title: "Align on plan of attack",
},
{
Title: "Confirm resolution",
},
},
},
{
Title: "Analysis",
Items: []app.ChecklistItem{
{
Title: "Writeup root-cause analysis",
},
{
Title: "Review post-mortem",
},
},
},
},
}
playbookID, err := r.playbookService.Create(testPlaybook, r.args.UserId)
if err != nil {
r.postCommandResponse("There was an error while creating playbook. Err: " + err.Error())
return
}
gotplaybook, err := r.playbookService.Get(playbookID)
if err != nil {
r.postCommandResponse(fmt.Sprintf("There was an error while retrieving playbook. ID: %v Err: %v", playbookID, err.Error()))
return
}
if gotplaybook.Title != testPlaybook.Title {
r.postCommandResponse(fmt.Sprintf("Retrieved playbook is wrong, ID: %v Playbook: %+v", playbookID, gotplaybook))
return
}
if gotplaybook.ID == "" {
r.postCommandResponse("Retrieved playbook has a blank ID")
return
}
gotPlaybooks, err := r.playbookService.GetPlaybooks()
if err != nil {
r.postCommandResponse("There was an error while retrieving all playbooks. Err: " + err.Error())
return
}
if len(gotPlaybooks) != 1 || gotPlaybooks[0].Title != testPlaybook.Title {
r.postCommandResponse(fmt.Sprintf("Retrieved playbooks are wrong: %+v", gotPlaybooks))
return
}
gotplaybook.Title = "This is an updated title"
if err = r.playbookService.Update(gotplaybook, r.args.UserId); err != nil {
r.postCommandResponse("Unable to update playbook Err:" + err.Error())
return
}
gotupdated, err := r.playbookService.Get(playbookID)
if err != nil {
r.postCommandResponse(fmt.Sprintf("There was an error while retrieving playbook. ID: %v Err: %v", playbookID, err.Error()))
return
}
if gotupdated.Title != gotplaybook.Title {
r.postCommandResponse("Update was ineffective")
return
}
todeleteid, err := r.playbookService.Create(testPlaybook, r.args.UserId)
if err != nil {
r.postCommandResponse("There was an error while creating playbook. Err: " + err.Error())
return
}
testPlaybook.ID = todeleteid
if err = r.playbookService.Archive(testPlaybook, r.args.UserId); err != nil {
r.postCommandResponse("There was an error while deleting playbook. Err: " + err.Error())
return
}
if deletedPlaybook, _ := r.playbookService.Get(todeleteid); deletedPlaybook.Title != "" {
r.postCommandResponse("Playbook should have been vaporized! Where's the kaboom? There was supposed to be an earth-shattering Kaboom!")
return
}
playbookRun, err := r.playbookRunService.CreatePlaybookRun(&app.PlaybookRun{
Name: "Cloud Incident 4739",
TeamID: r.args.TeamId,
OwnerUserID: r.args.UserId,
PlaybookID: gotplaybook.ID,
Checklists: gotplaybook.Checklists,
BroadcastChannelIDs: gotplaybook.BroadcastChannelIDs,
Type: app.RunTypePlaybook,
}, &gotplaybook, r.args.UserId, true)
if err != nil {
r.postCommandResponse("Unable to create test playbook run: " + err.Error())
return
}
if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{
Title: "I should be checked and second",
}); err != nil {
r.postCommandResponse("Unable to add checklist item: " + err.Error())
return
}
if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{
Title: "I should be deleted",
}); err != nil {
r.postCommandResponse("Unable to add checklist item: " + err.Error())
return
}
if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{
Title: "I should not say this.",
State: app.ChecklistItemStateClosed,
}); err != nil {
r.postCommandResponse("Unable to add checklist item: " + err.Error())
return
}
if err := r.playbookRunService.ModifyCheckedState(playbookRun.ID, r.args.UserId, app.ChecklistItemStateClosed, 0, 0); err != nil {
r.postCommandResponse("Unable to modify checked state: " + err.Error())
return
}
if err := r.playbookRunService.ModifyCheckedState(playbookRun.ID, r.args.UserId, app.ChecklistItemStateOpen, 0, 2); err != nil {
r.postCommandResponse("Unable to modify checked state: " + err.Error())
return
}
if err := r.playbookRunService.RemoveChecklistItem(playbookRun.ID, r.args.UserId, 0, 1); err != nil {
r.postCommandResponse("Unable to remove checklist item: " + err.Error())
return
}
if err := r.playbookRunService.EditChecklistItem(playbookRun.ID, r.args.UserId, 0, 1,
"I should say this! and be unchecked and first!", "", ""); err != nil {
r.postCommandResponse("Unable to remove checklist item: " + err.Error())
return
}
if err := r.playbookRunService.MoveChecklistItem(playbookRun.ID, r.args.UserId, 0, 0, 0, 1); err != nil {
r.postCommandResponse("Unable to remove checklist item: " + err.Error())
return
}
r.postCommandResponse("Self test success.")
}
func (r *Runner) actionTest(args []string) {
if r.api.GetConfig().ServiceSettings.EnableTesting == nil ||
!*r.api.GetConfig().ServiceSettings.EnableTesting {
r.postCommandResponse("Setting `EnableTesting` must be set to `true` to run the test command.")
return
}
if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) {
r.postCommandResponse("Running the test command is restricted to system administrators.")
return
}
if len(args) < 1 {
r.postCommandResponse("The `/playbook test` command needs at least one command.")
return
}
command := strings.ToLower(args[0])
var params = []string{}
if len(args) > 1 {
params = args[1:]
}
switch command {
case "create-playbooks":
r.actionTestGeneratePlaybooks(params)
case "create-playbook-run":
r.actionTestCreate(params)
return
case "bulk-data":
r.actionTestData(params)
case "self":
r.actionTestSelf(params)
default:
r.postCommandResponse(fmt.Sprintf("Command '%s' unknown.", args[0]))
return
}
}
func (r *Runner) actionTestGeneratePlaybooks(params []string) {
if len(params) < 1 {
r.postCommandResponse("The command expects one parameter: <numPlaybooks>")
return
}
numPlaybooks, err := strconv.Atoi(params[0])
if err != nil {
r.postCommandResponse("Error parsing the first argument. Must be a number.")
return
}
if numPlaybooks > 5 {
r.postCommandResponse("Maximum number of playbooks is 5")
return
}
rand.Shuffle(len(dummyListPlaybooks), func(i, j int) {
dummyListPlaybooks[i], dummyListPlaybooks[j] = dummyListPlaybooks[j], dummyListPlaybooks[i]
})
playbookIds := make([]string, 0, numPlaybooks)
for i := 0; i < numPlaybooks; i++ {
dummyPlaybook := dummyListPlaybooks[i]
dummyPlaybook.TeamID = r.args.TeamId
dummyPlaybook.Members = []app.PlaybookMember{
{
UserID: r.args.UserId,
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
},
}
newPlaybookID, errCreatePlaybook := r.playbookService.Create(dummyPlaybook, r.args.UserId)
if errCreatePlaybook != nil {
r.warnUserAndLogErrorf("unable to create playbook: %v", err)
return
}
playbookIds = append(playbookIds, newPlaybookID)
}
msg := "Playbooks successfully created"
for i, playbookID := range playbookIds {
url := fmt.Sprintf("/playbooks/playbooks/%s", playbookID)
msg += fmt.Sprintf("\n- [%s](%s)", dummyListPlaybooks[i].Title, url)
}
r.postCommandResponse(msg)
}
func (r *Runner) actionTestCreate(params []string) {
if len(params) < 3 {
r.postCommandResponse("The command expects three parameters: <playbook_id> <timestamp> <name>")
return
}
playbookID := params[0]
if !model.IsValidId(playbookID) {
r.postCommandResponse("The first parameter, <playbook_id>, must be a valid ID.")
return
}
playbook, err := r.playbookService.Get(playbookID)
if err != nil {
r.postCommandResponse(fmt.Sprintf("The playbook with ID '%s' does not exist.", playbookID))
return
}
creationTimestamp, err := time.ParseInLocation("2006-01-02", params[1], time.Now().Location())
if err != nil {
r.postCommandResponse(fmt.Sprintf("Timestamp '%s' could not be parsed as a date. If you want the playbook run to start on January 2, 2006, the timestamp should be '2006-01-02'.", params[1]))
return
}
playbookRunName := strings.Join(params[2:], " ")
playbookRun, err := r.playbookRunService.CreatePlaybookRun(
&app.PlaybookRun{
Name: playbookRunName,
OwnerUserID: r.args.UserId,
TeamID: r.args.TeamId,
PlaybookID: playbookID,
Checklists: playbook.Checklists,
Type: app.RunTypePlaybook,
},
&playbook,
r.args.UserId,
true,
)
if err != nil {
r.warnUserAndLogErrorf("unable to create playbook run: %v", err)
return
}
if err = r.playbookRunService.ChangeCreationDate(playbookRun.ID, creationTimestamp); err != nil {
r.warnUserAndLogErrorf("unable to change date of recently created playbook run: %v", err)
return
}
channel, err := r.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
r.warnUserAndLogErrorf("unable to retrieve information of playbook run's channel: %v", err)
return
}
r.postCommandResponse(fmt.Sprintf("PlaybookRun successfully created: ~%s.", channel.Name))
}
func (r *Runner) actionTestData(params []string) {
if len(params) < 3 {
r.postCommandResponse("`/playbook test bulk-data` expects at least 3 arguments: [ongoing] [ended] [days]. Optionally, a fourth argument can be added: [seed].")
return
}
ongoing, err := strconv.Atoi(params[0])
if err != nil {
r.postCommandResponse(fmt.Sprintf("The provided value for ongoing playbook runs, '%s', is not an integer.", params[0]))
return
}
ended, err := strconv.Atoi(params[1])
if err != nil {
r.postCommandResponse(fmt.Sprintf("The provided value for ended playbook runs, '%s', is not an integer.", params[1]))
return
}
days, err := strconv.Atoi((params[2]))
if err != nil {
r.postCommandResponse(fmt.Sprintf("The provided value for days, '%s', is not an integer.", params[2]))
return
}
if days < 1 {
r.postCommandResponse(fmt.Sprintf("The provided value for days, '%d', is not greater than 0.", days))
return
}
begin := time.Now().AddDate(0, 0, -days)
end := time.Now()
seed := time.Now().Unix()
if len(params) > 3 {
parsedSeed, err := strconv.ParseInt(params[3], 10, 0)
if err != nil {
r.postCommandResponse(fmt.Sprintf("The provided value for the random seed, '%s', is not an integer.", params[3]))
return
}
seed = parsedSeed
}
r.generateTestData(ongoing, ended, begin, end, seed)
}
var fakeCompanyNames = []string{
"Dach Inc",
"Schuster LLC",
"Kirlin Group",
"Kohler Group",
"Ruelas S.L.",
"Armenta S.L.",
"Vega S.A.",
"Delarosa S.A.",
"Sarabia S.A.",
"Torp - Reilly",
"Heathcote Inc",
"Swift - Bruen",
"Stracke - Lemke",
"Shields LLC",
"Bruen Group",
"Senger - Stehr",
"Krogh - Eide",
"Andresen BA",
"Hagen - Holm",
"Martinsen BA",
"Holm BA",
"Berg BA",
"Fossum RFH",
"Nordskaug - Torp",
"Gran - Lunde",
"Nordby BA",
"Ryan Gruppen",
"Karlsson AB",
"Nilsson HB",
"Karlsson Group",
"Miller - Harber",
"Yost Group",
"Leuschke Group",
"Mertz Group",
"Welch LLC",
"Baumbach Group",
"Ward - Schmitt",
"Romaguera Group",
"Hickle - Kemmer",
"Stewart Corp",
}
var playbookRunNames = []string{
"Cluster servers are down",
"API performance degradation",
"Customers unable to login",
"Deployment failed",
"Build failed",
"Build timeout failure",
"Server is unresponsive",
"Server is crashing on start-up",
"MM crashes on start-up",
"Provider is down",
"Database is unresponsive",
"Database servers are down",
"Database replica lag",
"LDAP fails to sync",
"LDAP account unable to login",
"Broken MFA process",
"MFA fails to login users",
"UI is unresponsive",
"Security threat",
"Security breach",
"Customers data breach",
"SLA broken",
"MySQL max connections error",
"Postgres max connections error",
"Elastic Search unresponsive",
"Posts deleted",
"Mentions deleted",
"Replies deleted",
"Cloud server is down",
"Cloud deployment failed",
"Cloud provisioner is down",
"Cloud running out of memory",
"Unable to create new users",
"Installations in crashloop",
"Compliance report timeout",
"RN crash",
"RN out of memory",
"RN performance issues",
"MM fails to start",
"MM HA sync errors",
}
var dummyListPlaybooks = []app.Playbook{
{
Title: "Blank Playbook",
Description: "This is an example of an empty playbook",
},
{
Title: "Test playbook",
RetrospectiveEnabled: true,
StatusUpdateEnabled: true,
Checklists: []app.Checklist{
{
Title: "Identification",
Items: []app.ChecklistItem{
{
Title: "Create Jira ticket",
},
{
Title: "Add on-call team members",
State: app.ChecklistItemStateClosed,
},
{
Title: "Identify blast radius",
},
{
Title: "Identify impacted services",
},
{
Title: "Collect server data logs",
},
{
Title: "Identify blast Analyze data logs",
},
},
},
{
Title: "Resolution",
Items: []app.ChecklistItem{
{
Title: "Align on plan of attack",
},
{
Title: "Confirm resolution",
},
},
},
{
Title: "Analysis",
Items: []app.ChecklistItem{
{
Title: "Writeup root-cause analysis",
},
{
Title: "Review post-mortem",
},
},
},
},
},
{
Title: "Release 2.4",
RetrospectiveEnabled: true,
StatusUpdateEnabled: true,
Checklists: []app.Checklist{
{
Title: "Preparation",
Items: []app.ChecklistItem{
{
Title: "Invite Feature Team to Channel",
Command: "/echo ''",
},
{
Title: "Acknowledge Alert",
},
{
Title: "Get Alert Info",
Command: "/announce ~release-checklist",
},
{
Title: "Invite Escalators",
Command: "/github mvp-2.4",
},
{
Title: "Determine Priority",
},
{
Title: "Update Alert Priority",
},
},
},
{
Title: "Meeting",
Items: []app.ChecklistItem{
{
Title: "Final Testing by QA",
},
{
Title: "Prepare Deployment Documentation",
},
{
Title: "Create New Alert for User",
},
},
},
{
Title: "Deployment",
Items: []app.ChecklistItem{
{
Title: "Database Backup",
},
{
Title: "Migrate New migration File",
},
{
Title: "Deploy Backend API",
},
{
Title: "Deploy Front-end",
},
{
Title: "Create new tag in gitlab",
},
},
},
},
},
{
Title: "Incident #4281",
Description: "There is an error when accessing message from deleted channel",
RetrospectiveEnabled: true,
StatusUpdateEnabled: true,
Checklists: []app.Checklist{
{
Title: "Prepare the Jira card for this task",
Items: []app.ChecklistItem{
{
Title: "Create new Jira Card and fill the description",
},
{
Title: "Set someone to be asignee for this task",
},
{
Title: "Set story point for this card",
},
},
},
{
Title: "Resolve the issue",
Items: []app.ChecklistItem{
{
Title: "Check the root cause of the issue",
},
{
Title: "Fix the bug",
},
{
Title: "Testing the issue manually by programmer",
},
},
},
{
Title: "QA",
Items: []app.ChecklistItem{
{
Title: "Create several scenario testing",
},
{
Title: "Implement it using cypress",
},
{
Title: "Run the testing and check the result",
},
},
},
{
Title: "Deployment",
Items: []app.ChecklistItem{
{
Title: "Merge the result to branch 'master'",
},
{
Title: "Create new Merge Request",
},
{
Title: "Run deployment pipeline",
},
{
Title: "Test the result in production",
},
},
},
},
},
{
Title: "Playbooks Playbook",
Description: "Sample playbook",
RetrospectiveEnabled: true,
StatusUpdateEnabled: true,
Checklists: []app.Checklist{
{
Title: "Triage",
Items: []app.ChecklistItem{
{
Title: "Announce incident type and resources",
},
{
Title: "Acknowledge alert",
},
{
Title: "Get alert info",
},
{
Title: "Invite escalators",
},
{
Title: "Determine priority",
},
{
Title: "Update alert priority",
},
{
Title: "Update alert priority",
},
{
Title: "Create a JIRA ticket",
Command: "/jira create",
},
{
Title: "Find out who’s on call",
Command: "/genie whoisoncall",
},
{
Title: "Announce incident",
},
{
Title: "Invite on-call lead",
},
},
}, {
Title: "Investigation",
Items: []app.ChecklistItem{
{
Title: "Perform initial investigation",
},
{
Title: "Escalate to other on-call members (optional)",
},
{
Title: "Escalate to other engineering teams (optional)",
},
},
}, {
Title: "Resolution",
Items: []app.ChecklistItem{
{
Title: "Close alert",
},
{
Title: "End the incident",
Command: "/playbook end",
},
{
Title: "Schedule a post-mortem",
},
{
Title: "Record post-mortem action items",
},
{
Title: "Update playbook with learnings",
},
{
Title: "Export channel message history",
Command: "/export",
},
{
Title: "Archive this channel",
},
},
},
},
},
}
// generateTestData generates `numActivePlaybookRuns` ongoing playbook runs and
// `numEndedPlaybookRuns` ended playbook runs, whose creation timestamp lies randomly
// between the `begin` and `end` timestamps.
// All playbook runs are created with a playbook randomly picked from the ones the
// user is a member of, and the randomness is controlled by the `seed` parameter
// to create reproducible results if needed.
func (r *Runner) generateTestData(numActivePlaybookRuns, numEndedPlaybookRuns int, begin, end time.Time, seed int64) {
rand.Seed(seed)
beginMillis := begin.Unix() * 1000
endMillis := end.Unix() * 1000
numPlaybookRuns := numActivePlaybookRuns + numEndedPlaybookRuns
if numPlaybookRuns == 0 {
r.postCommandResponse("Zero playbook runs created.")
return
}
timestamps := make([]int64, 0, numPlaybookRuns)
for i := 0; i < numPlaybookRuns; i++ {
timestamp := rand.Int63n(endMillis-beginMillis) + beginMillis
timestamps = append(timestamps, timestamp)
}
requesterInfo := app.RequesterInfo{
UserID: r.args.UserId,
TeamID: r.args.TeamId,
IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api),
}
playbooksResult, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId, app.PlaybookFilterOptions{
Page: 0,
PerPage: app.PerPageDefault,
})
if err != nil {
r.warnUserAndLogErrorf("Error getting playbooks: %v", err)
return
}
var playbooks []app.Playbook
if len(playbooksResult.Items) == 0 {
for _, dummyPlaybook := range dummyListPlaybooks {
dummyPlaybook.TeamID = r.args.TeamId
dummyPlaybook.Members = []app.PlaybookMember{
{
UserID: r.args.UserId,
Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin},
},
}
newPlaybookID, err := r.playbookService.Create(dummyPlaybook, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("unable to create playbook: %v", err)
return
}
newPlaybook, err := r.playbookService.Get(newPlaybookID)
if err != nil {
r.warnUserAndLogErrorf("Error getting playbook: %v", err)
return
}
playbooks = append(playbooks, newPlaybook)
}
} else {
playbooks = make([]app.Playbook, 0, len(playbooksResult.Items))
for _, thePlaybook := range playbooksResult.Items {
wholePlaybook, err := r.playbookService.Get(thePlaybook.ID)
if err != nil {
r.warnUserAndLogErrorf("Error getting playbook: %v", err)
return
}
playbooks = append(playbooks, wholePlaybook)
}
}
tableMsg := "| Run name | Created at | Status |\n|- |- |- |\n"
playbookRuns := make([]*app.PlaybookRun, 0, numPlaybookRuns)
for i := 0; i < numPlaybookRuns; i++ {
playbook := playbooks[rand.Intn(len(playbooks))]
playbookRunName := playbookRunNames[rand.Intn(len(playbookRunNames))]
// Give a company name to 1/3 of the playbook runs created
if rand.Intn(3) == 0 {
companyName := fakeCompanyNames[rand.Intn(len(fakeCompanyNames))]
playbookRunName = fmt.Sprintf("[%s] %s", companyName, playbookRunName)
}
playbookRun, err := r.playbookRunService.CreatePlaybookRun(
&app.PlaybookRun{
Name: playbookRunName,
OwnerUserID: r.args.UserId,
TeamID: r.args.TeamId,
PlaybookID: playbook.ID,
Checklists: playbook.Checklists,
RetrospectiveEnabled: playbook.RetrospectiveEnabled,
StatusUpdateEnabled: playbook.StatusUpdateEnabled,
Type: app.RunTypePlaybook,
},
&playbook,
r.args.UserId,
true,
)
if err != nil {
r.warnUserAndLogErrorf("Error creating playbook run: %v", err)
return
}
createAt := timeutils.GetTimeForMillis(timestamps[i])
err = r.playbookRunService.ChangeCreationDate(playbookRun.ID, createAt)
if err != nil {
r.warnUserAndLogErrorf("Error changing creation date: %v", err)
return
}
channel, err := r.api.GetChannelByID(playbookRun.ChannelID)
if err != nil {
r.warnUserAndLogErrorf("Error retrieveing playbook run's channel: %v", err)
return
}
status := "Ended"
if i >= numEndedPlaybookRuns {
status = "Ongoing"
}
tableMsg += fmt.Sprintf("|~%s|%s|%s|\n", channel.Name, createAt.Format("2006-01-02"), status)
playbookRuns = append(playbookRuns, playbookRun)
}
for i := 0; i < numEndedPlaybookRuns; i++ {
err := r.playbookRunService.FinishPlaybookRun(playbookRuns[i].ID, r.args.UserId)
if err != nil {
r.warnUserAndLogErrorf("Error ending the playbook run: %v", err)
return
}
}
r.postCommandResponse(fmt.Sprintf("The test data was successfully generated:\n\n%s\n", tableMsg))
}
func (r *Runner) actionNukeDB(args []string) {
if r.api.GetConfig().ServiceSettings.EnableTesting == nil ||
!*r.api.GetConfig().ServiceSettings.EnableTesting {
r.postCommandResponse(helpText)
return
}
if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) {
r.postCommandResponse("Nuking the database is restricted to system administrators.")
return
}
if len(args) != 2 || args[0] != "CONFIRM" || args[1] != "NUKE" {
r.postCommandResponse("Are you sure you want to nuke the database (delete all data -- instances, configuration)?" +
"All data will be lost. To nuke database, type `/playbook nuke-db CONFIRM NUKE`")
return
}
if err := r.playbookRunService.NukeDB(); err != nil {
r.warnUserAndLogErrorf("There was an error while nuking db: %v", err)
return
}
r.postCommandResponse("DB has been reset.")
}
// Execute should be called by the plugin when a command invocation is received from the Mattermost server.
func (r *Runner) Execute() error {
if err := r.isValid(); err != nil {
return err
}
split := strings.Fields(r.args.Command)
command := split[0]
parameters := []string{}
cmd := ""
if len(split) > 1 {
cmd = split[1]
}
if len(split) > 2 {
parameters = split[2:]
}
if command != "/playbook" {
return nil
}
switch cmd {
case "run":
r.actionRun(parameters)
case "run-playbook":
r.actionRunPlaybook(parameters)
case "finish":
r.actionFinish(parameters)
case "finish-by-id":
r.actionFinishByID(parameters)
case "update":
r.actionUpdate(parameters)
case "check":
r.actionCheck(parameters)
case "checkadd":
r.actionAddChecklistItem(parameters)
case "checkremove":
r.actionRemoveChecklistItem(parameters)
case "owner":
r.actionOwner(parameters)
case "info":
r.actionInfo(parameters)
case "add":
r.actionAdd(parameters)
case "timeline":
r.actionTimeline(parameters)
case "todo":
r.actionTodo()
case "settings":
r.actionSettings(parameters)
case "nuke-db":
r.actionNukeDB(parameters)
case "test":
r.actionTest(parameters)
default:
r.postCommandResponse(helpText)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
// Configuration captures the plugin's external configuration as exposed in the Mattermost server
// configuration, as well as values computed from the configuration. Any public fields will be
// deserialized from the Mattermost server configuration in OnConfigurationChange.
//
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
// configuration can change at any time, access to the configuration must be synchronized. The
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
// struct whenever it changes. You may replace this with whatever strategy you choose.
//
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
// copy appropriate for your types.
type Configuration struct {
// PlaybookCreatorsUserIds is a list of users that can edit playbooks
PlaybookCreatorsUserIds []string
// EnableExperimentalFeatures determines if experimental features are enabled.
EnableExperimentalFeatures bool
// ** The following are NOT stored on the server
// AdminUserIDs contains a list of user IDs that are allowed
// to administer plugin functions, even if not Mattermost sysadmins.
AllowedUserIDs []string
// BotUserID used to post messages.
BotUserID string
// AdminLogLevel is "debug", "info", "warn", or "error".
AdminLogLevel string
// AdminLogVerbose: set to include full context with admin log messages.
AdminLogVerbose bool
}
// Clone shallow copies the configuration. Your implementation may require a deep copy if
// your configuration has reference types.
func (c *Configuration) Clone() *Configuration {
var clone = *c
return &clone
}
func (c *Configuration) serialize() map[string]interface{} {
ret := make(map[string]interface{})
ret["BotUserID"] = c.BotUserID
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"reflect"
"sync"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
// const npsPluginID = "com.mattermost.nps"
// ServiceImpl holds access to the plugin's Configuration.
type ServiceImpl struct {
api playbooks.ServicesAPI
// configurationLock synchronizes access to the configuration.
configurationLock sync.RWMutex
// configuration is the active plugin configuration. Consult getConfiguration and
// setConfiguration for usage.
configuration *Configuration
// configChangeListeners will be notified when the OnConfigurationChange event has been called.
configChangeListeners map[string]func()
}
// NewConfigService Creates a new ServiceImpl struct.
func NewConfigService(api playbooks.ServicesAPI) *ServiceImpl {
c := &ServiceImpl{}
c.api = api
c.configuration = new(Configuration)
c.configChangeListeners = make(map[string]func())
// LoadPluginConfiguration never returns an error, so ignore it.
_ = api.LoadPluginConfiguration(c.configuration)
return c
}
// GetConfiguration retrieves the active configuration under lock, making it safe to use
// concurrently. The active configuration may change underneath the client of this method, but
// the struct returned by this API call is considered immutable.
func (c *ServiceImpl) GetConfiguration() *Configuration {
c.configurationLock.RLock()
defer c.configurationLock.RUnlock()
if c.configuration == nil {
return &Configuration{}
}
return c.configuration
}
// UpdateConfiguration updates the config. Any parts of the config that are persisted in the plugin's
// section in the server's config will be saved to the server.
func (c *ServiceImpl) UpdateConfiguration(f func(*Configuration)) error {
c.configurationLock.Lock()
if c.configuration == nil {
c.configuration = &Configuration{}
}
oldStorableConfig := c.configuration.serialize()
f(c.configuration)
newStorableConfig := c.configuration.serialize()
// Don't hold the lock longer than necessary, especially since we're calling the api and then listeners.
c.configurationLock.Unlock()
if !reflect.DeepEqual(oldStorableConfig, newStorableConfig) {
if appErr := c.api.SavePluginConfig(newStorableConfig); appErr != nil {
return errors.New(appErr.Error())
}
}
for _, f := range c.configChangeListeners {
f()
}
return nil
}
// RegisterConfigChangeListener registers a function that will called when the config might have
// been changed. Returns an id which can be used to unregister the listener.
func (c *ServiceImpl) RegisterConfigChangeListener(listener func()) string {
if c.configChangeListeners == nil {
c.configChangeListeners = make(map[string]func())
}
id := model.NewId()
c.configChangeListeners[id] = listener
return id
}
// UnregisterConfigChangeListener unregisters the listener function identified by id.
func (c *ServiceImpl) UnregisterConfigChangeListener(id string) {
delete(c.configChangeListeners, id)
}
// OnConfigurationChange is invoked when configuration changes may have been made.
// This method satisfies the interface expected by the server. Embed config.Config in the plugin.
func (c *ServiceImpl) OnConfigurationChange() error {
// Have we been setup by OnActivate?
if c.api == nil {
return nil
}
var configuration = new(Configuration)
// Load the public configuration fields from the Mattermost server configuration.
if err := c.api.LoadPluginConfiguration(configuration); err != nil {
return errors.Wrapf(err, "failed to load plugin configuration")
}
configuration.BotUserID = c.configuration.BotUserID
c.setConfiguration(configuration)
for _, f := range c.configChangeListeners {
f()
}
return nil
}
// setConfiguration replaces the active configuration under lock.
//
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
//
// This method panics if setConfiguration is called with the existing configuration. This almost
// certainly means that the configuration was modified without being cloned and may result in
// an unsafe access.
func (c *ServiceImpl) setConfiguration(configuration *Configuration) {
c.configurationLock.Lock()
defer c.configurationLock.Unlock()
if configuration != nil && c.configuration == configuration {
// Ignore assignment if the configuration struct is empty. Go will optimize the
// allocation for same to point at the same memory address, breaking the check
// above.
if reflect.ValueOf(*configuration).NumField() == 0 {
return
}
panic("setConfiguration called with the existing configuration")
}
c.configuration = configuration
}
// IsConfiguredForDevelopmentAndTesting returns true when the server has `EnableDeveloper` and
// `EnableTesting` configuration settings enabled.
func (c *ServiceImpl) IsConfiguredForDevelopmentAndTesting() bool {
config := c.api.GetConfig()
return config != nil &&
config.ServiceSettings.EnableTesting != nil &&
*config.ServiceSettings.EnableTesting &&
config.ServiceSettings.EnableDeveloper != nil &&
*config.ServiceSettings.EnableDeveloper
}
// IsCloud returns true when the server is on cloud, and false otherwise
func (c *ServiceImpl) IsCloud() bool {
license := c.api.GetLicense()
if license == nil || license.Features == nil || license.Features.Cloud == nil {
return false
}
return *license.Features.Cloud
}
// SupportsGivingFeedback returns nil when the nps plugin is installed and enabled, thus enabling giving feedback.
func (c *ServiceImpl) SupportsGivingFeedback() error {
//TODO: Do we need this functions?
// pluginState := c.pluginAPIAdapter.GetConfig().PluginSettings.PluginStates[npsPluginID]
// if pluginState == nil || !pluginState.Enable {
// return errors.New("nps plugin not enabled")
// }
// pluginStatus, err := c.api.Plugin.GetPluginStatus(npsPluginID)
// if err != nil {
// return fmt.Errorf("failed to query nps plugin status: %w", err)
// }
// if pluginStatus == nil {
// return errors.New("nps plugin not running")
// }
return errors.New("can't get nps plugin status")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package enterprise
import (
"github.com/mattermost/mattermost-server/v6/server/playbooks/product/pluginapi"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
type LicenseChecker struct {
api playbooks.ServicesAPI
}
func NewLicenseChecker(api playbooks.ServicesAPI) *LicenseChecker {
return &LicenseChecker{
api,
}
}
// isAtLeastE20Licensed returns true when the server either has an E20 license or is configured for development.
func (e *LicenseChecker) isAtLeastE20Licensed() bool {
config := e.api.GetConfig()
license := e.api.GetLicense()
return pluginapi.IsE20LicensedOrDevelopment(config, license)
}
// isAtLeastE10Licensed returns true when the server either has at least an E10 license or is configured for development.
func (e *LicenseChecker) isAtLeastE10Licensed() bool {
config := e.api.GetConfig()
license := e.api.GetLicense()
return pluginapi.IsE10LicensedOrDevelopment(config, license)
}
// PlaybookAllowed returns true if the specified playbook is valid with the current license.
func (e *LicenseChecker) PlaybookAllowed(isPlaybookPublic bool) bool {
// Private playbooks are E20-only
return e.isAtLeastE20Licensed() || isPlaybookPublic
}
// RetrospectiveAllowed returns true if the retrospective feature is allowed with the current license.
func (e *LicenseChecker) RetrospectiveAllowed() bool {
return e.isAtLeastE10Licensed()
}
// TimelineAllowed returns true if the timeline feature is allowed with the current license.
func (e *LicenseChecker) TimelineAllowed() bool {
return e.isAtLeastE10Licensed()
}
// StatsAllowed returns true if the stats feature is allowed with the current license.
func (e *LicenseChecker) StatsAllowed() bool {
return e.isAtLeastE20Licensed()
}
// ChecklistItemDueDateAllowed returns true if setting/editing checklist item due date is allowed.
func (e *LicenseChecker) ChecklistItemDueDateAllowed() bool {
return e.isAtLeastE10Licensed()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httptools
import (
"net"
"net/http"
"strings"
"time"
"unicode"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
)
func MakeClient(api playbooks.ServicesAPI) *http.Client {
return &http.Client{
Transport: MakeTransport(api),
Timeout: 30 * time.Second,
}
}
func splitFields(c rune) bool {
return unicode.IsSpace(c) || c == ','
}
// Copy paste with adaptations from sercvices/httpservice/httpservice.go in the future that package will be adapted
// to be used by the suite and this should be replaced.
func MakeTransport(api playbooks.ServicesAPI) *httpservice.MattermostTransport {
insecure := api.GetConfig().ServiceSettings.EnableInsecureOutgoingConnections != nil && *api.GetConfig().ServiceSettings.EnableInsecureOutgoingConnections
allowHost := func(host string) bool {
if api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
for _, allowed := range strings.FieldsFunc(*api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if host == allowed {
return true
}
}
return false
}
allowIP := func(ip net.IP) bool {
reservedIP := httpservice.IsReservedIP(ip)
ownIP, err := httpservice.IsOwnIP(ip)
// If there is an error getting the self-assigned IPs, default to the secure option
if err != nil {
return false
}
// If it's not a reserved IP and it's not self-assigned IP, accept the IP
if !reservedIP && !ownIP {
return true
}
if api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
// In the case it's the self-assigned IP, enforce that it needs to be explicitly added to the AllowedUntrustedInternalConnections
for _, allowed := range strings.FieldsFunc(*api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) {
return true
}
}
return false
}
return httpservice.NewTransport(insecure, allowHost, allowIP)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package metrics
import (
"os"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
const (
MetricsNamespace = "playbooks_plugin"
MetricsSubsystemPlaybooks = "playbooks"
MetricsSubsystemRuns = "runs"
MetricsSubsystemSystem = "system"
MetricsCloudInstallationLabel = "installationId"
)
type InstanceInfo struct {
Version string
InstallationID string
}
// Metrics used to instrumentate metrics in prometheus.
type Metrics struct {
registry *prometheus.Registry
instance *prometheus.GaugeVec
playbooksCreatedCount prometheus.Counter
playbooksArchivedCount prometheus.Counter
playbooksRestoredCount prometheus.Counter
runsCreatedCount prometheus.Counter
runsFinishedCount prometheus.Counter
errorsCount prometheus.Counter
playbooksActiveTotal prometheus.Gauge
runsActiveTotal prometheus.Gauge
remindersOutstandingTotal prometheus.Gauge
retrosOutstandingTotal prometheus.Gauge
followersActiveTotal prometheus.Gauge
participantsActiveTotal prometheus.Gauge
}
// NewMetrics Factory method to create a new metrics collector.
func NewMetrics(info InstanceInfo) *Metrics {
m := &Metrics{}
m.registry = prometheus.NewRegistry()
options := collectors.ProcessCollectorOpts{
Namespace: MetricsNamespace,
}
m.registry.MustRegister(collectors.NewProcessCollector(options))
m.registry.MustRegister(collectors.NewGoCollector())
additionalLabels := map[string]string{}
if info.InstallationID != "" {
additionalLabels[MetricsCloudInstallationLabel] = os.Getenv("MM_CLOUD_INSTALLATION_ID")
}
m.instance = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "playbook_instance_info",
Help: "Instance information for Playbook.",
ConstLabels: additionalLabels,
}, []string{"Version"})
m.registry.MustRegister(m.instance)
m.instance.WithLabelValues(info.Version).Set(1)
m.playbooksCreatedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemPlaybooks,
Name: "playbook_created_count",
Help: "Number of playbooks created since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.playbooksCreatedCount)
m.playbooksArchivedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemPlaybooks,
Name: "playbook_archived_count",
Help: "Number of playbooks archived since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.playbooksArchivedCount)
m.playbooksRestoredCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemPlaybooks,
Name: "playbook_restored_count",
Help: "Number of playbooks restored since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.playbooksRestoredCount)
m.runsCreatedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "runs_created_count",
Help: "Number of runs created since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.runsCreatedCount)
m.runsFinishedCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "runs_finished_count",
Help: "Number of runs finished since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.runsFinishedCount)
m.errorsCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemSystem,
Name: "errors_count",
Help: "Number of errors since the last launch.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.errorsCount)
m.playbooksActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemPlaybooks,
Name: "playbooks_active_total",
Help: "Total number of active playbooks.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.playbooksActiveTotal)
m.runsActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "runs_active_total",
Help: "Total number of active runs.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.runsActiveTotal)
m.remindersOutstandingTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "reminders_outstanding_total",
Help: "Total number of outstanding reminders.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.remindersOutstandingTotal)
m.retrosOutstandingTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "retros_outstanding_total",
Help: "Total number of outstanding retrospective reminders.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.retrosOutstandingTotal)
m.followersActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "followers_active_total",
Help: "Total number of active followers, including duplicates.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.followersActiveTotal)
m.participantsActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemRuns,
Name: "participants_active_total",
Help: "Total number of active participants (i.e. members of the playbook run channel when the run is active), including duplicates",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.participantsActiveTotal)
return m
}
func (m *Metrics) IncrementPlaybookCreatedCount(num int) {
if m != nil {
m.playbooksCreatedCount.Add(float64(num))
}
}
func (m *Metrics) IncrementPlaybookArchivedCount(num int) {
if m != nil {
m.playbooksArchivedCount.Add(float64(num))
}
}
func (m *Metrics) IncrementPlaybookRestoredCount(num int) {
if m != nil {
m.playbooksRestoredCount.Add(float64(num))
}
}
func (m *Metrics) IncrementRunsCreatedCount(num int) {
if m != nil {
m.runsCreatedCount.Add(float64(num))
}
}
func (m *Metrics) IncrementRunsFinishedCount(num int) {
if m != nil {
m.runsFinishedCount.Add(float64(num))
}
}
func (m *Metrics) IncrementErrorsCount(num int) {
if m != nil {
m.errorsCount.Add(float64(num))
}
}
func (m *Metrics) ObservePlaybooksActiveTotal(count int64) {
if m != nil {
m.playbooksActiveTotal.Set(float64(count))
}
}
func (m *Metrics) ObserveRunsActiveTotal(count int64) {
if m != nil {
m.runsActiveTotal.Set(float64(count))
}
}
func (m *Metrics) ObserveRemindersOutstandingTotal(count int64) {
if m != nil {
m.remindersOutstandingTotal.Set(float64(count))
}
}
func (m *Metrics) ObserveRetrosOutstandingTotal(count int64) {
if m != nil {
m.retrosOutstandingTotal.Set(float64(count))
}
}
func (m *Metrics) ObserveFollowersActiveTotal(count int64) {
if m != nil {
m.followersActiveTotal.Set(float64(count))
}
}
func (m *Metrics) ObserveParticipantsActiveTotal(count int64) {
if m != nil {
m.participantsActiveTotal.Set(float64(count))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package metrics
import (
"net/http"
"time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
// Service prometheus to run the server.
type Service struct {
*http.Server
}
type ErrorLoggerWrapper struct {
}
func (el *ErrorLoggerWrapper) Println(v ...interface{}) {
logrus.Warn("metric server error", v)
}
// NewMetricsServer factory method to create a new prometheus server.
func NewMetricsServer(address string, metricsService *Metrics) *Service {
return &Service{
&http.Server{
ReadTimeout: 30 * time.Second,
Addr: address,
Handler: promhttp.HandlerFor(metricsService.registry, promhttp.HandlerOpts{
ErrorLog: &ErrorLoggerWrapper{},
}),
},
}
}
// Run will start the prometheus server.
func (h *Service) Run() error {
return errors.Wrap(h.Server.ListenAndServe(), "prometheus ListenAndServe")
}
// Shutdown will shutdown the prometheus server.
func (h *Service) Shutdown() error {
return errors.Wrap(h.Server.Close(), "prometheus Close")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package scheduler
import (
"fmt"
"time"
)
type TaskFunc func()
type ScheduledTask struct {
Name string `json:"name"`
Interval time.Duration `json:"interval"`
Recurring bool `json:"recurring"`
function func()
cancel chan struct{}
cancelled chan struct{}
}
// WARNING: Tasks will run on every cluster node, so use this carefully.
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask {
return createTask(name, function, timeToExecution, false)
}
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
return createTask(name, function, interval, true)
}
func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: interval,
Recurring: recurring,
function: function,
cancel: make(chan struct{}),
cancelled: make(chan struct{}),
}
go func() {
defer close(task.cancelled)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
function()
case <-task.cancel:
return
}
if !task.Recurring {
break
}
}
}()
return task
}
func (task *ScheduledTask) Cancel() {
close(task.cancel)
<-task.cancelled
}
func (task *ScheduledTask) String() string {
return fmt.Sprintf(
"%s\nInterval: %s\nRecurring: %t\n",
task.Name,
task.Interval.String(),
task.Recurring,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it.
type channelActionStore struct {
pluginAPI PluginAPIClient
store *SQLStore
queryBuilder sq.StatementBuilderType
channelActionSelect sq.SelectBuilder
}
// NewPlaybookStore creates a new store for playbook service.
func NewChannelActionStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.ChannelActionStore {
channelActionSelect := sqlStore.builder.
Select(
"c.ID",
"c.ChannelID",
"c.Enabled",
"c.DeleteAt",
"c.ActionType",
"c.TriggerType",
"c.Payload",
).
From("IR_ChannelAction c")
return &channelActionStore{
pluginAPI: pluginAPI,
store: sqlStore,
queryBuilder: sqlStore.builder,
channelActionSelect: channelActionSelect,
}
}
// Create creates a new playbook
func (c *channelActionStore) Create(action app.GenericChannelAction) (string, error) {
if action.ID != "" {
return "", errors.New("ID should be empty")
}
action.ID = model.NewId()
payloadJSON, err := json.Marshal(action.Payload)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal payload json for action id: %q", action.ID)
}
if len(payloadJSON) > maxJSONLength {
return "", errors.Wrapf(errors.New("invalid data"), "payload json for action id '%s' is too long (max %d)", action.ID, maxJSONLength)
}
_, err = c.store.execBuilder(c.store.db, sq.
Insert("IR_ChannelAction").
SetMap(map[string]interface{}{
"ID": action.ID,
"ChannelID": action.ChannelID,
"Enabled": action.Enabled,
"DeleteAt": action.DeleteAt,
"ActionType": action.ActionType,
"TriggerType": action.TriggerType,
"Payload": payloadJSON,
}))
if err != nil {
return "", errors.Wrap(err, "failed to store new action")
}
return action.ID, nil
}
func (c *channelActionStore) Get(id string) (app.GenericChannelAction, error) {
if !model.IsValidId(id) {
return app.GenericChannelAction{}, errors.New("ID is not valid")
}
var action app.GenericChannelAction
err := c.store.getBuilder(c.store.db, &action, c.channelActionSelect.Where(sq.Eq{"c.ID": id}))
if err == sql.ErrNoRows {
return app.GenericChannelAction{}, errors.Wrapf(app.ErrNotFound, "action does not exist for id %q", id)
} else if err != nil {
return app.GenericChannelAction{}, errors.Wrapf(err, "failed to get action by id %q", id)
}
return action, nil
}
type sqlGenericChannelAction struct {
app.GenericChannelActionWithoutPayload
Payload json.RawMessage
}
func (c *channelActionStore) GetChannelActions(channelID string, options app.GetChannelActionOptions) ([]app.GenericChannelAction, error) {
if !model.IsValidId(channelID) {
return nil, errors.New("ID is not valid")
}
query := c.channelActionSelect.Where(sq.Eq{"c.ChannelID": channelID})
if options.TriggerType != "" {
query = query.Where(sq.Eq{"c.TriggerType": options.TriggerType})
}
if options.ActionType != "" {
query = query.Where(sq.Eq{"c.ActionType": options.ActionType})
}
sqlActions := []sqlGenericChannelAction{}
err := c.store.selectBuilder(c.store.db, &sqlActions, query)
if err == sql.ErrNoRows {
return nil, errors.Wrapf(app.ErrNotFound, "no actions for channel id %q", channelID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get actions for channel id %q", channelID)
}
actions := make([]app.GenericChannelAction, 0, len(sqlActions))
for _, sqlAction := range sqlActions {
switch sqlAction.ActionType {
case app.ActionTypeWelcomeMessage:
var welcomePayload app.WelcomeMessagePayload
if err := json.Unmarshal(sqlAction.Payload, &welcomePayload); err != nil {
return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID)
}
action := app.GenericChannelAction{
GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload,
Payload: welcomePayload,
}
actions = append(actions, action)
case app.ActionTypePromptRunPlaybook:
var promptRunPlaybookPayload app.PromptRunPlaybookFromKeywordsPayload
if err := json.Unmarshal(sqlAction.Payload, &promptRunPlaybookPayload); err != nil {
return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID)
}
action := app.GenericChannelAction{
GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload,
Payload: promptRunPlaybookPayload,
}
actions = append(actions, action)
case app.ActionTypeCategorizeChannel:
var categorizeChannelPayload app.CategorizeChannelPayload
if err := json.Unmarshal(sqlAction.Payload, &categorizeChannelPayload); err != nil {
return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID)
}
action := app.GenericChannelAction{
GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload,
Payload: categorizeChannelPayload,
}
actions = append(actions, action)
}
}
return actions, nil
}
func (c *channelActionStore) Update(action app.GenericChannelAction) error {
if action.ID == "" {
return errors.New("id should not be empty")
}
payloadJSON, err := json.Marshal(action.Payload)
if err != nil {
return errors.Wrapf(err, "failed to marshal payload json for action id: %q", action.ID)
}
_, err = c.store.execBuilder(c.store.db, sq.
Update("IR_ChannelAction").
SetMap(map[string]interface{}{
"ID": action.ID,
"ChannelID": action.ChannelID,
"Enabled": action.Enabled,
"DeleteAt": action.DeleteAt,
"ActionType": action.ActionType,
"TriggerType": action.TriggerType,
"Payload": payloadJSON,
}).
Where(sq.Eq{"ID": action.ID}))
if err != nil {
return errors.Wrapf(err, "failed to update action with id '%s'", action.ID)
}
return nil
}
// HasViewed returns true if userID has viewed channelID
func (c *channelActionStore) HasViewedChannel(userID, channelID string) bool {
query := sq.Expr(
`SELECT EXISTS(SELECT *
FROM IR_ViewedChannel as vc
WHERE vc.ChannelID = ?
AND vc.UserID = ?)
`, channelID, userID)
var exists bool
err := c.store.getBuilder(c.store.db, &exists, query)
if err != nil {
return false
}
return exists
}
// SetViewed records that userID has viewed channelID.
func (c *channelActionStore) SetViewedChannel(userID, channelID string) error {
if c.HasViewedChannel(userID, channelID) {
return nil
}
_, err := c.store.execBuilder(c.store.db, sq.
Insert("IR_ViewedChannel").
SetMap(map[string]interface{}{
"ChannelID": channelID,
"UserID": userID,
}))
if err != nil {
if c.store.db.DriverName() == model.DatabaseDriverMysql {
me, ok := err.(*mysql.MySQLError)
if ok && me.Number == 1062 {
return errors.Wrap(app.ErrDuplicateEntry, err.Error())
}
} else {
pe, ok := err.(*pq.Error)
if ok && pe.Code == "23505" {
return errors.Wrap(app.ErrDuplicateEntry, err.Error())
}
}
return errors.Wrapf(err, "failed to store userID and channelID")
}
return nil
}
func (c *channelActionStore) SetMultipleViewedChannel(userIDs []string, channelID string) error {
tx, err := c.store.db.Beginx()
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer c.store.finalizeTransaction(tx)
// Retrieve the users that have already viewed the channel
var usersToSkip []string
err = c.store.selectBuilder(tx, &usersToSkip, sq.
Select("UserID").
From("IR_ViewedChannel").
Where(sq.Eq{
"UserID": userIDs,
"ChannelID": channelID,
}))
if err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "unable to retrieve users that have already viewed the channel")
}
// Build a map out of the previous users for fast lookup
usersToSkipMap := make(map[string]bool)
for _, user := range usersToSkip {
usersToSkipMap[user] = true
}
// Filter out the users in the map from the original array
usersToSet := []string{}
for _, user := range userIDs {
if !usersToSkipMap[user] {
usersToSet = append(usersToSet, user)
}
}
if len(usersToSet) == 0 {
return nil
}
// Set the channelID as viewed for every user in usersToSet
query := sq.
Insert("IR_ViewedChannel").
Columns("UserID", "ChannelID")
for _, user := range usersToSet {
query = query.Values(user, channelID)
}
_, err = c.store.execBuilder(c.store.db, query)
if err != nil {
// If there's an error, return a specific one if possible
if c.store.db.DriverName() == model.DatabaseDriverMysql {
me, ok := err.(*mysql.MySQLError)
if ok && me.Number == 1062 {
return errors.Wrap(app.ErrDuplicateEntry, err.Error())
}
} else {
pe, ok := err.(*pq.Error)
if ok && pe.Code == "23505" {
return errors.Wrap(app.ErrDuplicateEntry, err.Error())
}
}
return errors.Wrapf(err, "failed to store userIDs and channelID")
}
if err = tx.Commit(); err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it.
type categoryStore struct {
pluginAPI PluginAPIClient
store *SQLStore
queryBuilder sq.StatementBuilderType
categorySelect sq.SelectBuilder
categoryItemSelect sq.SelectBuilder
}
// Ensure playbookStore implements the playbook.Store interface.
var _ app.CategoryStore = (*categoryStore)(nil)
func NewCategoryStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.CategoryStore {
categorySelect := sqlStore.builder.
Select(
"c.ID",
"c.Name",
"c.TeamID",
"c.UserID",
"c.Collapsed",
"c.CreateAt",
"c.UpdateAt",
"c.DeleteAt",
).
From("IR_Category c")
categoryItemSelect := sqlStore.builder.
Select(
"ci.ItemID",
"ci.Type",
).
From("IR_Category_Item ci")
return &categoryStore{
pluginAPI: pluginAPI,
store: sqlStore,
queryBuilder: sqlStore.builder,
categorySelect: categorySelect,
categoryItemSelect: categoryItemSelect,
}
}
// Get retrieves a Category. Returns ErrNotFound if not found.
func (c *categoryStore) Get(id string) (app.Category, error) {
if !model.IsValidId(id) {
return app.Category{}, errors.New("ID is not valid")
}
var category app.Category
err := c.store.getBuilder(c.store.db, &category, c.categorySelect.Where(sq.Eq{"c.ID": id}))
if err == sql.ErrNoRows {
return app.Category{}, errors.Wrapf(app.ErrNotFound, "category does not exist for id %q", id)
} else if err != nil {
return app.Category{}, errors.Wrapf(err, "failed to get category by id %q", id)
}
items, err := c.getItems(id)
if err != nil {
return app.Category{}, errors.Wrapf(err, "failed to get category items by id %q", id)
}
category.Items = items
return category, nil
}
func (c *categoryStore) getItems(id string) ([]app.CategoryItem, error) {
var items []app.CategoryItem
var playbookItems []app.CategoryItem
queryPlaybooks := c.queryBuilder.
Select(
"ci.ItemID",
"ci.Type",
"COALESCE(p.title, '') AS Name",
"COALESCE(p.public, false) AS Public",
).
From("IR_Category_Item ci").
LeftJoin("IR_Playbook as p on ci.ItemID=p.id").
Where(sq.And{sq.Eq{"ci.CategoryID": id}, sq.Eq{"ci.Type": "p"}})
err := c.store.selectBuilder(c.store.db, &playbookItems, queryPlaybooks)
if err == sql.ErrNoRows {
items = []app.CategoryItem{}
} else if err != nil {
return []app.CategoryItem{}, err
} else {
items = playbookItems
}
var runItems []app.CategoryItem
queryRuns := c.queryBuilder.
Select(
"ci.ItemID",
"ci.Type",
"COALESCE(r.name, '') AS Name",
).
From("IR_Category_Item ci").
LeftJoin("IR_Incident as r on ci.ItemID=r.id").
Where(sq.And{sq.Eq{"ci.CategoryID": id}, sq.Eq{"ci.Type": "r"}})
err = c.store.selectBuilder(c.store.db, &runItems, queryRuns)
if err == sql.ErrNoRows {
return items, nil
} else if err != nil {
return []app.CategoryItem{}, err
}
items = append(items, runItems...)
return items, nil
}
// Create creates a new Category
func (c *categoryStore) Create(category app.Category) error {
if _, err := c.store.execBuilder(c.store.db, sq.
Insert("IR_Category").
SetMap(map[string]interface{}{
"ID": category.ID,
"Name": category.Name,
"TeamID": category.TeamID,
"UserID": category.UserID,
"Collapsed": category.Collapsed,
"CreateAt": category.CreateAt,
"UpdateAt": category.UpdateAt,
})); err != nil {
return errors.Wrap(err, "failed to store new category")
}
return nil
}
// GetCategories retrieves all categories for user for team
func (c *categoryStore) GetCategories(teamID, userID string) ([]app.Category, error) {
query := c.categorySelect.Where(sq.And{sq.Eq{"c.TeamID": teamID}, sq.Eq{"c.UserID": userID}})
categories := []app.Category{}
err := c.store.selectBuilder(c.store.db, &categories, query)
if err == sql.ErrNoRows {
return nil, errors.Wrapf(app.ErrNotFound, "no category for team id %q and user id %q", teamID, userID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get categories for team id %q and user id %q", teamID, userID)
}
for i, category := range categories {
items, err := c.getItems(category.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get category items for category id %q", category.ID)
}
categories[i].Items = items
}
return categories, nil
}
// Update updates a category
func (c *categoryStore) Update(category app.Category) error {
if _, err := c.store.execBuilder(c.store.db, sq.
Update("IR_Category").
Set("Name", category.Name).
Set("UpdateAt", category.UpdateAt).
Set("Collapsed", category.Collapsed).
Where(sq.Eq{"ID": category.ID})); err != nil {
return errors.Wrapf(err, "failed to update category with id '%s'", category.ID)
}
return nil
}
// Delete deletes a category
func (c *categoryStore) Delete(categoryID string) error {
if _, err := c.store.execBuilder(c.store.db, sq.
Update("IR_Category").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"ID": categoryID})); err != nil {
return errors.Wrapf(err, "failed to delete category with id '%s'", categoryID)
}
return nil
}
// GetFavoriteCategory returns favorite category
func (c *categoryStore) GetFavoriteCategory(teamID, userID string) (app.Category, error) {
var category app.Category
err := c.store.getBuilder(c.store.db, &category, c.categorySelect.Where(sq.Eq{
"c.Name": "Favorite",
"c.TeamID": teamID,
"c.UserID": userID,
}))
if err == sql.ErrNoRows {
return app.Category{}, err
}
category.Items, err = c.getItems(category.ID)
if err != nil {
return app.Category{}, errors.Wrap(err, "failed to get Items for category")
}
return category, nil
}
// createFavoriteCategory creates and returns favorite category
func (c *categoryStore) createFavoriteCategory(teamID, userID string) (app.Category, error) {
now := model.GetMillis()
favCat := app.Category{
ID: model.NewId(),
Name: "Favorite",
TeamID: teamID,
UserID: userID,
Collapsed: false,
CreateAt: now,
UpdateAt: now,
Items: []app.CategoryItem{},
}
if err := c.Create(favCat); err != nil {
return app.Category{}, errors.Wrap(err, "can't create favorite category")
}
return favCat, nil
}
// AddItemToFavoriteCategory adds an item to favorite category,
// if favorite category does not exist it creates one
func (c *categoryStore) AddItemToFavoriteCategory(item app.CategoryItem, teamID, userID string) error {
favoriteCategory, err := c.GetFavoriteCategory(teamID, userID)
if err == sql.ErrNoRows {
// No favorite category, we should create one
if favoriteCategory, err = c.createFavoriteCategory(teamID, userID); err != nil {
return err
}
} else if err != nil {
return errors.Wrap(err, "can't get favorite category")
}
for _, favItem := range favoriteCategory.Items {
if favItem.ItemID == item.ItemID && favItem.Type == item.Type {
return errors.New("Item already is favorite")
}
}
if err := c.AddItemToCategory(item, favoriteCategory.ID); err != nil {
return errors.Wrap(err, "can't add item to favorite category")
}
return nil
}
// AddItemToCategory adds an item to category
func (c *categoryStore) AddItemToCategory(item app.CategoryItem, categoryID string) error {
if _, err := c.store.execBuilder(c.store.db, sq.
Insert("IR_Category_Item").
SetMap(map[string]interface{}{
"CategoryID": categoryID,
"ItemID": item.ItemID,
"Type": item.Type,
})); err != nil {
return errors.Wrap(err, "failed to store item in category")
}
return nil
}
// DeleteItemFromCategory deletes an item from category
func (c *categoryStore) DeleteItemFromCategory(item app.CategoryItem, categoryID string) error {
if _, err := c.store.execBuilder(c.store.db, sq.
Delete("IR_Category_Item").
Where(sq.Eq{
"CategoryID": categoryID,
"ItemID": item.ItemID,
"Type": item.Type,
})); err != nil {
return errors.Wrapf(err, "failed to delete category with item id '%s'", item.ItemID)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"embed"
"fmt"
"path/filepath"
"github.com/blang/semver"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/morph"
"github.com/mattermost/morph/drivers"
"github.com/mattermost/morph/sources"
"github.com/mattermost/morph/sources/embedded"
"github.com/pkg/errors"
"github.com/mattermost/morph/drivers/mysql"
"github.com/mattermost/morph/drivers/postgres"
)
//go:embed migrations
var assets embed.FS
// RunMigrations will run the migrations (if any). The caller should hold a cluster mutex if there
// is a danger of this being run on multiple servers at once.
func (sqlStore *SQLStore) RunMigrations() error {
currentSchemaVersion, err := sqlStore.GetCurrentVersion()
if err != nil {
return errors.Wrapf(err, "failed to get the current schema version")
}
// WARNING: Disable morph migrations until proper testing
// if err := sqlStore.runMigrationsWithMorph(); err != nil {
// return fmt.Errorf("failed to complete migrations (with morph): %w", err)
// }
if currentSchemaVersion.LT(LatestVersion()) {
if err := sqlStore.runMigrationsLegacy(currentSchemaVersion); err != nil {
return errors.Wrapf(err, "failed to complete migrations")
}
}
return nil
}
func (sqlStore *SQLStore) runMigrationsLegacy(originalSchemaVersion semver.Version) error {
currentSchemaVersion := originalSchemaVersion
for _, migration := range migrations {
if !currentSchemaVersion.EQ(migration.fromVersion) {
continue
}
if err := sqlStore.migrate(migration); err != nil {
return err
}
currentSchemaVersion = migration.toVersion
}
return nil
}
func (sqlStore *SQLStore) migrate(migration Migration) (err error) {
tx, err := sqlStore.db.Beginx()
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer sqlStore.finalizeTransaction(tx)
if err := migration.migrationFunc(tx, sqlStore); err != nil {
return errors.Wrapf(err, "error executing migration from version %s to version %s", migration.fromVersion.String(), migration.toVersion.String())
}
if err := sqlStore.SetCurrentVersion(tx, migration.toVersion); err != nil {
return errors.Wrapf(err, "failed to set the current version to %s", migration.toVersion.String())
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}
func (sqlStore *SQLStore) createDriver() (drivers.Driver, error) {
driverName := sqlStore.db.DriverName()
var driver drivers.Driver
var err error
switch driverName {
case model.DatabaseDriverMysql:
driver, err = mysql.WithInstance(sqlStore.db.DB)
case model.DatabaseDriverPostgres:
driver, err = postgres.WithInstance(sqlStore.db.DB)
default:
err = fmt.Errorf("unsupported database type %s for migration", driverName)
}
return driver, err
}
func (sqlStore *SQLStore) createSource() (sources.Source, error) {
driverName := sqlStore.db.DriverName()
assetsList, err := assets.ReadDir(filepath.Join("migrations", driverName))
if err != nil {
return nil, err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNamesForDriver[i] = entry.Name()
}
src, err := embedded.WithInstance(&embedded.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(filepath.Join("migrations", driverName, name))
},
})
return src, err
}
func (sqlStore *SQLStore) createMorphEngine() (*morph.Morph, error) {
src, err := sqlStore.createSource()
if err != nil {
return nil, err
}
driver, err := sqlStore.createDriver()
if err != nil {
return nil, err
}
opts := []morph.EngineOption{
morph.WithLock("mm-playbooks-lock-key"),
morph.SetMigrationTableName("IR_db_migrations"),
morph.SetStatementTimeoutInSeconds(100000),
}
engine, err := morph.New(context.Background(), driver, src, opts...)
return engine, err
}
// WARNING: We don't use morph migration until proper testing
// func (sqlStore *SQLStore) runMigrationsWithMorph() error {
// engine, err := sqlStore.createMorphEngine()
// if err != nil {
// return err
// }
// defer engine.Close()
// if err := engine.ApplyAll(); err != nil {
// return fmt.Errorf("could not apply migrations: %w", err)
// }
// return nil
// }
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"encoding/json"
"fmt"
"strings"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/blang/semver"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type Migration struct {
fromVersion semver.Version
toVersion semver.Version
migrationFunc func(sqlx.Ext, *SQLStore) error
}
const MySQLCharset = "DEFAULT CHARACTER SET utf8mb4"
var migrations = []Migration{
{
fromVersion: semver.MustParse("0.0.0"),
toVersion: semver.MustParse("0.1.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_System (
SKey VARCHAR(64) PRIMARY KEY,
SValue VARCHAR(1024) NULL
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_System")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Incident (
ID VARCHAR(26) PRIMARY KEY,
Name VARCHAR(1024) NOT NULL,
Description VARCHAR(4096) NOT NULL,
IsActive BOOLEAN NOT NULL,
CommanderUserID VARCHAR(26) NOT NULL,
TeamID VARCHAR(26) NOT NULL,
ChannelID VARCHAR(26) NOT NULL UNIQUE,
CreateAt BIGINT NOT NULL,
EndAt BIGINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ActiveStage BIGINT NOT NULL,
PostID VARCHAR(26) NOT NULL DEFAULT '',
PlaybookID VARCHAR(26) NOT NULL DEFAULT '',
ChecklistsJSON TEXT NOT NULL,
INDEX IR_Incident_TeamID (TeamID),
INDEX IR_Incident_TeamID_CommanderUserID (TeamID, CommanderUserID),
INDEX IR_Incident_ChannelID (ChannelID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Incident")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Playbook (
ID VARCHAR(26) PRIMARY KEY,
Title VARCHAR(1024) NOT NULL,
Description VARCHAR(4096) NOT NULL,
TeamID VARCHAR(26) NOT NULL,
CreatePublicIncident BOOLEAN NOT NULL,
CreateAt BIGINT NOT NULL,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ChecklistsJSON TEXT NOT NULL,
NumStages BIGINT NOT NULL DEFAULT 0,
NumSteps BIGINT NOT NULL DEFAULT 0,
INDEX IR_Playbook_TeamID (TeamID),
INDEX IR_PlaybookMember_PlaybookID (ID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Playbook")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_PlaybookMember (
PlaybookID VARCHAR(26) NOT NULL REFERENCES IR_Playbook(ID),
MemberID VARCHAR(26) NOT NULL,
INDEX IR_PlaybookMember_PlaybookID (PlaybookID),
INDEX IR_PlaybookMember_MemberID (MemberID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_PlaybookMember")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_System (
SKey VARCHAR(64) PRIMARY KEY,
SValue VARCHAR(1024) NULL
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_System")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Incident (
ID TEXT PRIMARY KEY,
Name TEXT NOT NULL,
Description TEXT NOT NULL,
IsActive BOOLEAN NOT NULL,
CommanderUserID TEXT NOT NULL,
TeamID TEXT NOT NULL,
ChannelID TEXT NOT NULL UNIQUE,
CreateAt BIGINT NOT NULL,
EndAt BIGINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ActiveStage BIGINT NOT NULL,
PostID TEXT NOT NULL DEFAULT '',
PlaybookID TEXT NOT NULL DEFAULT '',
ChecklistsJSON JSON NOT NULL
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Incident")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Playbook (
ID TEXT PRIMARY KEY,
Title TEXT NOT NULL,
Description TEXT NOT NULL,
TeamID TEXT NOT NULL,
CreatePublicIncident BOOLEAN NOT NULL,
CreateAt BIGINT NOT NULL,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ChecklistsJSON JSON NOT NULL,
NumStages BIGINT NOT NULL DEFAULT 0,
NumSteps BIGINT NOT NULL DEFAULT 0
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Playbook")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_PlaybookMember (
PlaybookID TEXT NOT NULL REFERENCES IR_Playbook(ID),
MemberID TEXT NOT NULL,
UNIQUE (PlaybookID, MemberID)
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_PlaybookMember")
}
if _, err := e.Exec(createPGIndex("IR_Incident_TeamID", "IR_Incident", "TeamID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Incident_TeamID")
}
if _, err := e.Exec(createPGIndex("IR_Incident_TeamID_CommanderUserID", "IR_Incident", "TeamID, CommanderUserID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Incident_TeamID_CommanderUserID")
}
if _, err := e.Exec(createPGIndex("IR_Incident_ChannelID", "IR_Incident", "ChannelID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Incident_ChannelID")
}
if _, err := e.Exec(createPGIndex("IR_Playbook_TeamID", "IR_Playbook", "TeamID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Playbook_TeamID")
}
if _, err := e.Exec(createPGIndex("IR_PlaybookMember_PlaybookID", "IR_PlaybookMember", "PlaybookID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_PlaybookMember_PlaybookID")
}
if _, err := e.Exec(createPGIndex("IR_PlaybookMember_MemberID", "IR_PlaybookMember", "MemberID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_PlaybookMember_MemberID ")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.1.0"),
toVersion: semver.MustParse("0.2.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// prior to v1.0.0 of the plugin, this migration was used to trigger the data migration from the kvstore
return nil
},
},
{
fromVersion: semver.MustParse("0.2.0"),
toVersion: semver.MustParse("0.3.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ActiveStageTitle", "VARCHAR(1024) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ActiveStageTitle to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ActiveStageTitle", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ActiveStageTitle to table IR_Incident")
}
}
getPlaybookRunsQuery := sqlStore.builder.
Select("ID", "ActiveStage", "ChecklistsJSON").
From("IR_Incident")
var playbookRuns []struct {
ID string
ActiveStage int
ChecklistsJSON json.RawMessage
}
if err := sqlStore.selectBuilder(e, &playbookRuns, getPlaybookRunsQuery); err != nil {
return errors.Wrapf(err, "failed getting playbook runs to update their ActiveStageTitle")
}
for _, playbookRun := range playbookRuns {
var checklists []app.Checklist
if err := json.Unmarshal(playbookRun.ChecklistsJSON, &checklists); err != nil {
return errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: '%s'", playbookRun.ID)
}
numChecklists := len(checklists)
if numChecklists == 0 {
continue
}
if playbookRun.ActiveStage < 0 || playbookRun.ActiveStage >= numChecklists {
logrus.WithFields(logrus.Fields{
"active_stage": playbookRun.ActiveStage,
"playbook_run_id": playbookRun.ID,
"num_checklists": numChecklists,
}).Warn("index out of bounds: setting ActiveStageTitle to the empty string", playbookRun.ActiveStage, playbookRun.ID, numChecklists)
continue
}
playbookRunUpdate := sqlStore.builder.
Update("IR_Incident").
Set("ActiveStageTitle", checklists[playbookRun.ActiveStage].Title).
Where(sq.Eq{"ID": playbookRun.ID})
if _, err := sqlStore.execBuilder(e, playbookRunUpdate); err != nil {
return errors.Errorf("failed updating the ActiveStageTitle field of playbook run '%s'", playbookRun.ID)
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.3.0"),
toVersion: semver.MustParse("0.4.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_StatusPosts (
IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID),
PostID VARCHAR(26) NOT NULL,
CONSTRAINT posts_unique UNIQUE (IncidentID, PostID),
INDEX IR_StatusPosts_IncidentID (IncidentID),
INDEX IR_StatusPosts_PostID (PostID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_StatusPosts")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderPostID", "VARCHAR(26)"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderPostID to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "BroadcastChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "BroadcastChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Playbook")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_StatusPosts (
IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID),
PostID TEXT NOT NULL,
UNIQUE (IncidentID, PostID)
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_StatusPosts")
}
if _, err := e.Exec(createPGIndex("IR_StatusPosts_IncidentID", "IR_StatusPosts", "IncidentID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_StatusPosts_IncidentID")
}
if _, err := e.Exec(createPGIndex("IR_StatusPosts_PostID", "IR_StatusPosts", "PostID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_StatusPosts_PostID ")
}
if err := addColumnToPGTable(e, "IR_Incident", "ReminderPostID", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderPostID to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Incident", "BroadcastChannelID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "BroadcastChannelID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.4.0"),
toVersion: semver.MustParse("0.5.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "PreviousReminder", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column PreviousReminder to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ReminderMessageTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET ReminderMessageTemplate = '' WHERE ReminderMessageTemplate IS NULL"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderMessageTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Incident SET ReminderMessageTemplate = '' WHERE ReminderMessageTemplate IS NULL"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "PreviousReminder", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column PreviousReminder to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ReminderMessageTemplate", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "ReminderMessageTemplate", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.5.0"),
toVersion: semver.MustParse("0.6.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "CurrentStatus", "VARCHAR(1024) NOT NULL DEFAULT 'Active'"); err != nil {
return errors.Wrapf(err, "failed adding column CurrentStatus to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_StatusPosts", "Status", "VARCHAR(1024) NOT NULL DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column Status to table IR_StatusPosts")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "CurrentStatus", "TEXT NOT NULL DEFAULT 'Active'"); err != nil {
return errors.Wrapf(err, "failed adding column CurrentStatus to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_StatusPosts", "Status", "TEXT NOT NULL DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column Status to table IR_StatusPosts")
}
}
if _, err := e.Exec("UPDATE IR_Incident SET CurrentStatus = 'Resolved' WHERE EndAt != 0"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Incident")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.6.0"),
toVersion: semver.MustParse("0.7.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_TimelineEvent
(
ID VARCHAR(26) NOT NULL,
IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID),
CreateAt BIGINT NOT NULL,
DeleteAt BIGINT NOT NULL DEFAULT 0,
EventAt BIGINT NOT NULL,
EventType VARCHAR(32) NOT NULL DEFAULT '',
Summary VARCHAR(256) NOT NULL DEFAULT '',
Details VARCHAR(4096) NOT NULL DEFAULT '',
PostID VARCHAR(26) NOT NULL DEFAULT '',
SubjectUserID VARCHAR(26) NOT NULL DEFAULT '',
CreatorUserID VARCHAR(26) NOT NULL DEFAULT '',
INDEX IR_TimelineEvent_ID (ID),
INDEX IR_TimelineEvent_IncidentID (IncidentID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_TimelineEvent")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_TimelineEvent
(
ID TEXT NOT NULL,
IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID),
CreateAt BIGINT NOT NULL,
DeleteAt BIGINT NOT NULL DEFAULT 0,
EventAt BIGINT NOT NULL,
EventType TEXT NOT NULL DEFAULT '',
Summary TEXT NOT NULL DEFAULT '',
Details TEXT NOT NULL DEFAULT '',
PostID TEXT NOT NULL DEFAULT '',
SubjectUserID TEXT NOT NULL DEFAULT '',
CreatorUserID TEXT NOT NULL DEFAULT ''
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_TimelineEvent")
}
if _, err := e.Exec(createPGIndex("IR_TimelineEvent_ID", "IR_TimelineEvent", "ID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_TimelineEvent_ID")
}
if _, err := e.Exec(createPGIndex("IR_TimelineEvent_IncidentID", "IR_TimelineEvent", "IncidentID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_TimelineEvent_IncidentID")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.7.0"),
toVersion: semver.MustParse("0.8.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ReporterUserID", "varchar(26) NOT NULL DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ReporterUserID to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ReporterUserID", "TEXT NOT NULL DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ReporterUserID to table IR_Incident")
}
}
if _, err := e.Exec(`UPDATE IR_Incident SET ReporterUserID = CommanderUserID WHERE ReporterUserID = ''`); err != nil {
return errors.Wrapf(err, "Failed to migrate ReporterUserID")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.8.0"),
toVersion: semver.MustParse("0.9.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedInvitedUserIDs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET ConcatenatedInvitedUserIDs = '' WHERE ConcatenatedInvitedUserIDs IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedUserIDs of table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedInvitedUserIDs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedInvitedUserIDs = '' WHERE ConcatenatedInvitedUserIDs IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedUserIDs of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "InviteUsersEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column InviteUsersEnabled to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedInvitedUserIDs", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedInvitedUserIDs", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "InviteUsersEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column InviteUsersEnabled to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.9.0"),
toVersion: semver.MustParse("0.10.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "DefaultCommanderID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "DefaultCommanderID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "DefaultCommanderEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderEnabled to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "DefaultCommanderID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "DefaultCommanderID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "DefaultCommanderEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column DefaultCommanderEnabled to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.10.0"),
toVersion: semver.MustParse("0.11.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
UPDATE IR_Incident
INNER JOIN Channels ON IR_Incident.ChannelID = Channels.ID
SET IR_Incident.CreateAt = Channels.CreateAt,
IR_Incident.DeleteAt = Channels.DeleteAt
WHERE IR_Incident.CreateAt = 0
AND IR_Incident.DeleteAt = 0
AND IR_Incident.ChannelID = Channels.ID
`); err != nil {
return errors.Wrap(err, "failed updating table IR_Incident with Channels' CreateAt and DeleteAt values")
}
} else {
if _, err := e.Exec(`
UPDATE IR_Incident
SET CreateAt = Channels.CreateAt,
DeleteAt = Channels.DeleteAt
FROM Channels
WHERE IR_Incident.CreateAt = 0
AND IR_Incident.DeleteAt = 0
AND IR_Incident.ChannelID = Channels.ID
`); err != nil {
return errors.Wrap(err, "failed updating table IR_Incident with Channels' CreateAt and DeleteAt values")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.11.0"),
toVersion: semver.MustParse("0.12.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "AnnouncementChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "AnnouncementChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "AnnouncementChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelEnabled to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "AnnouncementChannelID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "AnnouncementChannelID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "AnnouncementChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column AnnouncementChannelEnabled to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.12.0"),
toVersion: semver.MustParse("0.13.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "WebhookOnCreationURL", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET WebhookOnCreationURL = '' WHERE WebhookOnCreationURL IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column WebhookOnCreationURL of table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnCreationURL", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET WebhookOnCreationURL = '' WHERE WebhookOnCreationURL IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column WebhookOnCreationURL of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnCreationEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationEnabled to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "WebhookOnCreationURL", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnCreationURL", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnCreationEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnCreationEnabled to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.13.0"),
toVersion: semver.MustParse("0.14.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedInvitedGroupIDs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET ConcatenatedInvitedGroupIDs = '' WHERE ConcatenatedInvitedGroupIDs IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedGroupIDs of table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedInvitedGroupIDs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedInvitedGroupIDs = '' WHERE ConcatenatedInvitedGroupIDs IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedGroupIDs of table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedInvitedGroupIDs", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedInvitedGroupIDs", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.14.0"),
toVersion: semver.MustParse("0.15.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "Retrospective", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column Retrospective to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET Retrospective = '' WHERE Retrospective IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column Retrospective of table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "Retrospective", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column Retrospective to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.15.0"),
toVersion: semver.MustParse("0.16.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "MessageOnJoin", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET MessageOnJoin = '' WHERE MessageOnJoin IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column MessageOnJoin of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "MessageOnJoinEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoinEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "MessageOnJoin", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET MessageOnJoin = '' WHERE MessageOnJoin IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column MessageOnJoin of table IR_Incident")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_ViewedChannel
(
ChannelID VARCHAR(26) NOT NULL,
UserID VARCHAR(26) NOT NULL,
UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID (ChannelID, UserID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_ViewedChannel")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "MessageOnJoin", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "MessageOnJoinEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoinEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "MessageOnJoin", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Incident")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_ViewedChannel
(
ChannelID TEXT NOT NULL,
UserID TEXT NOT NULL
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_ViewedChannel")
}
if _, err := e.Exec(createUniquePGIndex("IR_ViewedChannel_ChannelID_UserID", "IR_ViewedChannel", "ChannelID, UserID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_ViewedChannel_ChannelID_UserID")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.16.0"),
toVersion: semver.MustParse("0.17.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectivePublishedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectivePublishedAt to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "RetrospectivePublishedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectivePublishedAt to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.17.0"),
toVersion: semver.MustParse("0.18.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveWasCanceled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveWasCanceled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveWasCanceled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveWasCanceled to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.18.0"),
toVersion: semver.MustParse("0.19.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook")
}
}
if _, err := e.Exec("UPDATE IR_Playbook SET RetrospectiveTemplate = '' WHERE RetrospectiveTemplate IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column RetrospectiveTemplate of table IR_Playbook")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.19.0"),
toVersion: semver.MustParse("0.20.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET WebhookOnStatusUpdateURL = '' WHERE WebhookOnStatusUpdateURL IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column WebhookOnStatusUpdateURL of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnStatusUpdateEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "WebhookOnStatusUpdateURL", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET WebhookOnStatusUpdateURL = '' WHERE WebhookOnStatusUpdateURL IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column WebhookOnStatusUpdateURL of table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnStatusUpdateEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "WebhookOnStatusUpdateURL", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.20.0"),
toVersion: semver.MustParse("0.21.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedSignalAnyKeywords", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedSignalAnyKeywords to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedSignalAnyKeywords = '' WHERE ConcatenatedSignalAnyKeywords IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedSignalAnyKeywords of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "SignalAnyKeywordsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column SignalAnyKeywordsEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "UpdateAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column UpdateAt to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET UpdateAt = CreateAt"); err != nil {
return errors.Wrapf(err, "failed setting default value in column UpdateAt of table IR_Playbook")
}
if _, err := e.Exec(`ALTER TABLE IR_Playbook ADD INDEX IR_Playbook_UpdateAt (UpdateAt)`); err != nil {
me, ok := err.(*mysql.MySQLError)
if !ok || me.Number != 1061 { // not a Duplicate key name error
return errors.Wrapf(err, "failed creating index IR_Playbook_UpdateAt")
}
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedSignalAnyKeywords", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedSignalAnyKeywords to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "SignalAnyKeywordsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column SignalAnyKeywordsEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "UpdateAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column UpdateAt to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET UpdateAt = CreateAt"); err != nil {
return errors.Wrapf(err, "failed setting default value in column UpdateAt of table IR_Playbook")
}
if _, err := e.Exec(createPGIndex("IR_Playbook_UpdateAt", "IR_Playbook", "UpdateAt")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Playbook_UpdateAt")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.21.0"),
toVersion: semver.MustParse("0.22.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "LastStatusUpdateAt", "BIGINT DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column LastStatusUpdateAt to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "LastStatusUpdateAt", "BIGINT DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column LastStatusUpdateAt to table IR_Incident")
}
}
var lastUpdateAts []struct {
ID string
LastStatusUpdateAt int64
}
// Fill in the LastStatusUpdateAt column as either the most recent status post, or
// if no posts: the playbook run's CreateAt.
lastUpdateAtSelect := sqlStore.builder.
Select("i.Id as ID", "COALESCE(MAX(p.CreateAt), i.CreateAt) as LastStatusUpdateAt").
From("IR_Incident as i").
LeftJoin("IR_StatusPosts as sp on i.Id = sp.IncidentId").
LeftJoin("Posts as p on sp.PostId = p.Id").
GroupBy("i.Id")
if err := sqlStore.selectBuilder(e, &lastUpdateAts, lastUpdateAtSelect); err != nil {
return errors.Wrapf(err, "failed getting incidents to update their LastStatusUpdateAt")
}
for _, row := range lastUpdateAts {
incidentUpdate := sqlStore.builder.
Update("IR_Incident").
Set("LastStatusUpdateAt", row.LastStatusUpdateAt).
Where(sq.Eq{"ID": row.ID})
if _, err := sqlStore.execBuilder(e, incidentUpdate); err != nil {
return errors.Wrapf(err, "failed to update incident's LastStatusUpdateAt for id: %s", row.ID)
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.22.0"),
toVersion: semver.MustParse("0.23.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.23.0"),
toVersion: semver.MustParse("0.24.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.24.0"),
toVersion: semver.MustParse("0.25.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := renameColumnMySQL(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Playbook")
}
if err := renameColumnMySQL(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil {
return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Incident")
}
if err := dropColumnMySQL(e, "IR_StatusPosts", "Status"); err != nil {
return errors.Wrap(err, "failed dropping column Status in table IR_StatusPosts")
}
} else {
if err := renameColumnPG(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled"); err != nil {
return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Playbook")
}
if err := renameColumnPG(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled"); err != nil {
return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Incident")
}
if err := dropColumnPG(e, "IR_StatusPosts", "Status"); err != nil {
return errors.Wrap(err, "failed dropping column Status in table IR_StatusPosts")
}
}
if _, err := e.Exec(`
UPDATE IR_Incident
SET CurrentStatus =
CASE
WHEN CurrentStatus = 'Archived'
THEN 'Finished'
ELSE 'InProgress'
END;
`); err != nil {
return errors.Wrap(err, "failed changing CurrentStatus to Archived or InProgress in table IR_Incident")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.25.0"),
toVersion: semver.MustParse("0.26.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "CategoryName", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column CategoryName to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled=1"); err != nil {
return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "CategoryName", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column CategoryName to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled=1"); err != nil {
return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "CategoryName", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column CategoryName to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled"); err != nil {
return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "CategoryName", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column CategoryName to table IR_Incident")
}
if _, err := e.Exec("UPDATE IR_Incident SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled"); err != nil {
return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.26.0"),
toVersion: semver.MustParse("0.27.0"),
// This deprecates columns BroadcastChannelID (in singular), AnnouncementChannelID and AnnouncementChannelEnabled
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
updateIncidentTableQuery := `
UPDATE IR_Incident SET
ConcatenatedBroadcastChannelIds = (
COALESCE(
CONCAT_WS(
',',
CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END,
CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END
),
'')
)
`
updatePlaybookTableQuery := `
UPDATE IR_Playbook SET
ConcatenatedBroadcastChannelIds = (
COALESCE(
CONCAT_WS(
',',
CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END,
CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END
),
'')
)
, BroadcastEnabled = (CASE
WHEN BroadcastChannelID != '' THEN TRUE
WHEN AnnouncementChannelEnabled = TRUE THEN TRUE
ELSE FALSE
END)
`
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Incident")
}
if _, err := e.Exec(updateIncidentTableQuery); err != nil {
return errors.Wrapf(err, "failed setting value in column ConcatenatedBroadcastChannelIds of table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "BroadcastEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastEnabled to table IR_Playbook")
}
if _, err := e.Exec(updatePlaybookTableQuery); err != nil {
return errors.Wrapf(err, "failed setting value in columns ConcatenatedBroadcastChannelIds and BroadcastEnabled of table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Incident")
}
if _, err := e.Exec(updateIncidentTableQuery); err != nil {
return errors.Wrapf(err, "failed setting value in column ConcatenatedBroadcastChannelIds of table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "BroadcastEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column BroadcastEnabled to table IR_Playbook")
}
if _, err := e.Exec(updatePlaybookTableQuery); err != nil {
return errors.Wrapf(err, "failed setting value in columns ConcatenatedBroadcastChannelIds and BroadcastEnabled of table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.27.0"),
toVersion: semver.MustParse("0.28.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ChannelIDToRootID", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelIDToRootID to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ChannelIDToRootID", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelIDToRootID to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.28.0"),
toVersion: semver.MustParse("0.29.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`ALTER TABLE IR_System CONVERT TO CHARACTER SET utf8mb4`); err != nil {
return errors.Wrapf(err, "failed to migrate character set")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.29.0"),
toVersion: semver.MustParse("0.30.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addPrimaryKey(e, sqlStore, "IR_PlaybookMember", "(MemberID, PlaybookID)"); err != nil {
return err
}
if err := dropIndexIfExists(e, sqlStore, "IR_StatusPosts", "posts_unique"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "IR_StatusPosts", "(IncidentID, PostID)"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "IR_TimelineEvent", "(ID)"); err != nil {
return err
}
if err := dropIndexIfExists(e, sqlStore, "IR_ViewedChannel", "IR_ViewedChannel_ChannelID_UserID"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "IR_ViewedChannel", "(ChannelID, UserID)"); err != nil {
return err
}
} else {
if err := addPrimaryKey(e, sqlStore, "ir_playbookmember", "(MemberID, PlaybookID)"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "ir_statusposts", "(IncidentID, PostID)"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "ir_timelineevent", "(ID)"); err != nil {
return err
}
if err := addPrimaryKey(e, sqlStore, "ir_viewedchannel", "(ChannelID, UserID)"); err != nil {
return err
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.30.0"),
toVersion: semver.MustParse("0.31.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Best effort migration so we just log the error to avoid killing the plugin.
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec("UPDATE IGNORE PluginKeyValueStore SET PluginId='playbooks' WHERE PluginId='com.mattermost.plugin-incident-management'"); err != nil {
logrus.WithError(err).Error("failed to migrate KV store plugin id")
}
} else {
if _, err := e.Exec("UPDATE PluginKeyValueStore k SET PluginId='playbooks' WHERE PluginId='com.mattermost.plugin-incident-management' AND NOT EXISTS ( SELECT 1 FROM PluginKeyValueStore WHERE PluginId='playbooks' AND PKey = k.PKey )"); err != nil {
logrus.WithError(err).Error("failed to migrate KV store plugin id")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.31.0"),
toVersion: semver.MustParse("0.32.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.32.0"),
toVersion: semver.MustParse("0.33.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := renameColumnMySQL(e, "IR_Playbook", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Playbook")
}
if err := renameColumnMySQL(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Playbook")
}
if err := renameColumnMySQL(e, "IR_Incident", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Incident")
}
if err := renameColumnMySQL(e, "IR_Incident", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs", "TEXT"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Incident")
}
} else {
if err := renameColumnPG(e, "IR_Playbook", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Playbook")
}
if err := renameColumnPG(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Playbook")
}
if err := renameColumnPG(e, "IR_Incident", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Incident")
}
if err := renameColumnPG(e, "IR_Incident", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs"); err != nil {
return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.33.0"),
toVersion: semver.MustParse("0.34.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_UserInfo
(
ID VARCHAR(26) PRIMARY KEY,
LastDailyTodoDMAt BIGINT
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_UserInfo")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_UserInfo
(
ID TEXT PRIMARY KEY,
LastDailyTodoDMAt BIGINT
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_UserInfo")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.34.0"),
toVersion: semver.MustParse("0.35.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_UserInfo", "DigestNotificationSettingsJSON", "JSON"); err != nil {
return errors.Wrapf(err, "failed adding column DigestNotificationSettings to table IR_UserInfo")
}
} else {
if err := addColumnToPGTable(e, "IR_UserInfo", "DigestNotificationSettingsJSON", "JSON"); err != nil {
return errors.Wrapf(err, "failed adding column DigestNotificationSettings to table IR_UserInfo")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.35.0"),
toVersion: semver.MustParse("0.36.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if err := dropIndexIfExists(e, sqlStore, "IR_StatusPosts", "posts_unique"); err != nil {
return err
}
return dropIndexIfExists(e, sqlStore, "IR_ViewedChannel", "IR_ViewedChannel_ChannelID_UserID")
},
},
{
fromVersion: semver.MustParse("0.36.0"),
toVersion: semver.MustParse("0.37.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Existing runs without a reminder need to have a reminder set; use 1 week from now.
oneWeek := 7 * 24 * time.Hour
// Get overdue runs
overdueQuery := sqlStore.builder.
Select("ID").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusInProgress}).
Where(sq.NotEq{"PreviousReminder": 0})
if sqlStore.db.DriverName() == model.DatabaseDriverMysql {
overdueQuery = overdueQuery.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)"))
} else {
overdueQuery = overdueQuery.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)"))
}
var runIDs []string
if err := sqlStore.selectBuilder(sqlStore.db, &runIDs, overdueQuery); err != nil {
return errors.Wrap(err, "failed to query for overdue runs")
}
// Get runs that never had a status update set
otherQuery := sqlStore.builder.
Select("ID").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusInProgress}).
Where(sq.Eq{"PreviousReminder": 0})
var otherRunIDs []string
if err := sqlStore.selectBuilder(sqlStore.db, &otherRunIDs, otherQuery); err != nil {
return errors.Wrap(err, "failed to query for overdue runs")
}
// Set the new reminders
runIDs = append(runIDs, otherRunIDs...)
for _, ID := range runIDs {
// Just in case (so we don't crash out during the migration) remove any old reminders
sqlStore.scheduler.Cancel(ID)
if _, err := sqlStore.scheduler.ScheduleOnce(ID, time.Now().Add(oneWeek)); err != nil {
return errors.Wrapf(err, "failed to set new schedule for run id: %s", ID)
}
// Set the PreviousReminder, and pretend that this was a LastStatusUpdateAt so that
// the reminder timers will show the correct time for when a status update is due.
updatePrevReminderAndLastUpdateAt := sqlStore.builder.
Update("IR_Incident").
SetMap(map[string]interface{}{
"PreviousReminder": oneWeek,
"LastStatusUpdateAt": model.GetMillis(),
}).
Where(sq.Eq{"ID": ID})
if _, err := sqlStore.execBuilder(sqlStore.db, updatePrevReminderAndLastUpdateAt); err != nil {
return errors.Wrap(err, "failed to update new PreviousReminder and LastStatusUpdateAt")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.37.0"),
toVersion: semver.MustParse("0.38.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Run_Participants (
IncidentID VARCHAR(26) NULL REFERENCES IR_Incident(ID),
UserID VARCHAR(26) NOT NULL,
IsFollower BOOLEAN NOT NULL,
INDEX IR_Run_Participants_UserID (UserID),
INDEX IR_Run_Participants_IncidentID (IncidentID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Run_Participants")
}
if err := addPrimaryKey(e, sqlStore, "IR_Run_Participants", "(IncidentID, UserID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_Run_Participants")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Run_Participants (
UserID TEXT NOT NULL,
IncidentID TEXT NULL REFERENCES IR_Incident(ID),
IsFollower BOOLEAN NOT NULL
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Run_Participants")
}
if err := addPrimaryKey(e, sqlStore, "ir_run_participants", "(IncidentID, UserID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for ir_run_participants")
}
if _, err := e.Exec(createPGIndex("IR_Run_Participants_UserID", "IR_Run_Participants", "UserID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Run_Participants_UserID")
}
if _, err := e.Exec(createPGIndex("IR_Run_Participants_IncidentID", "IR_Run_Participants", "IncidentID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Run_Participants_IncidentID")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.38.0"),
toVersion: semver.MustParse("0.39.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "RunSummaryTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column RunSummaryTemplate to table IR_Playbook")
}
if _, err := e.Exec("UPDATE IR_Playbook SET RunSummaryTemplate = '' WHERE RunSummaryTemplate IS NULL"); err != nil {
return errors.Wrapf(err, "failed updating default value of column RunSummaryTemplate from table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "RunSummaryTemplate", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column RunSummaryTemplate to table IR_Playbook")
}
}
// Copy the values from the Description column, historically used for the run summary template, into the new RunSummaryTemplate column
if _, err := e.Exec("UPDATE IR_Playbook SET RunSummaryTemplate = Description, Description = '' WHERE Description <> ''"); err != nil {
return errors.Wrapf(err, "failed updating default value of column RunSummaryTemplate from table IR_Playbook")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.39.0"),
toVersion: semver.MustParse("0.40.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_PlaybookAutoFollow (
PlaybookID VARCHAR(26) NULL REFERENCES IR_Playbook(ID),
UserID VARCHAR(26) NOT NULL
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_PlaybookAutoFollow")
}
if err := addPrimaryKey(e, sqlStore, "IR_PlaybookAutoFollow", "(PlaybookID, UserID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_PlaybookAutoFollow")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_PlaybookAutoFollow (
PlaybookID TEXT NULL REFERENCES IR_Playbook(ID),
UserID TEXT NOT NULL
);
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_PlaybookAutoFollow")
}
if err := addPrimaryKey(e, sqlStore, "ir_playbookautofollow", "(PlaybookID, UserID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_PlaybookAutoFollow")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.40.0"),
toVersion: semver.MustParse("0.41.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelNameTemplate", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelNameTemplate to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "ChannelNameTemplate", "TEXT DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelNameTemplate to table IR_Playbook")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.41.0"),
toVersion: semver.MustParse("0.42.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.42.0"),
toVersion: semver.MustParse("0.43.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.43.0"),
toVersion: semver.MustParse("0.44.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_PlaybookMember", "Roles", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "Public", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_PlaybookMember", "Roles", "TEXT"); err != nil {
return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "Public", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook")
}
}
// Set all existing members to admins
if _, err := e.Exec("UPDATE IR_PlaybookMember SET Roles = 'playbook_member playbook_admin' WHERE Roles IS NULL"); err != nil {
return errors.Wrapf(err, "failed setting default value in column Roles of table IR_Playbook")
}
// Set all playbooks with no members as public
if _, err := e.Exec("UPDATE IR_Playbook p SET Public = true WHERE NOT EXISTS(SELECT 1 FROM IR_PlaybookMember as pm WHERE pm.PlaybookID = p.ID)"); err != nil {
return errors.Wrapf(err, "failed setting default value in column ConcatenatedSignalAnyKeywords of table IR_Playbook")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.44.0"),
toVersion: semver.MustParse("0.45.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Existing runs without a reminder need to have a reminder set; use 1 week from now.
oneWeek := 7 * 24 * time.Hour
// Get runs whose reminder was dismissed (PreviousReminder was set to 0), but only for those
// that have status updates enabled (or else they can't fix an overdue status update)
dimissedQuery := sqlStore.builder.
Select("ID").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusInProgress}).
Where(sq.Eq{"PreviousReminder": 0}).
Where(sq.Eq{"StatusUpdateEnabled": true})
var runIDs []string
if err := sqlStore.selectBuilder(sqlStore.db, &runIDs, dimissedQuery); err != nil {
return errors.Wrap(err, "failed to query for overdue runs")
}
// Set the new reminders
for _, ID := range runIDs {
// Just in case (so we don't crash out during the migration) remove any old reminders
sqlStore.scheduler.Cancel(ID)
if _, err := sqlStore.scheduler.ScheduleOnce(ID, time.Now().Add(oneWeek)); err != nil {
return errors.Wrapf(err, "failed to set new schedule for run id: %s", ID)
}
// Set the PreviousReminder, and pretend that this was a LastStatusUpdateAt so that
// the reminder timers will show the correct time for when a status update is due.
updatePrevReminderAndLastUpdateAt := sqlStore.builder.
Update("IR_Incident").
SetMap(map[string]interface{}{
"PreviousReminder": oneWeek,
"LastStatusUpdateAt": model.GetMillis(),
}).
Where(sq.Eq{"ID": ID})
if _, err := sqlStore.execBuilder(sqlStore.db, updatePrevReminderAndLastUpdateAt); err != nil {
return errors.Wrap(err, "failed to update new PreviousReminder and LastStatusUpdateAt")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.45.0"),
toVersion: semver.MustParse("0.46.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "RunSummaryTemplateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RunSummaryTemplateEnabled to table IR_Playbook")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "RunSummaryTemplateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RunSummaryTemplateEnabled to table IR_Playbook")
}
}
// All playbooks that have an empty run summary should have their run summary disabled (it defaults to enabled)
playbookUpdate := sqlStore.builder.
Update("IR_Playbook").
Set("RunSummaryTemplateEnabled", false).
Where(sq.Eq{"RunSummaryTemplate": ""})
if _, err := sqlStore.execBuilder(e, playbookUpdate); err != nil {
return errors.Wrap(err, "failed updating RunSummaryTemplateEnabled")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.46.0"),
toVersion: semver.MustParse("0.47.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// set CurrentStatus = Finished for runs with EndAt > 0 || IsActive == false
updateOldStatuses := sqlStore.builder.
Update("IR_Incident").
Set("CurrentStatus", app.StatusFinished).
Where(sq.Or{
sq.Gt{"EndAt": 0},
sq.Eq{"IsActive": false},
})
if _, err := sqlStore.execBuilder(sqlStore.db, updateOldStatuses); err != nil {
return errors.Wrap(err, "failed to update new CurrentStatus for old runs")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.47.0"),
toVersion: semver.MustParse("0.48.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_MetricConfig (
ID VARCHAR(26) PRIMARY KEY,
PlaybookID VARCHAR(26) NOT NULL REFERENCES IR_Playbook(ID),
Title VARCHAR(512) NOT NULL,
Description VARCHAR(4096) NOT NULL,
Type VARCHAR(32) NOT NULL,
Target BIGINT NOT NULL,
Ordering TINYINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0,
INDEX IR_MetricConfig_PlaybookID (PlaybookID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Metric (
IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID),
MetricConfigID VARCHAR(26) NOT NULL REFERENCES IR_MetricConfig(ID),
Value BIGINT NOT NULL,
Published BOOLEAN NOT NULL,
INDEX IR_Metric_IncidentID (IncidentID),
INDEX IR_Metric_MetricConfigID (MetricConfigID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Metric")
}
if err := addPrimaryKey(e, sqlStore, "IR_Metric", "(IncidentID, MetricConfigID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_Metric")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_MetricConfig (
ID TEXT PRIMARY KEY,
PlaybookID TEXT NOT NULL REFERENCES IR_Playbook(ID),
Title TEXT NOT NULL,
Description TEXT NOT NULL,
Type TEXT NOT NULL,
Target BIGINT NOT NULL,
Ordering SMALLINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
if _, err := e.Exec(createPGIndex("IR_MetricConfig_PlaybookID", "IR_MetricConfig", "PlaybookID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_MetricConfig_PlaybookID")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Metric (
IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID),
MetricConfigID TEXT NOT NULL REFERENCES IR_MetricConfig(ID),
Value BIGINT NOT NULL,
Published BOOLEAN NOT NULL
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Metric")
}
if err := addPrimaryKey(e, sqlStore, "ir_metric", "(IncidentID, MetricConfigID)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_Metric")
}
if _, err := e.Exec(createPGIndex("IR_Metric_IncidentID", "IR_Metric", "IncidentID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Metric_IncidentID")
}
if _, err := e.Exec(createPGIndex("IR_Metric_MetricConfigID", "IR_Metric", "MetricConfigID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Metric_MetricConfigID")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.48.0"),
toVersion: semver.MustParse("0.49.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`ALTER TABLE IR_MetricConfig MODIFY COLUMN Target BIGINT`); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
if _, err := e.Exec(`ALTER TABLE IR_Metric MODIFY COLUMN Value BIGINT`); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
} else {
if _, err := e.Exec(`ALTER TABLE IR_MetricConfig ALTER COLUMN Target DROP NOT NULL`); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
if _, err := e.Exec(`ALTER TABLE IR_Metric ALTER COLUMN Value DROP NOT NULL`); err != nil {
return errors.Wrapf(err, "failed creating table IR_MetricConfig")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.49.0"),
toVersion: semver.MustParse("0.50.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_ChannelAction (
ID VARCHAR(26) PRIMARY KEY,
ChannelID VARCHAR(26),
Enabled BOOLEAN DEFAULT FALSE,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ActionType TEXT NOT NULL,
TriggerType TEXT NOT NULL,
Payload JSON NOT NULL,
INDEX IR_ChannelAction_ChannelID (ChannelID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_ChannelAction")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_ChannelAction (
ID TEXT PRIMARY KEY,
ChannelID VARCHAR(26),
Enabled BOOLEAN DEFAULT FALSE,
DeleteAt BIGINT NOT NULL DEFAULT 0,
ActionType TEXT NOT NULL,
TriggerType TEXT NOT NULL,
Payload JSON NOT NULL
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_ChannelAction")
}
if _, err := e.Exec(createPGIndex("IR_ChannelAction_ChannelID", "IR_ChannelAction", "ChannelID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_ChannelAction_ChannelID")
}
}
// Retrieve the channel ID and welcome message of every run
selectQuery := sqlStore.builder.
Select("ChannelID", "MessageOnJoin").
From("IR_Incident").
Where(sq.And{
sq.NotEq{"MessageOnJoin": ""},
})
var rows []struct {
ChannelID string
MessageOnJoin string
}
if err := sqlStore.selectBuilder(e, &rows, selectQuery); err != nil {
return errors.Wrapf(err, "failed to retrieve the ChannelID and MessageOnJoin from IR_Incident")
}
// Create a new action for every row returned before
if len(rows) > 0 {
insertQuery := sqlStore.builder.
Insert("IR_ChannelAction").
Columns("ID", "ChannelID", "Enabled", "ActionType", "TriggerType", "Payload")
for _, row := range rows {
payload := struct {
Message string
}{row.MessageOnJoin}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return errors.Wrapf(err, "failed to marshal welcome message payload: %v", payload)
}
insertQuery = insertQuery.Values(model.NewId(), row.ChannelID, true, "send_welcome_message", "new_member_joins", payloadJSON)
}
if _, err := sqlStore.execBuilder(e, insertQuery); err != nil {
return errors.Wrapf(err, "failed to create the channel actions for the existing runs")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.50.0"),
toVersion: semver.MustParse("0.51.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Retrieve the channel ID and category name of every run
selectQuery := sqlStore.builder.
Select("ChannelID", "CategoryName").
From("IR_Incident").
Where(sq.NotEq{"CategoryName": ""})
var rows []struct {
ChannelID string
CategoryName string
}
if err := sqlStore.selectBuilder(e, &rows, selectQuery); err != nil {
return errors.Wrapf(err, "failed to retrieve the ChannelID and CategoryName from IR_Incident")
}
// Create a new action for every row returned before
if len(rows) > 0 {
insertQuery := sqlStore.builder.
Insert("IR_ChannelAction").
Columns("ID", "ChannelID", "Enabled", "ActionType", "TriggerType", "Payload")
for _, row := range rows {
payload := struct {
CategoryName string `json:"category_name"`
}{row.CategoryName}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return errors.Wrapf(err, "failed to marshal category name payload: %v", payload)
}
insertQuery = insertQuery.Values(model.NewId(), row.ChannelID, true, "categorize_channel", "new_member_joins", payloadJSON)
}
if _, err := sqlStore.execBuilder(e, insertQuery); err != nil {
return errors.Wrapf(err, "failed to create the channel actions for the existing runs")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.51.0"),
toVersion: semver.MustParse("0.52.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// moved migration code to the next version to remove an unnecessary column
return nil
},
},
{
fromVersion: semver.MustParse("0.52.0"),
toVersion: semver.MustParse("0.53.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateBroadcastChannelsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastChannelsEnabled to table IR_Incident")
}
if err := dropColumnMySQL(e, "IR_Incident", "StatusUpdateBroadcastFollowersEnabled"); err != nil {
return errors.Wrapf(err, "failed dropping column StatusUpdateBroadcastFollowersEnabled from table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateBroadcastWebhooksEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastWebhooksEnabled to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateBroadcastChannelsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastChannelsEnabled to table IR_Incident")
}
if err := dropColumnPG(e, "IR_Incident", "StatusUpdateBroadcastFollowersEnabled"); err != nil {
return errors.Wrapf(err, "failed dropping column StatusUpdateBroadcastFollowersEnabled from table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateBroadcastWebhooksEnabled", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastWebhooksEnabled to table IR_Incident")
}
}
// enable channels broadcast where channels ids list is not empty
channelsBroadcast := sqlStore.builder.
Update("IR_Incident").
Set("StatusUpdateBroadcastChannelsEnabled", true).
Where(sq.NotEq{"ConcatenatedBroadcastChannelIDs": ""})
if _, err := sqlStore.execBuilder(e, channelsBroadcast); err != nil {
return errors.Wrapf(err, "failed updating the StatusUpdateBroadcastChannelsEnabled column")
}
// enable webhooks broadcast where webhooks list is not empty
webhooksBroadcast := sqlStore.builder.
Update("IR_Incident").
Set("StatusUpdateBroadcastWebhooksEnabled", true).
Where(sq.NotEq{"ConcatenatedWebhookOnStatusUpdateURLs": ""})
if _, err := sqlStore.execBuilder(e, webhooksBroadcast); err != nil {
return errors.Wrapf(err, "failed updating the StatusUpdateBroadcastWebhooksEnabled column")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.53.0"),
toVersion: semver.MustParse("0.54.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "SummaryModifiedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "SummaryModifiedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil {
return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.54.0"),
toVersion: semver.MustParse("0.55.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Category (
ID VARCHAR(26) PRIMARY KEY,
Name VARCHAR(512) NOT NULL,
TeamID VARCHAR(26) NOT NULL,
UserID VARCHAR(26) NOT NULL,
Collapsed BOOLEAN DEFAULT FALSE,
CreateAt BIGINT NOT NULL,
UpdateAt BIGINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0,
INDEX IR_Category_TeamID_UserID (TeamID, UserID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Category")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Category_Item (
Type VARCHAR(1) NOT NULL,
CategoryID VARCHAR(26) NOT NULL REFERENCES IR_Category(ID),
ItemID VARCHAR(26) NOT NULL,
INDEX IR_Category_Item_CategoryID (CategoryID)
)
` + MySQLCharset); err != nil {
return errors.Wrapf(err, "failed creating table IR_Category_Item")
}
if err := addPrimaryKey(e, sqlStore, "IR_Category_Item", "(CategoryID, ItemID, Type)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_Category_Item")
}
} else {
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Category (
ID TEXT PRIMARY KEY,
Name TEXT NOT NULL,
TeamID TEXT NOT NULL,
UserID TEXT NOT NULL,
Collapsed BOOLEAN DEFAULT FALSE,
CreateAt BIGINT NOT NULL,
UpdateAt BIGINT NOT NULL DEFAULT 0,
DeleteAt BIGINT NOT NULL DEFAULT 0
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Category")
}
if _, err := e.Exec(createPGIndex("IR_Category_TeamID_UserID", "IR_Category", "TeamID, UserID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Category_TeamID_UserID")
}
if _, err := e.Exec(`
CREATE TABLE IF NOT EXISTS IR_Category_Item (
Type TEXT NOT NULL,
CategoryID TEXT NOT NULL REFERENCES IR_Category(ID),
ItemID TEXT NOT NULL
)
`); err != nil {
return errors.Wrapf(err, "failed creating table IR_Category_Item")
}
if _, err := e.Exec(createPGIndex("IR_Category_Item_CategoryID", "IR_Category_Item", "CategoryID")); err != nil {
return errors.Wrapf(err, "failed creating index IR_Category_Item_CategoryID")
}
if err := addPrimaryKey(e, sqlStore, "ir_category_item", "(CategoryID, ItemID, Type)"); err != nil {
return errors.Wrapf(err, "failed creating primary key for IR_Category_Item")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.55.0"),
toVersion: semver.MustParse("0.56.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Find all users who are members of channels where runs have been created.
// Add them as members of the playbook but only if it's a public playbook.
if _, err := e.Exec(`
INSERT INTO IR_PlaybookMember
SELECT DISTINCT
pb.ID as PlaybookID,
cm.UserID as MemberID,
'playbook_member' as Roles
FROM IR_Playbook as pb
JOIN IR_Incident as run on run.PlaybookID = pb.ID
JOIN ChannelMembers as cm on cm.ChannelID = run.ChannelID
LEFT JOIN IR_PlaybookMember as pm on pm.PlaybookID = pb.ID AND pm.MemberID = cm.UserID
LEFT JOIN Bots as b ON b.UserID = cm.UserID
WHERE
pb.Public = true AND
pb.DeleteAt = 0 AND
pm.PlaybookID IS NULL AND
b.UserId IS NULL
`); err != nil {
// Migration is optional so no failure just logging. (it will not try again)
logrus.WithError(err).Warn("failed to add existing users as playbook members")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.56.0"),
toVersion: semver.MustParse("0.57.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Run_Participants", "IsParticipant", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident")
}
if _, err := e.Exec(`ALTER TABLE IR_Run_Participants ALTER IsFollower SET DEFAULT FALSE`); err != nil {
return errors.Wrapf(err, "failed to set new column default for IsFollower")
}
} else {
if err := addColumnToPGTable(e, "IR_Run_Participants", "IsParticipant", "BOOLEAN DEFAULT FALSE"); err != nil {
return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident")
}
if _, err := e.Exec(`ALTER TABLE IR_Run_Participants ALTER COLUMN IsFollower SET DEFAULT FALSE`); err != nil {
return errors.Wrapf(err, "failed to set new column default for IsFollower")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.57.0"),
toVersion: semver.MustParse("0.58.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
// Find all users who are members of channels where runs have been created and are followers of the run.
// Update them to become members of the playbook run
var err error
if e.DriverName() == model.DatabaseDriverMysql {
_, err = e.Exec(`
UPDATE IR_Run_Participants
INNER JOIN IR_Incident ON IR_Run_Participants.IncidentID = IR_Incident.ID
INNER JOIN ChannelMembers ON ChannelMembers.ChannelID = IR_Incident.ChannelID
SET IR_Run_Participants.IsParticipant = true
WHERE
IR_Run_Participants.UserID = ChannelMembers.UserID
`)
} else {
_, err = e.Exec(`
UPDATE IR_Run_Participants
SET IsParticipant = true
FROM IR_Incident
INNER JOIN ChannelMembers ON ChannelMembers.ChannelID = IR_Incident.ChannelID
WHERE
IR_Run_Participants.UserID = ChannelMembers.UserID AND
IR_Run_Participants.IncidentID = IR_Incident.ID;
`)
}
if err != nil {
// Migration is optional so no failure just logging. (it will not try again)
logrus.WithError(err).Debug("failed to update existing users as playbook members")
}
// Find all users who are members of channels where runs have been created.
// Add them as members of the playbook run
if _, err := e.Exec(`
INSERT INTO IR_Run_Participants (UserID, IncidentID, IsFollower, IsParticipant)
SELECT DISTINCT
cm.UserID as UserID,
run.ID as IncidentID,
false as IsFollower,
true as IsParticipant
FROM IR_Incident as run
JOIN ChannelMembers as cm on cm.ChannelID = run.ChannelID
LEFT JOIN IR_Run_Participants as rp on rp.IncidentID = run.ID AND rp.UserID = cm.UserID
WHERE
rp.IncidentID IS NULL
`); err != nil {
// Migration is optional so no failure just logging. (it will not try again)
logrus.WithError(err).Debug("failed to add existing users as playbook members")
}
return nil
},
},
{
fromVersion: semver.MustParse("0.58.0"),
toVersion: semver.MustParse("0.59.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
type ColTypeChange struct {
ColName string
Size uint32
}
// Migrations are only for postgres
if e.DriverName() == model.DatabaseDriverMysql {
return nil
}
errCollected := []string{}
changes := map[string][]ColTypeChange{
"ir_incident": {
{"id", 26},
{"name", 1024},
{"description", 4096},
{"commanderuserid", 26},
{"teamid", 26},
{"channelid", 26},
{"postid", 26},
{"playbookid", 26},
{"activestagetitle", 1024},
{"reminderpostid", 26},
{"broadcastchannelid", 26},
{"remindermessagetemplate", 65535},
{"currentstatus", 1024},
{"reporteruserid", 26},
{"concatenatedinviteduserids", 65535},
{"defaultcommanderid", 26},
{"announcementchannelid", 26},
{"concatenatedwebhookoncreationurls", 65535},
{"concatenatedwebhookonstatusupdateurls", 65535},
{"concatenatedinvitedgroupids", 65535},
{"retrospective", 65535},
{"messageonjoin", 65535},
{"categoryname", 65535},
{"concatenatedbroadcastchannelids", 65535},
{"channelidtorootid", 65535},
},
"ir_playbook": {
{"id", 26},
{"title", 1024},
{"description", 4096},
{"teamid", 26},
{"broadcastchannelid", 26},
{"remindermessagetemplate", 65535},
{"concatenatedinviteduserids", 65535},
{"defaultcommanderid", 26},
{"announcementchannelid", 26},
{"concatenatedwebhookoncreationurls", 65535},
{"concatenatedinvitedgroupids", 65535},
{"messageonjoin", 65535},
{"retrospectivetemplate", 65535},
{"concatenatedwebhookonstatusupdateurls", 65535},
{"concatenatedsignalanykeywords", 65535},
{"categoryname", 65535},
{"concatenatedbroadcastchannelids", 65535},
{"runsummarytemplate", 65535},
{"channelnametemplate", 65535},
},
"ir_statusposts": {
{"incidentid", 26},
{"postid", 26},
},
"ir_category": {
{"id", 26},
{"name", 512},
{"teamid", 26},
{"userid", 26},
},
"ir_category_item": {
{"type", 1},
{"categoryid", 26},
{"itemid", 26},
},
"ir_channelaction": {
{"id", 26},
{"actiontype", 65535},
{"triggertype", 65535},
},
"ir_metric": {
{"incidentid", 26},
{"metricconfigid", 26},
},
"ir_metricconfig": {
{"id", 26},
{"playbookid", 26},
{"title", 512},
{"description", 4096},
{"type", 32},
},
"ir_playbookautofollow": {
{"playbookid", 26},
{"userid", 26},
},
"ir_playbookmember": {
{"playbookid", 26},
{"memberid", 26},
{"roles", 65535},
},
"ir_run_participants": {
{"userid", 26},
{"incidentid", 26},
},
"ir_viewedchannel": {
{"userid", 26},
{"channelid", 26},
},
"ir_timelineevent": {
{"id", 26},
{"incidentid", 26},
{"eventtype", 32},
{"summary", 256},
{"details", 4096},
{"postid", 26},
{"subjectuserid", 26},
{"creatoruserid", 26},
},
"ir_userinfo": {
{"id", 26},
},
}
for table, cols := range changes {
for _, col := range cols {
err := changeColumnTypeToPGTable(e, table, col.ColName, fmt.Sprintf("varchar(%d)", col.Size))
if err != nil {
errCollected = append(errCollected, err.Error())
}
}
}
if len(errCollected) > 0 {
return errors.New(strings.Join(errCollected, ",\n "))
}
return nil
},
},
{
fromVersion: semver.MustParse("0.59.0"),
toVersion: semver.MustParse("0.60.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Incident")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Incident", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Incident")
}
if err := addColumnToPGTable(e, "IR_Playbook", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Incident", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil {
return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Incident")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.60.0"),
toVersion: semver.MustParse("0.61.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelID to table IR_Playbook")
}
if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelMode", "VARCHAR(32) DEFAULT 'create_new_channel'"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelMode to table IR_Incident")
}
// We drop entirely the unique index for MySQL, there's an additional index on ChannelID that is kept
if err := dropIndexIfExists(e, sqlStore, "IR_Incident", "ChannelID"); err != nil {
return errors.Wrapf(err, "failed to drop ir_incident_channelid_key index on table ir_incident")
}
if _, err := e.Exec("UPDATE IR_Incident i JOIN Channels c ON c.id=i.ChannelID AND i.Name='' SET i.name=c.DisplayName"); err != nil {
return errors.Wrapf(err, "failed to update all old run names from channel names")
}
} else {
if err := addColumnToPGTable(e, "IR_Playbook", "ChannelID", "VARCHAR(26) DEFAULT ''"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelID to table IR_Playbook")
}
if err := addColumnToPGTable(e, "IR_Playbook", "ChannelMode", "VARCHAR(32) DEFAULT 'create_new_channel'"); err != nil {
return errors.Wrapf(err, "failed adding column ChannelMode to table IR_Incident")
}
// Unique constraint is dropped but index is kept
if _, err := e.Exec("ALTER TABLE IR_Incident DROP CONSTRAINT IF EXISTS ir_incident_channelid_key"); err != nil {
return errors.Wrapf(err, "failed to drop constraint ir_incident_channelid_key on table ir_incident")
}
if _, err := e.Exec("UPDATE IR_Incident i SET name=c.DisplayName FROM Channels c WHERE c.id=i.ChannelID AND i.Name=''"); err != nil {
return errors.Wrapf(err, "failed to update all old run names from channel names")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.61.0"),
toVersion: semver.MustParse("0.62.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if _, err := e.Exec(`
UPDATE IR_UserInfo
SET DigestNotificationSettingsJSON =
JSON_SET(DigestNotificationSettingsJSON, '$.disable_weekly_digest',
JSON_EXTRACT(DigestNotificationSettingsJSON, '$.disable_daily_digest'));
`); err != nil {
return errors.Wrapf(err, "failed adding disable_weekly_digest field to IR_UserInfo DigestNotificationSettingsJSON")
}
} else {
if _, err := e.Exec(`
UPDATE IR_UserInfo
SET DigestNotificationSettingsJSON = (DigestNotificationSettingsJSON::jsonb ||
jsonb_build_object('disable_weekly_digest', (DigestNotificationSettingsJSON::jsonb->>'disable_daily_digest')::boolean))::json;
`); err != nil {
return errors.Wrapf(err, "failed adding disable_weekly_digest field to IR_UserInfo DigestNotificationSettingsJSON")
}
}
return nil
},
},
{
fromVersion: semver.MustParse("0.62.0"),
toVersion: semver.MustParse("0.63.0"),
migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error {
if e.DriverName() == model.DatabaseDriverMysql {
if err := addColumnToMySQLTable(e, "IR_Incident", "RunType", "VARCHAR(32) DEFAULT 'playbook'"); err != nil {
return errors.Wrapf(err, "failed adding column RunType to table IR_Incident")
}
} else {
if err := addColumnToPGTable(e, "IR_Incident", "RunType", "VARCHAR(32) DEFAULT 'playbook'"); err != nil {
return errors.Wrapf(err, "failed adding column RunType to table IR_Incident")
}
}
return nil
},
},
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import sq "github.com/Masterminds/squirrel"
func InsertRun(sqlStore *SQLStore, run map[string]interface{}) error {
_, err := sqlStore.execBuilder(sqlStore.db, sq.
Insert("IR_Incident").
SetMap(run))
return err
}
func InsertPlaybook(sqlStore *SQLStore, playbook map[string]interface{}) error {
_, err := sqlStore.execBuilder(sqlStore.db, sq.
Insert("IR_Playbook").
SetMap(playbook))
return err
}
func InsertPost(sqlStore *SQLStore, id string, createdAt int64) error {
_, err := sqlStore.execBuilder(sqlStore.db, sq.
Insert("Posts").
SetMap(map[string]interface{}{
"Id": id,
"CreateAt": createdAt,
}))
return err
}
func InsertStatusPost(sqlStore *SQLStore, incidentID, postID string) error {
_, err := sqlStore.execBuilder(sqlStore.db, sq.
Insert("IR_StatusPosts").
SetMap(map[string]interface{}{
"IncidentID": incidentID,
"PostID": postID,
}))
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
"github.com/jmoiron/sqlx"
)
// 'IF NOT EXISTS' syntax is not supported in Postgres 9.4, so we need
// this workaround to make the migration idempotent
var createPGIndex = func(indexName, tableName, columns string) string {
return fmt.Sprintf(`
DO
$$
BEGIN
IF to_regclass('%s') IS NULL THEN
CREATE INDEX %s ON %s (%s);
END IF;
END
$$;
`, indexName, indexName, tableName, columns)
}
// 'IF NOT EXISTS' syntax is not supported in Postgres 9.4, so we need
// this workaround to make the migration idempotent
var createUniquePGIndex = func(indexName, tableName, columns string) string {
return fmt.Sprintf(`
DO
$$
BEGIN
IF to_regclass('%s') IS NULL THEN
CREATE UNIQUE INDEX %s ON %s (%s);
END IF;
END
$$;
`, indexName, indexName, tableName, columns)
}
var addColumnToPGTable = func(e sqlx.Ext, tableName, columnName, columnType string) error {
_, err := e.Exec(fmt.Sprintf(`
DO
$$
BEGIN
ALTER TABLE %s ADD %s %s;
EXCEPTION
WHEN duplicate_column THEN
RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" already exists in table "%s".';
END
$$;
`, tableName, columnName, columnType, columnName, tableName))
return err
}
var changeColumnTypeToPGTable = func(e sqlx.Ext, tableName, columnName, columnType string) error {
_, err := e.Exec(fmt.Sprintf(`
DO
$$
BEGIN
ALTER TABLE %s ALTER COLUMN %s TYPE %s;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" can not be changed to type %s in table "%s".';
END
$$;
`, tableName, columnName, columnType, columnName, columnType, tableName))
return err
}
var addColumnToMySQLTable = func(e sqlx.Ext, tableName, columnName, columnType string) error {
var result int
err := e.QueryRowx(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
tableName,
columnName,
).Scan(&result)
// Only alter the table if we don't find the column
if err == sql.ErrNoRows {
_, err = e.Exec(fmt.Sprintf("ALTER TABLE %s ADD %s %s", tableName, columnName, columnType))
}
return err
}
var renameColumnMySQL = func(e sqlx.Ext, tableName, oldColName, newColName, colDatatype string) error {
var result int
err := e.QueryRowx(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
tableName,
newColName,
).Scan(&result)
// Only alter the table if we don't find the column
if err == sql.ErrNoRows {
_, err = e.Exec(fmt.Sprintf("ALTER TABLE %s CHANGE %s %s %s", tableName, oldColName, newColName, colDatatype))
}
return err
}
var renameColumnPG = func(e sqlx.Ext, tableName, oldColName, newColName string) error {
_, err := e.Exec(fmt.Sprintf(`
DO
$$
BEGIN
ALTER TABLE %s RENAME COLUMN %s TO %s;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" does not exist in table "%s".';
END
$$;
`, tableName, oldColName, newColName, oldColName, tableName))
return err
}
var dropColumnMySQL = func(e sqlx.Ext, tableName, colName string) error {
var result int
err := e.QueryRowx(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
tableName,
colName,
).Scan(&result)
if err == sql.ErrNoRows {
return nil
}
// Only alter the table if we find the column
if err == nil && result == 1 {
_, err = e.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN %s", tableName, colName))
}
return err
}
var dropColumnPG = func(e sqlx.Ext, tableName, colName string) error {
_, err := e.Exec(fmt.Sprintf(`
DO
$$
BEGIN
ALTER TABLE %s DROP COLUMN %s;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" does not exist in table "%s".';
END
$$;
`, tableName, colName, colName, tableName))
return err
}
func addPrimaryKey(e sqlx.Ext, sqlStore *SQLStore, tableName, primaryKey string) error {
hasPK := 0
dbSelectionLine := "AND tco.table_schema = (SELECT DATABASE())"
if e.DriverName() == model.DatabaseDriverPostgres {
dbSelectionLine = "AND tco.table_catalog = (SELECT current_database())"
}
if err := sqlStore.db.Get(&hasPK, fmt.Sprintf(`
SELECT 1 FROM information_schema.table_constraints tco
WHERE tco.table_name = '%s'
%s
AND tco.constraint_type = 'PRIMARY KEY'
`, tableName, dbSelectionLine)); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "unable to determine if a primary key exists")
}
if hasPK == 0 {
if _, err := e.Exec(fmt.Sprintf(`
ALTER TABLE %s ADD PRIMARY KEY %s
`, tableName, primaryKey)); err != nil {
return errors.Wrap(err, "unable to add a primary key")
}
}
return nil
}
func dropIndexIfExists(e sqlx.Ext, sqlStore *SQLStore, tableName, indexName string) error {
hasIndex := 0
if e.DriverName() == model.DatabaseDriverMysql {
if err := sqlStore.db.Get(&hasIndex, fmt.Sprintf(`
SELECT 1 FROM information_schema.statistics s
WHERE s.table_name = '%s'
AND s.index_schema = (SELECT DATABASE())
AND index_name = '%s'
`, tableName, indexName)); err != nil && err != sql.ErrNoRows {
return errors.Wrapf(err, "unable to determine if index %s on table %s exists", indexName, tableName)
}
if hasIndex == 1 {
if _, err := e.Exec(fmt.Sprintf("DROP INDEX %s ON %s", indexName, tableName)); err != nil {
return errors.Wrapf(err, "failed to drop index %s on table %s", indexName, tableName)
}
}
} else if e.DriverName() == model.DatabaseDriverPostgres {
if _, err := e.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)); err != nil {
return errors.Wrapf(err, "failed to drop index %s on table %s", indexName, tableName)
}
}
return nil
}
func columnExists(sqlStore *SQLStore, tableName, columnName string) (bool, error) {
results := []string{}
var err error
if sqlStore.db.DriverName() == model.DatabaseDriverMysql {
err = sqlStore.db.Select(&results, `
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
`, tableName, columnName)
} else if sqlStore.db.DriverName() == model.DatabaseDriverPostgres {
err = sqlStore.db.Select(&results, `
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = $1
AND COLUMN_NAME = $2
`, strings.ToLower(tableName), strings.ToLower(columnName))
}
return len(results) > 0, err
}
type TableInfo struct {
TableName string
ColumnName string
DataType string
IsNullable string
ColumnKey string
ColumnDefault *string
Extra string
CharacterMaximumLength *string
}
// getDBSchemaInfo returns info for each table created by Playbook plugin
func getDBSchemaInfo(store *SQLStore) ([]TableInfo, error) {
var results []TableInfo
var err error
if store.db.DriverName() == model.DatabaseDriverMysql {
err = store.db.Select(&results, `
SELECT
TABLE_NAME as TableName, COLUMN_NAME as ColumnName, DATA_TYPE as DataType,
IS_NULLABLE as IsNullable, COLUMN_KEY as ColumnKey, COLUMN_DEFAULT as ColumnDefault,
EXTRA as Extra, CHARACTER_MAXIMUM_LENGTH as CharacterMaximumLength
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME LIKE 'IR_%'
AND TABLE_NAME != 'IR_db_migrations'
ORDER BY TABLE_NAME ASC, ORDINAL_POSITION ASC
`)
} else if store.db.DriverName() == model.DatabaseDriverPostgres {
err = store.db.Select(&results, `
SELECT
TABLE_NAME as TableName, COLUMN_NAME as ColumnName, DATA_TYPE as DataType,
IS_NULLABLE as IsNullable, COLUMN_DEFAULT as ColumnDefault, CHARACTER_MAXIMUM_LENGTH as CharacterMaximumLength
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = 'public'
AND TABLE_NAME LIKE 'ir_%'
AND TABLE_NAME != 'ir_db_migrations'
ORDER BY TABLE_NAME ASC, ORDINAL_POSITION ASC
`)
}
return results, err
}
type IndexInfo struct {
TableName string
IndexName string
// Postgres specific field
IndexDef string
// MySQL specific fields
ColumnName string
}
// getDBIndexesInfo returns index info for each table created by Playbook plugin
func getDBIndexesInfo(store *SQLStore) ([]IndexInfo, error) {
var results []IndexInfo
var err error
if store.db.DriverName() == model.DatabaseDriverMysql {
err = store.db.Select(&results, `
SELECT TABLE_NAME as TableName, INDEX_NAME as IndexName, COLUMN_NAME as ColumnName
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME LIKE 'ir_%'
AND TABLE_NAME != 'ir_db_migrations'
ORDER BY TABLE_NAME ASC, COLUMN_NAME ASC, INDEX_NAME ASC;
`)
} else if store.db.DriverName() == model.DatabaseDriverPostgres {
err = store.db.Select(&results, `
SELECT TABLENAME as TableName, INDEXNAME as IndexName, INDEXDEF as IndexDef
FROM pg_indexes
WHERE SCHEMANAME = 'public'
AND TABLENAME LIKE 'ir_%'
AND TABLENAME != 'ir_db_migrations'
ORDER BY TABLENAME ASC, INDEXNAME ASC;
`)
}
return results, err
}
type ConstraintsInfo struct {
ConstraintName string
TableName string
ConstraintType string
}
// getDBIndexesInfo returns index info for each table created by Playbook plugin
func getDBConstraintsInfo(store *SQLStore) ([]ConstraintsInfo, error) {
var results []ConstraintsInfo
var err error
if store.db.DriverName() == model.DatabaseDriverMysql {
err = store.db.Select(&results, `
SELECT CONSTRAINT_NAME as ConstraintName, TABLE_NAME as TableName, CONSTRAINT_TYPE as ConstraintType
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_NAME LIKE 'ir_%'
AND TABLE_NAME != 'ir_db_migrations'
AND TABLE_SCHEMA = (SELECT DATABASE())
ORDER BY CONSTRAINT_NAME ASC, TABLE_NAME ASC;
`)
} else if store.db.DriverName() == model.DatabaseDriverPostgres {
err = store.db.Select(&results, `
SELECT conname as ConstraintName, contype as ConstraintType
FROM pg_constraint
WHERE conname LIKE 'ir_%'
AND conname NOT LIKE 'ir_db_migrations%'
ORDER BY conname ASC, contype ASC;
`)
}
return results, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
type sqlPlaybook struct {
app.Playbook
ChecklistsJSON json.RawMessage
ConcatenatedInvitedUserIDs string
ConcatenatedInvitedGroupIDs string
ConcatenatedSignalAnyKeywords string
ConcatenatedBroadcastChannelIDs string
ConcatenatedWebhookOnCreationURLs string
ConcatenatedWebhookOnStatusUpdateURLs string
}
// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it.
type playbookStore struct {
pluginAPI PluginAPIClient
store *SQLStore
queryBuilder sq.StatementBuilderType
playbookSelect sq.SelectBuilder
membersSelect sq.SelectBuilder
metricsSelect sq.SelectBuilder
}
// Ensure playbookStore implements the playbook.Store interface.
var _ app.PlaybookStore = (*playbookStore)(nil)
type playbookMember struct {
PlaybookID string
MemberID string
Roles string
}
// definied to call a common insights query builder for both user and team insights
const insightsQueryTypeUser = "insights_query_type_user"
const insightsQueryTypeTeam = "insights_query_type_team"
func applyPlaybookFilterOptionsSort(builder sq.SelectBuilder, options app.PlaybookFilterOptions) (sq.SelectBuilder, error) {
var sort string
switch options.Sort {
case app.SortByID:
sort = "ID"
case app.SortByTitle:
sort = "Title"
case app.SortByStages:
sort = "NumStages"
case app.SortBySteps:
sort = "NumSteps"
case app.SortByRuns:
sort = "NumRuns"
case app.SortByCreateAt:
sort = "CreateAt"
case app.SortByLastRunAt:
sort = "LastRunAt"
case app.SortByActiveRuns:
sort = "ActiveRuns"
case "":
// Default to a stable sort if none explicitly provided.
sort = "ID"
default:
return sq.SelectBuilder{}, errors.Errorf("unsupported sort parameter '%s'", options.Sort)
}
var direction string
switch options.Direction {
case app.DirectionAsc:
direction = "ASC"
case app.DirectionDesc:
direction = "DESC"
case "":
// Default to an ascending sort if none explicitly provided.
direction = "ASC"
default:
return sq.SelectBuilder{}, errors.Errorf("unsupported direction parameter '%s'", options.Direction)
}
builder = builder.OrderByClause(fmt.Sprintf("%s %s", sort, direction))
page := options.Page
perPage := options.PerPage
if page < 0 {
page = 0
}
if perPage < 0 {
perPage = 0
}
builder = builder.
Offset(uint64(page * perPage)).
Limit(uint64(perPage))
return builder, nil
}
// NewPlaybookStore creates a new store for playbook service.
func NewPlaybookStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.PlaybookStore {
playbookSelect := sqlStore.builder.
Select(
"p.ID",
"p.Title",
"p.Description",
"p.Public",
"p.TeamID",
"p.CreatePublicIncident AS CreatePublicPlaybookRun",
"p.CreateAt",
"p.UpdateAt",
"p.DeleteAt",
"p.NumStages",
"p.NumSteps",
`(
1 + -- Channel creation is hard-coded
CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END +
CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END +
CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END +
CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END +
CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END +
CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END
) AS NumActions`,
"COALESCE(p.ReminderMessageTemplate, '') ReminderMessageTemplate",
"p.ReminderTimerDefaultSeconds",
"p.StatusUpdateEnabled",
"p.ConcatenatedInvitedUserIDs",
"p.ConcatenatedInvitedGroupIDs",
"p.InviteUsersEnabled",
"p.DefaultCommanderID AS DefaultOwnerID",
"p.DefaultCommanderEnabled AS DefaultOwnerEnabled",
"p.ConcatenatedBroadcastChannelIDs",
"p.BroadcastEnabled",
"p.ConcatenatedWebhookOnCreationURLs",
"p.WebhookOnCreationEnabled",
"p.MessageOnJoin",
"p.MessageOnJoinEnabled",
"p.RetrospectiveReminderIntervalSeconds",
"p.RetrospectiveTemplate",
"p.RetrospectiveEnabled",
"p.ConcatenatedWebhookOnStatusUpdateURLs",
"p.WebhookOnStatusUpdateEnabled",
"p.ConcatenatedSignalAnyKeywords",
"p.SignalAnyKeywordsEnabled",
"p.CategorizeChannelEnabled",
"p.CreateChannelMemberOnNewParticipant",
"p.RemoveChannelMemberOnRemovedParticipant",
"p.ChannelID",
"p.ChannelMode",
"p.ChecklistsJSON",
"COALESCE(p.CategoryName, '') CategoryName",
"p.RunSummaryTemplateEnabled",
"COALESCE(p.RunSummaryTemplate, '') RunSummaryTemplate",
"COALESCE(p.ChannelNameTemplate, '') ChannelNameTemplate",
"COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole",
"COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole",
"COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole",
"COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole",
).
From("IR_Playbook p").
LeftJoin("Teams t ON t.Id = p.TeamID").
LeftJoin("Schemes s ON t.SchemeId = s.Id")
membersSelect := sqlStore.builder.
Select(
"PlaybookID",
"MemberID",
"Roles",
).
From("IR_PlaybookMember").
OrderBy("MemberID ASC") // Entirely for consistency for the tests
metricsSelect := sqlStore.builder.
Select(
"ID",
"PlaybookID",
"Title",
"Description",
"Type",
"Target",
).
From("IR_MetricConfig").
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("Ordering ASC")
newStore := &playbookStore{
pluginAPI: pluginAPI,
store: sqlStore,
queryBuilder: sqlStore.builder,
playbookSelect: playbookSelect,
membersSelect: membersSelect,
metricsSelect: metricsSelect,
}
return newStore
}
// Create creates a new playbook
func (p *playbookStore) Create(playbook app.Playbook) (id string, err error) {
if playbook.ID != "" {
return "", errors.New("ID should be empty")
}
playbook.ID = model.NewId()
rawPlaybook, err := toSQLPlaybook(playbook)
if err != nil {
return "", err
}
tx, err := p.store.db.Beginx()
if err != nil {
return "", errors.Wrap(err, "could not begin transaction")
}
defer p.store.finalizeTransaction(tx)
_, err = p.store.execBuilder(tx, sq.
Insert("IR_Playbook").
SetMap(map[string]interface{}{
"ID": rawPlaybook.ID,
"Title": rawPlaybook.Title,
"Description": rawPlaybook.Description,
"TeamID": rawPlaybook.TeamID,
"Public": rawPlaybook.Public,
"CreatePublicIncident": rawPlaybook.CreatePublicPlaybookRun,
"CreateAt": rawPlaybook.CreateAt,
"UpdateAt": rawPlaybook.UpdateAt,
"DeleteAt": rawPlaybook.DeleteAt,
"ChecklistsJSON": rawPlaybook.ChecklistsJSON,
"NumStages": len(rawPlaybook.Checklists),
"NumSteps": getSteps(rawPlaybook.Playbook),
"ReminderMessageTemplate": rawPlaybook.ReminderMessageTemplate,
"ReminderTimerDefaultSeconds": rawPlaybook.ReminderTimerDefaultSeconds,
"StatusUpdateEnabled": rawPlaybook.StatusUpdateEnabled,
"ConcatenatedInvitedUserIDs": rawPlaybook.ConcatenatedInvitedUserIDs,
"ConcatenatedInvitedGroupIDs": rawPlaybook.ConcatenatedInvitedGroupIDs,
"InviteUsersEnabled": rawPlaybook.InviteUsersEnabled,
"DefaultCommanderID": rawPlaybook.DefaultOwnerID,
"DefaultCommanderEnabled": rawPlaybook.DefaultOwnerEnabled,
"ConcatenatedBroadcastChannelIDs": rawPlaybook.ConcatenatedBroadcastChannelIDs,
"BroadcastEnabled": rawPlaybook.BroadcastEnabled, //nolint
"ConcatenatedWebhookOnCreationURLs": rawPlaybook.ConcatenatedWebhookOnCreationURLs,
"WebhookOnCreationEnabled": rawPlaybook.WebhookOnCreationEnabled,
"MessageOnJoin": rawPlaybook.MessageOnJoin,
"MessageOnJoinEnabled": rawPlaybook.MessageOnJoinEnabled,
"RetrospectiveReminderIntervalSeconds": rawPlaybook.RetrospectiveReminderIntervalSeconds,
"RetrospectiveTemplate": rawPlaybook.RetrospectiveTemplate,
"RetrospectiveEnabled": rawPlaybook.RetrospectiveEnabled,
"ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs,
"WebhookOnStatusUpdateEnabled": rawPlaybook.WebhookOnStatusUpdateEnabled,
"ConcatenatedSignalAnyKeywords": rawPlaybook.ConcatenatedSignalAnyKeywords,
"SignalAnyKeywordsEnabled": rawPlaybook.SignalAnyKeywordsEnabled,
"CategorizeChannelEnabled": rawPlaybook.CategorizeChannelEnabled,
"CategoryName": rawPlaybook.CategoryName,
"RunSummaryTemplateEnabled": rawPlaybook.RunSummaryTemplateEnabled,
"RunSummaryTemplate": rawPlaybook.RunSummaryTemplate,
"ChannelNameTemplate": rawPlaybook.ChannelNameTemplate,
"CreateChannelMemberOnNewParticipant": rawPlaybook.CreateChannelMemberOnNewParticipant,
"RemoveChannelMemberOnRemovedParticipant": rawPlaybook.RemoveChannelMemberOnRemovedParticipant,
"ChannelID": rawPlaybook.ChannelID,
"ChannelMode": rawPlaybook.ChannelMode,
}))
if err != nil {
return "", errors.Wrap(err, "failed to store new playbook")
}
if err = p.replacePlaybookMembers(tx, rawPlaybook.Playbook); err != nil {
return "", errors.Wrap(err, "failed to replace playbook members")
}
if err = p.replacePlaybookMetrics(tx, rawPlaybook.Playbook); err != nil {
return "", errors.Wrap(err, "failed to replace playbook metrics configs")
}
if err = tx.Commit(); err != nil {
return "", errors.Wrap(err, "could not commit transaction")
}
return rawPlaybook.ID, nil
}
// Get retrieves a playbook
func (p *playbookStore) Get(id string) (app.Playbook, error) {
if id == "" {
return app.Playbook{}, errors.New("ID cannot be empty")
}
tx, err := p.store.db.Beginx()
if err != nil {
return app.Playbook{}, errors.Wrap(err, "could not begin transaction")
}
defer p.store.finalizeTransaction(tx)
var rawPlaybook sqlPlaybook
err = p.store.getBuilder(tx, &rawPlaybook, p.playbookSelect.Where(sq.Eq{"p.ID": id}))
if err == sql.ErrNoRows {
return app.Playbook{}, errors.Wrapf(app.ErrNotFound, "playbook does not exist for id '%s'", id)
} else if err != nil {
return app.Playbook{}, errors.Wrapf(err, "failed to get playbook by id '%s'", id)
}
playbook, err := toPlaybook(rawPlaybook)
if err != nil {
return app.Playbook{}, err
}
var members []playbookMember
err = p.store.selectBuilder(tx, &members, p.membersSelect.Where(sq.Eq{"PlaybookID": id}))
if err != nil && err != sql.ErrNoRows {
return app.Playbook{}, errors.Wrapf(err, "failed to get memberIDs for playbook with id '%s'", id)
}
var metrics []app.PlaybookMetricConfig
err = p.store.selectBuilder(tx, &metrics, p.metricsSelect.Where(sq.Eq{"PlaybookID": id}))
if err != nil && err != sql.ErrNoRows {
return app.Playbook{}, errors.Wrapf(err, "failed to get metrics configs for playbook with id '%s'", id)
}
if err = tx.Commit(); err != nil {
return app.Playbook{}, errors.Wrap(err, "could not commit transaction")
}
addMembersToPlaybook(members, &playbook)
playbook.Metrics = metrics
return playbook, nil
}
// GetPlaybooks retrieves all playbooks that are not deleted.
// Members are not retrieved for this as the query would be large and we don't need it for this for now.
// This is only used for the keywords feature
func (p *playbookStore) GetPlaybooks() ([]app.Playbook, error) {
tx, err := p.store.db.Beginx()
if err != nil {
return nil, errors.Wrap(err, "could not begin transaction")
}
defer p.store.finalizeTransaction(tx)
var playbooks []app.Playbook
err = p.store.selectBuilder(tx, &playbooks, p.store.builder.
Select(
"p.ID",
"p.Title",
"p.Description",
"p.TeamID",
"p.Public",
"p.CreatePublicIncident AS CreatePublicPlaybookRun",
"p.CreateAt",
"p.DeleteAt",
"p.NumStages",
"p.NumSteps",
"COUNT(i.ID) AS NumRuns",
"COALESCE(MAX(i.CreateAt), 0) AS LastRunAt",
`(
1 + -- Channel creation is hard-coded
CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END +
CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END +
CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END +
CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END +
CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END +
CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END
) AS NumActions`,
"COALESCE(ChannelNameTemplate, '') ChannelNameTemplate",
"COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole",
"COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole",
"COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole",
"COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole",
).
From("IR_Playbook AS p").
LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID").
LeftJoin("Teams t ON t.Id = p.TeamID").
LeftJoin("Schemes s ON t.SchemeId = s.Id").
Where(sq.Eq{"p.DeleteAt": 0}).
GroupBy("p.ID").
GroupBy("s.Id"))
if err == sql.ErrNoRows {
return nil, errors.Wrap(app.ErrNotFound, "no playbooks found")
} else if err != nil {
return nil, errors.Wrap(err, "failed to get playbooks")
}
return playbooks, nil
}
// GetPlaybooksForTeam retrieves all playbooks on the specified team given the provided options.
func (p *playbookStore) GetPlaybooksForTeam(requesterInfo app.RequesterInfo, teamID string, opts app.PlaybookFilterOptions) (app.GetPlaybooksResults, error) {
// Check that you are a playbook member or there are no restrictions.
permissionsAndFilter := sq.Expr(`(
EXISTS(SELECT 1
FROM IR_PlaybookMember as pm
WHERE pm.PlaybookID = p.ID
AND pm.MemberID = ?)
)`, requesterInfo.UserID)
if !opts.WithMembershipOnly { // return all public playbooks and private ones user is member of
permissionsAndFilter = sq.Or{sq.Expr(`p.Public = true`), permissionsAndFilter}
}
teamLimitExpr := buildTeamLimitExpr(requesterInfo, teamID, "p")
queryForResults := p.store.builder.
Select(
"p.ID",
"p.Title",
"p.Description",
"p.TeamID",
"p.Public",
"p.CreatePublicIncident AS CreatePublicPlaybookRun",
"p.CreateAt",
"p.DeleteAt",
"p.NumStages",
"p.NumSteps",
"p.DefaultCommanderEnabled AS DefaultOwnerEnabled",
"p.DefaultCommanderID AS DefaultOwnerID",
"COUNT(i.ID) AS NumRuns",
"COUNT(CASE WHEN i.CurrentStatus='InProgress' THEN 1 END) AS ActiveRuns",
"COALESCE(MAX(i.CreateAt), 0) AS LastRunAt",
`(
1 + -- Channel creation is hard-coded
CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END +
CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END +
CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END +
CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END +
CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END +
CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END +
CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END +
CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END
) AS NumActions`,
"COALESCE(ChannelNameTemplate, '') ChannelNameTemplate",
"COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole",
"COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole",
"COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole",
"COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole",
).
From("IR_Playbook AS p").
LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID").
LeftJoin("Teams t ON t.Id = p.TeamID").
LeftJoin("Schemes s ON t.SchemeId = s.Id").
GroupBy("p.ID").
GroupBy("s.Id").
Where(permissionsAndFilter).
Where(teamLimitExpr)
if len(opts.PlaybookIDs) > 0 {
queryForResults = queryForResults.Where(sq.Eq{"p.ID": opts.PlaybookIDs})
}
queryForResults, err := applyPlaybookFilterOptionsSort(queryForResults, opts)
if err != nil {
return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to apply sort options")
}
queryForTotal := p.store.builder.
Select("COUNT(*)").
From("IR_Playbook AS p").
Where(permissionsAndFilter).
Where(teamLimitExpr)
if opts.SearchTerm != "" {
column := "p.Title"
searchString := opts.SearchTerm
// Postgres performs a case-sensitive search, so we need to lowercase
// both the column contents and the search string
if p.store.db.DriverName() == model.DatabaseDriverPostgres {
column = "LOWER(p.Title)"
searchString = strings.ToLower(opts.SearchTerm)
}
queryForResults = queryForResults.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")})
queryForTotal = queryForTotal.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")})
}
if !opts.WithArchived {
queryForResults = queryForResults.Where(sq.Eq{"p.DeleteAt": 0})
queryForTotal = queryForTotal.Where(sq.Eq{"DeleteAt": 0})
}
var playbooks []app.Playbook
err = p.store.selectBuilder(p.store.db, &playbooks, queryForResults)
if err == sql.ErrNoRows {
return app.GetPlaybooksResults{}, errors.Wrap(app.ErrNotFound, "no playbooks found")
} else if err != nil {
return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbooks")
}
var total int
if err = p.store.getBuilder(p.store.db, &total, queryForTotal); err != nil {
return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get total count")
}
ids := make([]string, len(playbooks))
for _, pb := range playbooks {
ids = append(ids, pb.ID)
}
var members []playbookMember
err = p.store.selectBuilder(p.store.db, &members, p.membersSelect.Where(sq.Eq{"PlaybookID": ids}))
if err != nil {
return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbook members")
}
var metrics []app.PlaybookMetricConfig
err = p.store.selectBuilder(p.store.db, &metrics, p.metricsSelect.Where(sq.Eq{"PlaybookID": ids}))
if err != nil {
return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbooks metrics")
}
addMembersToPlaybooks(members, playbooks)
addMetricsToPlaybooks(metrics, playbooks)
pageCount := 0
if opts.PerPage > 0 {
pageCount = int(math.Ceil(float64(total) / float64(opts.PerPage)))
}
hasMore := opts.Page+1 < pageCount
return app.GetPlaybooksResults{
TotalCount: total,
PageCount: pageCount,
HasMore: hasMore,
Items: playbooks,
}, nil
}
// GetPlaybooksWithKeywords retrieves all playbooks with keywords enabled
func (p *playbookStore) GetPlaybooksWithKeywords(opts app.PlaybookFilterOptions) ([]app.Playbook, error) {
queryForResults := p.store.builder.
Select("ID", "Title", "UpdateAt", "TeamID", "ConcatenatedSignalAnyKeywords").
From("IR_Playbook AS p").
Where(sq.Eq{"SignalAnyKeywordsEnabled": true}).
Offset(uint64(opts.Page * opts.PerPage)).
Limit(uint64(opts.PerPage))
var rawPlaybooks []sqlPlaybook
err := p.store.selectBuilder(p.store.db, &rawPlaybooks, queryForResults)
if err == sql.ErrNoRows {
return []app.Playbook{}, nil
} else if err != nil {
return []app.Playbook{}, errors.Wrap(err, "failed to get playbooks")
}
playbooks := make([]app.Playbook, 0, len(rawPlaybooks))
for _, playbook := range rawPlaybooks {
out, err := toPlaybook(playbook)
if err != nil {
return nil, errors.Wrapf(err, "can't convert raw playbook to playbook type")
}
playbooks = append(playbooks, out)
}
return playbooks, nil
}
// GetTimeLastUpdated retrieves time last playbook was updated at.
// Passed argument determines whether to include playbooks with
// SignalAnyKeywordsEnabled flag or not.
func (p *playbookStore) GetTimeLastUpdated(onlyPlaybooksWithKeywordsEnabled bool) (int64, error) {
queryForResults := p.store.builder.
Select("COALESCE(MAX(UpdateAt), 0)").
From("IR_Playbook AS p").
Where(sq.Eq{"DeleteAt": 0})
if onlyPlaybooksWithKeywordsEnabled {
queryForResults = queryForResults.Where(sq.Eq{"SignalAnyKeywordsEnabled": true})
}
var updateAt []int64
err := p.store.selectBuilder(p.store.db, &updateAt, queryForResults)
if err == sql.ErrNoRows {
return 0, nil
} else if err != nil {
return 0, errors.Wrap(err, "failed to get playbooks")
}
return updateAt[0], nil
}
// GetPlaybookIDsForUser retrieves playbooks user can access
// Notice that method is not checking weather or not user is member of a team
func (p *playbookStore) GetPlaybookIDsForUser(userID string, teamID string) ([]string, error) {
// Check that you are a playbook member or there are no restrictions.
permissionsAndFilter := sq.Expr(`(
EXISTS(SELECT 1
FROM IR_PlaybookMember as pm
WHERE pm.PlaybookID = p.ID
AND pm.MemberID = ?)
OR NOT EXISTS(SELECT 1
FROM IR_PlaybookMember as pm
WHERE pm.PlaybookID = p.ID)
)`, userID)
queryForResults := p.store.builder.
Select("ID").
From("IR_Playbook AS p").
Where(sq.Eq{"DeleteAt": 0}).
Where(sq.Eq{"TeamID": teamID}).
Where(permissionsAndFilter)
var playbookIDs []string
err := p.store.selectBuilder(p.store.db, &playbookIDs, queryForResults)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get playbookIDs for a user - %v", userID)
}
return playbookIDs, nil
}
func (p *playbookStore) GraphqlUpdate(id string, setmap map[string]interface{}) error {
if id == "" {
return errors.New("id should not be empty")
}
// if checklists are passed and len (as string) is bigger than limit -> fails
if _, exists := setmap["ChecklistsJSON"]; exists {
if len(string(setmap["ChecklistsJSON"].([]uint8))) > maxJSONLength {
return fmt.Errorf("failed update playbook with id '%s': json too long (max %d)", id, maxJSONLength)
}
}
_, err := p.store.execBuilder(p.store.db, sq.
Update("IR_Playbook").
SetMap(setmap).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to update playbook with id '%s'", id)
}
return nil
}
// Update updates a playbook
func (p *playbookStore) Update(playbook app.Playbook) (err error) {
if playbook.ID == "" {
return errors.New("id should not be empty")
}
rawPlaybook, err := toSQLPlaybook(playbook)
if err != nil {
return err
}
tx, err := p.store.db.Beginx()
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer p.store.finalizeTransaction(tx)
_, err = p.store.execBuilder(tx, sq.
Update("IR_Playbook").
SetMap(map[string]interface{}{
"Title": rawPlaybook.Title,
"Description": rawPlaybook.Description,
"TeamID": rawPlaybook.TeamID,
"Public": rawPlaybook.Public,
"CreatePublicIncident": rawPlaybook.CreatePublicPlaybookRun,
"UpdateAt": rawPlaybook.UpdateAt,
"DeleteAt": rawPlaybook.DeleteAt,
"ChecklistsJSON": rawPlaybook.ChecklistsJSON,
"NumStages": len(rawPlaybook.Checklists),
"NumSteps": getSteps(rawPlaybook.Playbook),
"ReminderMessageTemplate": rawPlaybook.ReminderMessageTemplate,
"ReminderTimerDefaultSeconds": rawPlaybook.ReminderTimerDefaultSeconds,
"StatusUpdateEnabled": rawPlaybook.StatusUpdateEnabled,
"ConcatenatedInvitedUserIDs": rawPlaybook.ConcatenatedInvitedUserIDs,
"ConcatenatedInvitedGroupIDs": rawPlaybook.ConcatenatedInvitedGroupIDs,
"InviteUsersEnabled": rawPlaybook.InviteUsersEnabled,
"DefaultCommanderID": rawPlaybook.DefaultOwnerID,
"DefaultCommanderEnabled": rawPlaybook.DefaultOwnerEnabled,
"ConcatenatedBroadcastChannelIDs": rawPlaybook.ConcatenatedBroadcastChannelIDs,
"BroadcastEnabled": rawPlaybook.BroadcastEnabled, //nolint
"ConcatenatedWebhookOnCreationURLs": rawPlaybook.ConcatenatedWebhookOnCreationURLs,
"WebhookOnCreationEnabled": rawPlaybook.WebhookOnCreationEnabled,
"MessageOnJoin": rawPlaybook.MessageOnJoin,
"MessageOnJoinEnabled": rawPlaybook.MessageOnJoinEnabled,
"RetrospectiveReminderIntervalSeconds": rawPlaybook.RetrospectiveReminderIntervalSeconds,
"RetrospectiveTemplate": rawPlaybook.RetrospectiveTemplate,
"RetrospectiveEnabled": rawPlaybook.RetrospectiveEnabled,
"ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs,
"WebhookOnStatusUpdateEnabled": rawPlaybook.WebhookOnStatusUpdateEnabled,
"ConcatenatedSignalAnyKeywords": rawPlaybook.ConcatenatedSignalAnyKeywords,
"SignalAnyKeywordsEnabled": rawPlaybook.SignalAnyKeywordsEnabled,
"CategorizeChannelEnabled": rawPlaybook.CategorizeChannelEnabled,
"CategoryName": rawPlaybook.CategoryName,
"RunSummaryTemplateEnabled": rawPlaybook.RunSummaryTemplateEnabled,
"RunSummaryTemplate": rawPlaybook.RunSummaryTemplate,
"ChannelNameTemplate": rawPlaybook.ChannelNameTemplate,
"CreateChannelMemberOnNewParticipant": rawPlaybook.CreateChannelMemberOnNewParticipant,
"RemoveChannelMemberOnRemovedParticipant": rawPlaybook.RemoveChannelMemberOnRemovedParticipant,
"ChannelID": rawPlaybook.ChannelID,
"ChannelMode": rawPlaybook.ChannelMode,
}).
Where(sq.Eq{"ID": rawPlaybook.ID}))
if err != nil {
return errors.Wrapf(err, "failed to update playbook with id '%s'", rawPlaybook.ID)
}
if err = p.replacePlaybookMembers(tx, rawPlaybook.Playbook); err != nil {
return errors.Wrapf(err, "failed to replace playbook members for playbook with id '%s'", rawPlaybook.ID)
}
if err = p.replacePlaybookMetrics(tx, rawPlaybook.Playbook); err != nil {
return errors.Wrapf(err, "failed to replace playbook metrics configs for playbook with id '%s'", rawPlaybook.ID)
}
if err = tx.Commit(); err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}
// Archive archives a playbook.
func (p *playbookStore) Archive(id string) error {
if id == "" {
return errors.New("ID cannot be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Update("IR_Playbook").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to delete playbook with id '%s'", id)
}
return nil
}
// Restore restores a deleted playbook.
func (p *playbookStore) Restore(id string) error {
if id == "" {
return errors.New("ID cannot be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Update("IR_Playbook").
Set("DeleteAt", 0).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to restore playbook with id '%s'", id)
}
return nil
}
// Get number of active playbooks.
func (p *playbookStore) GetPlaybooksActiveTotal() (int64, error) {
var count int64
query := p.store.builder.
Select("COUNT(*)").
From("IR_Playbook").
Where(sq.Eq{"DeleteAt": 0})
if err := p.store.getBuilder(p.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count active playbooks'")
}
return count, nil
}
// Get number of active playbooks.
func (p *playbookStore) GetNumMetrics(playbookID string) (int64, error) {
var count int64
query := p.store.builder.
Select("COUNT(*)").
From("IR_MetricConfig").
Where(sq.Eq{"PlaybookID": playbookID})
if err := p.store.getBuilder(p.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count metrics")
}
return count, nil
}
func (p *playbookStore) AddPlaybookMember(id string, memberID string) error {
if id == "" || memberID == "" {
return errors.New("ids should not be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Insert("IR_PlaybookMember").
Columns("PlaybookID", "MemberID", "Roles").
Values(id, memberID, app.PlaybookRoleMember))
if err != nil {
return errors.Wrapf(err, "failed to update playbook with id '%s'", id)
}
return nil
}
func (p *playbookStore) RemovePlaybookMember(id string, memberID string) error {
if id == "" || memberID == "" {
return errors.New("ids should not be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Delete("IR_PlaybookMember").
Where(sq.Eq{"PlaybookID": id}).
Where(sq.Eq{"MemberID": memberID}))
if err != nil {
return errors.Wrapf(err, "failed to update playbook with id '%s'", id)
}
return nil
}
// replacePlaybookMembers replaces the members of a playbook
func (p *playbookStore) replacePlaybookMembers(q queryExecer, playbook app.Playbook) error {
// Delete existing members who are not in the new playbook.MemberIDs list
delBuilder := sq.Delete("IR_PlaybookMember").
Where(sq.Eq{"PlaybookID": playbook.ID})
if _, err := p.store.execBuilder(q, delBuilder); err != nil {
return err
}
if len(playbook.Members) == 0 {
return nil
}
insert := sq.
Insert("IR_PlaybookMember").
Columns("PlaybookID", "MemberID", "Roles")
for _, m := range playbook.Members {
insert = insert.Values(playbook.ID, m.UserID, strings.Join(m.Roles, " "))
}
if _, err := p.store.execBuilder(q, insert); err != nil {
return err
}
return nil
}
// replacePlaybookMetrics replaces the metric configs of a playbook
func (p *playbookStore) replacePlaybookMetrics(q queryExecer, playbook app.Playbook) error {
// First, we mark as deleted all existing metrics for this playbook, then restore those which are in the playbook object.
updateBuilder := sq.Update("IR_MetricConfig").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"PlaybookID": playbook.ID}).
Where(sq.Eq{"DeleteAt": 0})
if _, err := p.store.execBuilder(q, updateBuilder); err != nil {
return err
}
// Restore and update existing metric configs. Insert a new ones.
var err error
for i, m := range playbook.Metrics {
if m.ID == "" {
_, err = p.store.execBuilder(q, sq.
Insert("IR_MetricConfig").
Columns("ID", "PlaybookID", "Title", "Description", "Type", "Target", "Ordering").
Values(model.NewId(), playbook.ID, m.Title, m.Description, m.Type, m.Target, i))
} else {
_, err = p.store.execBuilder(q, sq.
Update("IR_MetricConfig").
SetMap(map[string]interface{}{
"Title": m.Title,
"Description": m.Description,
"Target": m.Target,
"Ordering": i,
"DeleteAt": 0,
}).
Where(sq.Eq{"ID": m.ID}),
)
}
if err != nil {
return err
}
}
return nil
}
func (p *playbookStore) AutoFollow(playbookID, userID string) error {
var err error
if p.store.db.DriverName() == model.DatabaseDriverMysql {
_, err = p.store.execBuilder(p.store.db, sq.
Insert("IR_PlaybookAutoFollow").
Columns("PlaybookID", "UserID").
Values(playbookID, userID).
Suffix("ON DUPLICATE KEY UPDATE playbookID = playbookID"))
} else {
_, err = p.store.execBuilder(p.store.db, sq.
Insert("IR_PlaybookAutoFollow").
Columns("PlaybookID", "UserID").
Values(playbookID, userID).
Suffix("ON CONFLICT (PlaybookID,UserID) DO NOTHING"))
}
return errors.Wrapf(err, "failed to insert autofollowing '%s' for playbook '%s'", userID, playbookID)
}
func (p *playbookStore) AutoUnfollow(playbookID, userID string) error {
if _, err := p.store.execBuilder(p.store.db, sq.
Delete("IR_PlaybookAutoFollow").
Where(sq.And{sq.Eq{"UserID": userID}, sq.Eq{"PlaybookID": playbookID}})); err != nil {
return errors.Wrapf(err, "failed to delete autofollow '%s' for playbook '%s'", userID, playbookID)
}
return nil
}
func (p *playbookStore) GetAutoFollows(playbookID string) ([]string, error) {
query := p.queryBuilder.
Select("UserID").
From("IR_PlaybookAutoFollow").
Where(sq.Eq{"PlaybookID": playbookID})
autoFollows := make([]string, 0)
err := p.store.selectBuilder(p.store.db, &autoFollows, query)
if err == sql.ErrNoRows {
return []string{}, nil
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get autoFollows for playbook '%s'", playbookID)
}
return autoFollows, nil
}
func (p *playbookStore) GetMetric(id string) (*app.PlaybookMetricConfig, error) {
metricSelect := p.queryBuilder.
Select(
"c.ID",
"c.PlaybookID",
"c.Title",
"c.Description",
"c.Type",
"c.Target",
).
From("IR_MetricConfig c").
Where(sq.Eq{"c.ID": id})
var metric app.PlaybookMetricConfig
err := p.store.getBuilder(p.store.db, &metric, metricSelect)
if err != nil {
return nil, err
}
return &metric, nil
}
func (p *playbookStore) AddMetric(playbookID string, config app.PlaybookMetricConfig) error {
numExistingMetrics, err := p.GetNumMetrics(playbookID)
if err != nil {
return err
}
if numExistingMetrics >= app.MaxMetricsPerPlaybook {
return errors.Errorf("playbook cannot have more than %d key metrics", app.MaxMetricsPerPlaybook)
}
_, err = p.store.execBuilder(p.store.db, sq.
Insert("IR_MetricConfig").
Columns("ID", "PlaybookID", "Title", "Description", "Type", "Target", "Ordering").
Values(model.NewId(), playbookID, config.Title, config.Description, config.Type, config.Target, numExistingMetrics))
if err != nil {
return errors.Wrapf(err, "failed to add metric")
}
return nil
}
func (p *playbookStore) DeleteMetric(id string) error {
if id == "" {
return errors.New("id should not be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Update("IR_MetricConfig").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to delete metric with id %q", id)
}
return nil
}
func (p *playbookStore) UpdateMetric(id string, setmap map[string]interface{}) error {
if id == "" {
return errors.New("id should not be empty")
}
_, err := p.store.execBuilder(p.store.db, sq.
Update("IR_MetricConfig").
SetMap(setmap).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to update metric with id %q", id)
}
return nil
}
func generatePlaybookSchemeRoles(member playbookMember, playbook *app.Playbook) []string {
schemeRoles := []string{}
for _, role := range strings.Fields(member.Roles) {
if role == app.PlaybookRoleAdmin {
if playbook.DefaultPlaybookAdminRole == "" {
schemeRoles = append(schemeRoles, app.PlaybookRoleAdmin)
} else {
schemeRoles = append(schemeRoles, playbook.DefaultPlaybookAdminRole)
}
} else if role == app.PlaybookRoleMember {
if playbook.DefaultPlaybookMemberRole == "" {
schemeRoles = append(schemeRoles, app.PlaybookRoleMember)
} else {
schemeRoles = append(schemeRoles, playbook.DefaultPlaybookMemberRole)
}
}
}
return schemeRoles
}
func addMembersToPlaybooks(members []playbookMember, playbooks []app.Playbook) {
playbookToMembers := make(map[string][]playbookMember)
for _, member := range members {
playbookToMembers[member.PlaybookID] = append(playbookToMembers[member.PlaybookID], member)
}
for i, playbook := range playbooks {
addMembersToPlaybook(playbookToMembers[playbook.ID], &(playbooks[i]))
}
}
func addMembersToPlaybook(members []playbookMember, playbook *app.Playbook) {
for _, m := range members {
playbook.Members = append(playbook.Members, app.PlaybookMember{
UserID: m.MemberID,
Roles: strings.Fields(m.Roles),
SchemeRoles: generatePlaybookSchemeRoles(m, playbook),
})
}
}
func addMetricsToPlaybooks(metrics []app.PlaybookMetricConfig, playbooks []app.Playbook) {
playbookToMetrics := make(map[string][]app.PlaybookMetricConfig)
for _, metric := range metrics {
playbookToMetrics[metric.PlaybookID] = append(playbookToMetrics[metric.PlaybookID], metric)
}
for i, playbook := range playbooks {
playbooks[i].Metrics = playbookToMetrics[playbook.ID]
}
}
func getSteps(playbook app.Playbook) int {
steps := 0
for _, p := range playbook.Checklists {
steps += len(p.Items)
}
return steps
}
func toSQLPlaybook(playbook app.Playbook) (*sqlPlaybook, error) {
checklistsJSON, err := json.Marshal(playbook.Checklists)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal checklist json for playbook id: '%s'", playbook.ID)
}
if len(checklistsJSON) > maxJSONLength {
return nil, errors.Wrapf(errors.New("invalid data"), "checklist json for playbook id '%s' is too long (max %d)", playbook.ID, maxJSONLength)
}
return &sqlPlaybook{
Playbook: playbook,
ChecklistsJSON: checklistsJSON,
ConcatenatedInvitedUserIDs: strings.Join(playbook.InvitedUserIDs, ","),
ConcatenatedInvitedGroupIDs: strings.Join(playbook.InvitedGroupIDs, ","),
ConcatenatedSignalAnyKeywords: strings.Join(playbook.SignalAnyKeywords, ","),
ConcatenatedBroadcastChannelIDs: strings.Join(playbook.BroadcastChannelIDs, ","),
ConcatenatedWebhookOnCreationURLs: strings.Join(playbook.WebhookOnCreationURLs, ","),
ConcatenatedWebhookOnStatusUpdateURLs: strings.Join(playbook.WebhookOnStatusUpdateURLs, ","),
}, nil
}
func toPlaybook(rawPlaybook sqlPlaybook) (app.Playbook, error) {
p := rawPlaybook.Playbook
if len(rawPlaybook.ChecklistsJSON) > 0 {
if err := json.Unmarshal(rawPlaybook.ChecklistsJSON, &p.Checklists); err != nil {
return app.Playbook{}, errors.Wrapf(err, "failed to unmarshal checklists json for playbook id: '%s'", p.ID)
}
}
p.InvitedUserIDs = []string(nil)
if rawPlaybook.ConcatenatedInvitedUserIDs != "" {
p.InvitedUserIDs = strings.Split(rawPlaybook.ConcatenatedInvitedUserIDs, ",")
}
p.InvitedGroupIDs = []string(nil)
if rawPlaybook.ConcatenatedInvitedGroupIDs != "" {
p.InvitedGroupIDs = strings.Split(rawPlaybook.ConcatenatedInvitedGroupIDs, ",")
}
p.SignalAnyKeywords = []string(nil)
if rawPlaybook.ConcatenatedSignalAnyKeywords != "" {
p.SignalAnyKeywords = strings.Split(rawPlaybook.ConcatenatedSignalAnyKeywords, ",")
}
p.BroadcastChannelIDs = []string(nil)
if rawPlaybook.ConcatenatedBroadcastChannelIDs != "" {
p.BroadcastChannelIDs = strings.Split(rawPlaybook.ConcatenatedBroadcastChannelIDs, ",")
}
p.WebhookOnCreationURLs = []string(nil)
if rawPlaybook.ConcatenatedWebhookOnCreationURLs != "" {
p.WebhookOnCreationURLs = strings.Split(rawPlaybook.ConcatenatedWebhookOnCreationURLs, ",")
}
p.WebhookOnStatusUpdateURLs = []string(nil)
if rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs != "" {
p.WebhookOnStatusUpdateURLs = strings.Split(rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs, ",")
}
return p, nil
}
// insights - store manager functions
func (p *playbookStore) GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*app.PlaybooksInsightsList, error) {
query := insightsQueryBuilder(p, teamID, userID, opts, insightsQueryTypeTeam)
topPlaybooksList := make([]*app.PlaybookInsight, 0)
err := p.store.selectBuilder(p.store.db, &topPlaybooksList, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top team playbooks for for user: %s", userID)
}
topPlaybooks := GetTopPlaybooksInsightsListWithPagination(topPlaybooksList, opts.PerPage)
return topPlaybooks, nil
}
func (p *playbookStore) GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*app.PlaybooksInsightsList, error) {
query := insightsQueryBuilder(p, teamID, userID, opts, insightsQueryTypeUser)
topPlaybooksList := make([]*app.PlaybookInsight, 0)
err := p.store.selectBuilder(p.store.db, &topPlaybooksList, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top user playbooks for for user: %s", userID)
}
topPlaybooks := GetTopPlaybooksInsightsListWithPagination(topPlaybooksList, opts.PerPage)
return topPlaybooks, nil
}
func insightsQueryBuilder(p *playbookStore, teamID, userID string, opts *model.InsightsOpts, queryType string) sq.SelectBuilder {
permissionsAndFilter := sq.Expr(`(
EXISTS(SELECT 1
FROM IR_PlaybookMember as pm
WHERE pm.PlaybookID = p.ID
AND pm.MemberID = ?)
)`, userID)
var whereCondition sq.And
if queryType == insightsQueryTypeUser {
whereCondition = sq.And{
permissionsAndFilter,
sq.Eq{"p.TeamID": teamID},
sq.GtOrEq{"i.CreateAt": opts.StartUnixMilli},
}
} else if queryType == insightsQueryTypeTeam {
whereCondition = sq.And{
sq.GtOrEq{"i.CreateAt": opts.StartUnixMilli},
sq.Or{
permissionsAndFilter,
sq.Eq{"p.Public": true},
},
sq.Eq{"p.TeamID": teamID},
}
} else {
whereCondition = sq.And{}
}
offset := opts.Page * opts.PerPage
limit := opts.PerPage
query := p.queryBuilder.
Select(
"p.ID as PlaybookID",
"p.Title",
"COUNT(i.ID) AS NumRuns",
"COALESCE(MAX(i.CreateAt), 0) AS LastRunAt",
).
From("IR_Playbook as p").
LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID").
Where(whereCondition).
GroupBy("p.ID").
OrderBy("NumRuns desc").
Offset(uint64(offset)).
Limit(uint64(limit + 1))
return query
}
// GetTopPlaybooksInsightsListWithPagination returns a page given a list of PlaybooksInsight assumed to be
// sorted by Runs(score). Returns a PlaybooksInsightsList.
func GetTopPlaybooksInsightsListWithPagination(playbooks []*app.PlaybookInsight, limit int) *app.PlaybooksInsightsList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(playbooks) == limit+1) {
hasNext = true
playbooks = playbooks[:len(playbooks)-1]
}
return &app.PlaybooksInsightsList{HasNext: hasNext, Items: playbooks}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"strings"
"time"
"gopkg.in/guregu/null.v4"
"github.com/jmoiron/sqlx"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
const (
legacyEventTypeCommanderChanged = "commander_changed"
)
type sqlPlaybookRun struct {
app.PlaybookRun
ChecklistsJSON json.RawMessage
ConcatenatedInvitedUserIDs string
ConcatenatedInvitedGroupIDs string
ConcatenatedParticipantIDs string
ConcatenatedBroadcastChannelIDs string
ConcatenatedWebhookOnCreationURLs string
ConcatenatedWebhookOnStatusUpdateURLs string
Metric null.Int
}
type sqlRunMetricData struct {
IncidentID string
MetricConfigID string
Value null.Int
}
// playbookRunStore holds the information needed to fulfill the methods in the store interface.
type playbookRunStore struct {
pluginAPI PluginAPIClient
store *SQLStore
queryBuilder sq.StatementBuilderType
playbookRunSelect sq.SelectBuilder
statusPostsSelect sq.SelectBuilder
timelineEventsSelect sq.SelectBuilder
metricsDataSelectSingleRun sq.SelectBuilder
sqlMetricsDataSelectMultipleRuns sq.SelectBuilder
}
// Ensure playbookRunStore implements the app.PlaybookRunStore interface.
var _ app.PlaybookRunStore = (*playbookRunStore)(nil)
type playbookRunStatusPosts []struct {
PlaybookRunID string
app.StatusPost
}
func applyPlaybookRunFilterOptionsSort(builder sq.SelectBuilder, options app.PlaybookRunFilterOptions) (sq.SelectBuilder, error) {
var sort string
switch options.Sort {
case app.SortByCreateAt:
sort = "CreateAt"
case app.SortByID:
sort = "ID"
case app.SortByName:
sort = "Name"
case app.SortByOwnerUserID:
sort = "OwnerUserID"
case app.SortByTeamID:
sort = "TeamID"
case app.SortByEndAt:
sort = "EndAt"
case app.SortByStatus:
sort = "CurrentStatus"
case app.SortByLastStatusUpdateAt:
sort = "LastStatusUpdateAt"
case "":
// Default to a stable sort if none explicitly provided.
sort = "ID"
case app.SortByMetric0, app.SortByMetric1, app.SortByMetric2, app.SortByMetric3:
// Will handle below
default:
return sq.SelectBuilder{}, errors.Errorf("unsupported sort parameter '%s'", options.Sort)
}
var direction string
switch options.Direction {
case app.DirectionAsc:
direction = "ASC"
case app.DirectionDesc:
direction = "DESC"
case "":
// Default to an ascending sort if none explicitly provided.
direction = "ASC"
default:
return sq.SelectBuilder{}, errors.Errorf("unsupported direction parameter '%s'", options.Direction)
}
page := options.Page
perPage := options.PerPage
if page < 0 {
page = 0
}
if perPage < 0 {
perPage = 0
}
builder = builder.
Offset(uint64(page * perPage)).
Limit(uint64(perPage))
switch options.Sort {
case app.SortByMetric0, app.SortByMetric1, app.SortByMetric2, app.SortByMetric3:
if options.PlaybookID == "" {
return sq.SelectBuilder{}, errors.New("sorting by metric requires a playbook_id")
}
ordering := 0
switch options.Sort {
case app.SortByMetric1:
ordering = 1
case app.SortByMetric2:
ordering = 2
case app.SortByMetric3:
ordering = 3
}
// Since we're sorting by metric, we need to create the correct metric column to sort by
builder = builder.Column(
sq.Alias(
sq.Select("m.Value").
From("IR_Metric AS m").
InnerJoin("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)").
Where("mc.DeleteAt = 0").
Where(sq.Eq{"mc.PlaybookID": options.PlaybookID}).
Where("m.IncidentID = i.ID").
Where(sq.Eq{"mc.Ordering": ordering}),
"Metric",
)).
OrderByClause("Metric " + direction)
default:
builder = builder.OrderByClause(fmt.Sprintf("%s %s", sort, direction))
}
return builder, nil
}
// NewPlaybookRunStore creates a new store for playbook run ServiceImpl.
func NewPlaybookRunStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.PlaybookRunStore {
// construct the participants list so that the frontend doesn't have to query the server, bc if
// the user is not a member of the channel they won't have permissions to get the user list
participantsCol := `
COALESCE(
(SELECT string_agg(rp.UserId, ',')
FROM IR_Incident as i2
JOIN IR_Run_Participants as rp on rp.IncidentID = i2.ID
WHERE i2.Id = i.Id
AND rp.IsParticipant = true
AND rp.UserId NOT IN (SELECT UserId FROM Bots)
), ''
) AS ConcatenatedParticipantIDs`
if sqlStore.db.DriverName() == model.DatabaseDriverMysql {
participantsCol = `
COALESCE(
(SELECT group_concat(rp.UserId separator ',')
FROM IR_Incident as i2
JOIN IR_Run_Participants as rp on rp.IncidentID = i2.ID
WHERE i2.Id = i.Id
AND rp.IsParticipant = true
AND rp.UserId NOT IN (SELECT UserId FROM Bots)
), ''
) AS ConcatenatedParticipantIDs`
}
// When adding a PlaybookRun column #1: add to this select
playbookRunSelect := sqlStore.builder.
Select("i.ID", "i.Name AS Name", "i.Description AS Summary", "i.CommanderUserID AS OwnerUserID", "i.TeamID", "i.ChannelID",
"i.CreateAt", "i.EndAt", "i.DeleteAt", "i.PostID", "i.PlaybookID", "i.ReporterUserID", "i.CurrentStatus", "i.LastStatusUpdateAt",
"i.ChecklistsJSON", "COALESCE(i.ReminderPostID, '') ReminderPostID", "i.PreviousReminder",
"COALESCE(ReminderMessageTemplate, '') ReminderMessageTemplate", "ReminderTimerDefaultSeconds", "StatusUpdateEnabled",
"ConcatenatedInvitedUserIDs", "ConcatenatedInvitedGroupIDs", "DefaultCommanderID AS DefaultOwnerID",
"ConcatenatedBroadcastChannelIDs", "ConcatenatedWebhookOnCreationURLs", "Retrospective", "RetrospectiveEnabled", "MessageOnJoin", "RetrospectivePublishedAt", "RetrospectiveReminderIntervalSeconds",
"RetrospectiveWasCanceled", "ConcatenatedWebhookOnStatusUpdateURLs", "StatusUpdateBroadcastChannelsEnabled", "StatusUpdateBroadcastWebhooksEnabled",
"CreateChannelMemberOnNewParticipant", "RemoveChannelMemberOnRemovedParticipant",
"COALESCE(CategoryName, '') CategoryName", "SummaryModifiedAt", "i.RunType AS Type").
Column(participantsCol).
From("IR_Incident AS i")
statusPostsSelect := sqlStore.builder.
Select("sp.IncidentID AS PlaybookRunID", "p.ID", "p.CreateAt", "p.DeleteAt").
From("IR_StatusPosts as sp").
Join("Posts as p ON sp.PostID = p.Id")
timelineEventsSelect := sqlStore.builder.
Select(
"te.ID",
"te.IncidentID AS PlaybookRunID",
"te.CreateAt",
"te.DeleteAt",
"te.EventAt",
).
// Map "commander_changed" to "owner_changed", preserving database compatibility
// without complicating the code.
Column(
sq.Alias(
sq.Case().
When(sq.Eq{"te.EventType": legacyEventTypeCommanderChanged}, sq.Expr("?", app.OwnerChanged)).
Else("te.EventType"),
"EventType",
),
).
Columns(
"te.Summary",
"te.Details",
"te.PostID",
"te.SubjectUserID",
"te.CreatorUserID",
).
From("IR_TimelineEvent as te")
metricsDataSelectSingleRun := sqlStore.builder.
Select("MetricConfigID", "Value").
From("IR_Metric AS m").
Join("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)").
Where("mc.DeleteAt = 0")
sqlMetricsDataSelectMultipleRuns := sqlStore.builder.
Select("IncidentID", "MetricConfigID", "Value").
From("IR_Metric AS m").
Join("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)").
Where("mc.DeleteAt = 0").
OrderBy("mc.Ordering ASC")
return &playbookRunStore{
pluginAPI: pluginAPI,
store: sqlStore,
queryBuilder: sqlStore.builder,
playbookRunSelect: playbookRunSelect,
statusPostsSelect: statusPostsSelect,
timelineEventsSelect: timelineEventsSelect,
metricsDataSelectSingleRun: metricsDataSelectSingleRun,
sqlMetricsDataSelectMultipleRuns: sqlMetricsDataSelectMultipleRuns,
}
}
// GetPlaybookRuns returns filtered playbook runs and the total count before paging.
func (s *playbookRunStore) GetPlaybookRuns(requesterInfo app.RequesterInfo, options app.PlaybookRunFilterOptions) (*app.GetPlaybookRunsResults, error) {
permissionsExpr := s.buildPermissionsExpr(requesterInfo)
teamLimitExpr := buildTeamLimitExpr(requesterInfo, options.TeamID, "i")
queryForResults := s.playbookRunSelect.
Where(permissionsExpr).
Where(teamLimitExpr)
queryForTotal := s.store.builder.
Select("COUNT(*)").
From("IR_Incident AS i").
Where(permissionsExpr).
Where(teamLimitExpr)
if len(options.Statuses) != 0 {
queryForResults = queryForResults.Where(sq.Eq{"i.CurrentStatus": options.Statuses})
queryForTotal = queryForTotal.Where(sq.Eq{"i.CurrentStatus": options.Statuses})
}
if len(options.Types) != 0 {
queryForResults = queryForResults.Where(sq.Eq{"i.RunType": options.Types})
queryForTotal = queryForTotal.Where(sq.Eq{"i.RunType": options.Types})
}
if options.OwnerID != "" {
queryForResults = queryForResults.Where(sq.Eq{"i.CommanderUserID": options.OwnerID})
queryForTotal = queryForTotal.Where(sq.Eq{"i.CommanderUserID": options.OwnerID})
}
if options.ParticipantID != "" {
membershipClause := s.queryBuilder.
Select("1").
Prefix("EXISTS(").
From("IR_Run_Participants AS p").
Where("p.IncidentID = i.ID").
Where("p.IsParticipant = true").
Where(sq.Eq{"p.UserID": strings.ToLower(options.ParticipantID)}).
Suffix(")")
queryForResults = queryForResults.Where(membershipClause)
queryForTotal = queryForTotal.Where(membershipClause)
}
if options.ParticipantOrFollowerID != "" {
userIDFilter := strings.ToLower(options.ParticipantOrFollowerID)
followerFilterExpr := sq.Expr(`EXISTS(SELECT 1
FROM IR_Run_Participants as rp
WHERE rp.IncidentID = i.ID
AND rp.UserID = ?
AND rp.IsFollower = TRUE)`, userIDFilter)
participantFilterExpr := sq.Expr(`EXISTS(SELECT 1
FROM IR_Run_Participants as rp
WHERE rp.IncidentID = i.ID
AND rp.UserID = ?
AND rp.IsParticipant = TRUE)`, userIDFilter)
myRunsClause := sq.Or{followerFilterExpr, participantFilterExpr}
if options.IncludeFavorites {
favoriteFilterExpr := sq.Expr(`EXISTS(SELECT 1
FROM IR_Category AS cat
INNER JOIN IR_Category_Item it ON cat.ID = it.CategoryID
WHERE cat.Name = 'Favorite'
AND it.Type = 'r'
AND it.ItemID = i.ID
AND cat.UserID = ?)`, userIDFilter)
myRunsClause = append(myRunsClause, favoriteFilterExpr)
}
queryForResults = queryForResults.Where(myRunsClause)
queryForTotal = queryForTotal.Where(myRunsClause)
}
if options.PlaybookID != "" {
queryForResults = queryForResults.Where(sq.Eq{"i.PlaybookID": options.PlaybookID})
queryForTotal = queryForTotal.Where(sq.Eq{"i.PlaybookID": options.PlaybookID})
}
// TODO: do we need to sanitize (replace any '%'s in the search term)?
if options.SearchTerm != "" {
column := "i.Name"
searchString := options.SearchTerm
// Postgres performs a case-sensitive search, so we need to lowercase
// both the column contents and the search string
if s.store.db.DriverName() == model.DatabaseDriverPostgres {
column = "LOWER(i.Name)"
searchString = strings.ToLower(options.SearchTerm)
}
queryForResults = queryForResults.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")})
queryForTotal = queryForTotal.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")})
}
if options.ChannelID != "" {
queryForResults = queryForResults.Where(sq.Eq{"i.ChannelId": options.ChannelID})
queryForTotal = queryForTotal.Where(sq.Eq{"i.ChannelId": options.ChannelID})
}
queryForResults = queryActiveBetweenTimes(queryForResults, options.ActiveGTE, options.ActiveLT)
queryForTotal = queryActiveBetweenTimes(queryForTotal, options.ActiveGTE, options.ActiveLT)
queryForResults = queryStartedBetweenTimes(queryForResults, options.StartedGTE, options.StartedLT)
queryForTotal = queryStartedBetweenTimes(queryForTotal, options.StartedGTE, options.StartedLT)
queryForResults, err := applyPlaybookRunFilterOptionsSort(queryForResults, options)
if err != nil {
return nil, errors.Wrap(err, "failed to apply sort options")
}
tx, err := s.store.db.Beginx()
if err != nil {
return nil, errors.Wrap(err, "could not begin transaction")
}
defer s.store.finalizeTransaction(tx)
var rawPlaybookRuns []sqlPlaybookRun
if err = s.store.selectBuilder(tx, &rawPlaybookRuns, queryForResults); err != nil {
return nil, errors.Wrap(err, "failed to query for playbook runs")
}
var total int
if err = s.store.getBuilder(tx, &total, queryForTotal); err != nil {
return nil, errors.Wrap(err, "failed to get total count")
}
pageCount := 0
if options.PerPage > 0 {
pageCount = int(math.Ceil(float64(total) / float64(options.PerPage)))
}
hasMore := options.Page+1 < pageCount
playbookRuns := make([]app.PlaybookRun, 0, len(rawPlaybookRuns))
playbookRunIDs := make([]string, 0, len(rawPlaybookRuns))
for _, rawPlaybookRun := range rawPlaybookRuns {
var playbookRun *app.PlaybookRun
playbookRun, err = s.toPlaybookRun(rawPlaybookRun)
if err != nil {
return nil, err
}
playbookRuns = append(playbookRuns, *playbookRun)
playbookRunIDs = append(playbookRunIDs, playbookRun.ID)
}
var statusPosts playbookRunStatusPosts
postInfoSelect := s.statusPostsSelect.
OrderBy("p.CreateAt").
Where(sq.Eq{"sp.IncidentID": playbookRunIDs})
err = s.store.selectBuilder(tx, &statusPosts, postInfoSelect)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to get playbook run status posts")
}
timelineEvents, err := s.getTimelineEventsForPlaybookRun(tx, playbookRunIDs)
if err != nil {
return nil, err
}
metricsData, err := s.getMetricsForPlaybookRun(tx, playbookRunIDs)
if err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, errors.Wrap(err, "could not commit transaction")
}
addStatusPostsToPlaybookRuns(statusPosts, playbookRuns)
addTimelineEventsToPlaybookRuns(timelineEvents, playbookRuns)
addMetricsToPlaybookRuns(metricsData, playbookRuns)
return &app.GetPlaybookRunsResults{
TotalCount: total,
PageCount: pageCount,
PerPage: options.PerPage,
HasMore: hasMore,
Items: playbookRuns,
}, nil
}
// CreatePlaybookRun creates a new playbook run. If playbook run has an ID, that ID will be used.
func (s *playbookRunStore) CreatePlaybookRun(playbookRun *app.PlaybookRun) (*app.PlaybookRun, error) {
if playbookRun == nil {
return nil, errors.New("playbook run is nil")
}
playbookRun = playbookRun.Clone()
if playbookRun.ID == "" {
playbookRun.ID = model.NewId()
}
playbookRun.Checklists = populateChecklistIDs(playbookRun.Checklists)
rawPlaybookRun, err := toSQLPlaybookRun(*playbookRun)
if err != nil {
return nil, err
}
if rawPlaybookRun.Type != app.RunTypeChannelChecklist && rawPlaybookRun.Type != app.RunTypePlaybook {
rawPlaybookRun.Type = app.RunTypePlaybook
}
// When adding a PlaybookRun column #2: add to the SetMap
_, err = s.store.execBuilder(s.store.db, sq.
Insert("IR_Incident").
SetMap(map[string]interface{}{
"ID": rawPlaybookRun.ID,
"Name": rawPlaybookRun.Name,
"Description": rawPlaybookRun.Summary,
"SummaryModifiedAt": rawPlaybookRun.SummaryModifiedAt,
"CommanderUserID": rawPlaybookRun.OwnerUserID,
"ReporterUserID": rawPlaybookRun.ReporterUserID,
"TeamID": rawPlaybookRun.TeamID,
"ChannelID": rawPlaybookRun.ChannelID,
"CreateAt": rawPlaybookRun.CreateAt,
"EndAt": rawPlaybookRun.EndAt,
"PostID": rawPlaybookRun.PostID,
"PlaybookID": rawPlaybookRun.PlaybookID,
"ChecklistsJSON": rawPlaybookRun.ChecklistsJSON,
"ReminderPostID": rawPlaybookRun.ReminderPostID,
"PreviousReminder": rawPlaybookRun.PreviousReminder,
"ReminderMessageTemplate": rawPlaybookRun.ReminderMessageTemplate,
"StatusUpdateEnabled": rawPlaybookRun.StatusUpdateEnabled,
"ReminderTimerDefaultSeconds": rawPlaybookRun.ReminderTimerDefaultSeconds,
"CurrentStatus": rawPlaybookRun.CurrentStatus,
"LastStatusUpdateAt": rawPlaybookRun.LastStatusUpdateAt,
"ConcatenatedInvitedUserIDs": rawPlaybookRun.ConcatenatedInvitedUserIDs,
"ConcatenatedInvitedGroupIDs": rawPlaybookRun.ConcatenatedInvitedGroupIDs,
"DefaultCommanderID": rawPlaybookRun.DefaultOwnerID,
"ConcatenatedBroadcastChannelIDs": rawPlaybookRun.ConcatenatedBroadcastChannelIDs,
"ConcatenatedWebhookOnCreationURLs": rawPlaybookRun.ConcatenatedWebhookOnCreationURLs,
"Retrospective": rawPlaybookRun.Retrospective,
"RetrospectivePublishedAt": rawPlaybookRun.RetrospectivePublishedAt,
"RetrospectiveEnabled": rawPlaybookRun.RetrospectiveEnabled,
"MessageOnJoin": rawPlaybookRun.MessageOnJoin,
"RetrospectiveReminderIntervalSeconds": rawPlaybookRun.RetrospectiveReminderIntervalSeconds,
"RetrospectiveWasCanceled": rawPlaybookRun.RetrospectiveWasCanceled,
"ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs,
"CategoryName": rawPlaybookRun.CategoryName,
"StatusUpdateBroadcastChannelsEnabled": rawPlaybookRun.StatusUpdateBroadcastChannelsEnabled,
"StatusUpdateBroadcastWebhooksEnabled": rawPlaybookRun.StatusUpdateBroadcastWebhooksEnabled,
"CreateChannelMemberOnNewParticipant": rawPlaybookRun.CreateChannelMemberOnNewParticipant,
"RemoveChannelMemberOnRemovedParticipant": rawPlaybookRun.RemoveChannelMemberOnRemovedParticipant,
"RunType": rawPlaybookRun.Type,
// Preserved for backwards compatibility with v1.2
"ActiveStage": 0,
"ActiveStageTitle": "",
"IsActive": true,
"DeleteAt": 0,
}))
if err != nil {
return nil, errors.Wrapf(err, "failed to store new playbook run")
}
return playbookRun, nil
}
// UpdatePlaybookRun updates a playbook run.
func (s *playbookRunStore) UpdatePlaybookRun(playbookRun *app.PlaybookRun) (*app.PlaybookRun, error) {
if playbookRun == nil {
return nil, errors.New("playbook run is nil")
}
if playbookRun.ID == "" {
return nil, errors.New("ID should not be empty")
}
playbookRun = playbookRun.Clone()
playbookRun.Checklists = populateChecklistIDs(playbookRun.Checklists)
rawPlaybookRun, err := toSQLPlaybookRun(*playbookRun)
if err != nil {
return nil, err
}
tx, err := s.store.db.Beginx()
if err != nil {
return nil, errors.Wrap(err, "could not begin transaction")
}
defer s.store.finalizeTransaction(tx)
// When adding a PlaybookRun column #3: add to this SetMap (if it is a column that can be updated)
_, err = s.store.execBuilder(tx, sq.
Update("IR_Incident").
SetMap(map[string]interface{}{
"Name": rawPlaybookRun.Name,
"Description": rawPlaybookRun.Summary,
"SummaryModifiedAt": rawPlaybookRun.SummaryModifiedAt,
"CommanderUserID": rawPlaybookRun.OwnerUserID,
"LastStatusUpdateAt": rawPlaybookRun.LastStatusUpdateAt,
"ChecklistsJSON": rawPlaybookRun.ChecklistsJSON,
"ReminderPostID": rawPlaybookRun.ReminderPostID,
"PreviousReminder": rawPlaybookRun.PreviousReminder,
"ConcatenatedInvitedUserIDs": rawPlaybookRun.ConcatenatedInvitedUserIDs,
"ConcatenatedInvitedGroupIDs": rawPlaybookRun.ConcatenatedInvitedGroupIDs,
"DefaultCommanderID": rawPlaybookRun.DefaultOwnerID,
"ConcatenatedBroadcastChannelIDs": rawPlaybookRun.ConcatenatedBroadcastChannelIDs,
"ConcatenatedWebhookOnCreationURLs": rawPlaybookRun.ConcatenatedWebhookOnCreationURLs,
"Retrospective": rawPlaybookRun.Retrospective,
"RetrospectivePublishedAt": rawPlaybookRun.RetrospectivePublishedAt,
"MessageOnJoin": rawPlaybookRun.MessageOnJoin,
"RetrospectiveReminderIntervalSeconds": rawPlaybookRun.RetrospectiveReminderIntervalSeconds,
"RetrospectiveWasCanceled": rawPlaybookRun.RetrospectiveWasCanceled,
"ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs,
"StatusUpdateBroadcastChannelsEnabled": rawPlaybookRun.StatusUpdateBroadcastChannelsEnabled,
"StatusUpdateBroadcastWebhooksEnabled": rawPlaybookRun.StatusUpdateBroadcastWebhooksEnabled,
"StatusUpdateEnabled": rawPlaybookRun.StatusUpdateEnabled,
"CreateChannelMemberOnNewParticipant": rawPlaybookRun.CreateChannelMemberOnNewParticipant,
"RemoveChannelMemberOnRemovedParticipant": rawPlaybookRun.RemoveChannelMemberOnRemovedParticipant,
"RunType": rawPlaybookRun.Type,
}).
Where(sq.Eq{"ID": rawPlaybookRun.ID}))
if err != nil {
return nil, errors.Wrapf(err, "failed to update playbook run with id '%s'", rawPlaybookRun.ID)
}
if err = s.updateRunMetrics(tx, rawPlaybookRun.PlaybookRun); err != nil {
return nil, errors.Wrapf(err, "failed to update playbook run metrics for run with id '%s'", rawPlaybookRun.PlaybookRun.ID)
}
if err = tx.Commit(); err != nil {
return nil, errors.Wrap(err, "could not commit transaction")
}
return playbookRun, nil
}
func (s *playbookRunStore) UpdateStatus(statusPost *app.SQLStatusPost) error {
if statusPost == nil {
return errors.New("status post is nil")
}
if statusPost.PlaybookRunID == "" {
return errors.New("needs playbook run ID")
}
if statusPost.PostID == "" {
return errors.New("needs post ID")
}
if _, err := s.store.execBuilder(s.store.db, sq.
Insert("IR_StatusPosts").
SetMap(map[string]interface{}{
"IncidentID": statusPost.PlaybookRunID,
"PostID": statusPost.PostID,
})); err != nil {
return errors.Wrap(err, "failed to add new status post")
}
return nil
}
func (s *playbookRunStore) FinishPlaybookRun(playbookRunID string, endAt int64) error {
if _, err := s.store.execBuilder(s.store.db, sq.
Update("IR_Incident").
SetMap(map[string]interface{}{
"CurrentStatus": app.StatusFinished,
"EndAt": endAt,
}).
Where(sq.Eq{"ID": playbookRunID}),
); err != nil {
return errors.Wrapf(err, "failed to finish run for id '%s'", playbookRunID)
}
return nil
}
func (s *playbookRunStore) RestorePlaybookRun(playbookRunID string, restoredAt int64) error {
if _, err := s.store.execBuilder(s.store.db, sq.
Update("IR_Incident").
SetMap(map[string]interface{}{
"CurrentStatus": app.StatusInProgress,
"EndAt": 0,
"LastStatusUpdateAt": restoredAt,
}).
Where(sq.Eq{"ID": playbookRunID})); err != nil {
return errors.Wrapf(err, "failed to restore run for id '%s'", playbookRunID)
}
return nil
}
// CreateTimelineEvent creates the timeline event
func (s *playbookRunStore) CreateTimelineEvent(event *app.TimelineEvent) (*app.TimelineEvent, error) {
if event.PlaybookRunID == "" {
return nil, errors.New("needs playbook run ID")
}
if event.EventType == "" {
return nil, errors.New("needs event type")
}
if event.CreateAt == 0 {
event.CreateAt = model.GetMillis()
}
event.ID = model.NewId()
eventType := string(event.EventType)
if event.EventType == app.OwnerChanged {
eventType = legacyEventTypeCommanderChanged
}
_, err := s.store.execBuilder(s.store.db, sq.
Insert("IR_TimelineEvent").
SetMap(map[string]interface{}{
"ID": event.ID,
"IncidentID": event.PlaybookRunID,
"CreateAt": event.CreateAt,
"DeleteAt": event.DeleteAt,
"EventAt": event.EventAt,
"EventType": eventType,
"Summary": event.Summary,
"Details": event.Details,
"PostID": event.PostID,
"SubjectUserID": event.SubjectUserID,
"CreatorUserID": event.CreatorUserID,
}))
if err != nil {
return nil, errors.Wrap(err, "failed to insert timeline event")
}
return event, nil
}
// UpdateTimelineEvent updates (or inserts) the timeline event
func (s *playbookRunStore) UpdateTimelineEvent(event *app.TimelineEvent) error {
if event.ID == "" {
return errors.New("needs event ID")
}
if event.PlaybookRunID == "" {
return errors.New("needs playbook run ID")
}
if event.EventType == "" {
return errors.New("needs event type")
}
eventType := string(event.EventType)
if event.EventType == app.OwnerChanged {
eventType = legacyEventTypeCommanderChanged
}
_, err := s.store.execBuilder(s.store.db, sq.
Update("IR_TimelineEvent").
SetMap(map[string]interface{}{
"IncidentID": event.PlaybookRunID,
"CreateAt": event.CreateAt,
"DeleteAt": event.DeleteAt,
"EventAt": event.EventAt,
"EventType": eventType,
"Summary": event.Summary,
"Details": event.Details,
"PostID": event.PostID,
"SubjectUserID": event.SubjectUserID,
"CreatorUserID": event.CreatorUserID,
}).
Where(sq.Eq{"ID": event.ID}))
if err != nil {
return errors.Wrap(err, "failed to update timeline event")
}
return nil
}
// GetPlaybookRun gets a playbook run by ID.
func (s *playbookRunStore) GetPlaybookRun(playbookRunID string) (*app.PlaybookRun, error) {
if playbookRunID == "" {
return nil, errors.New("ID cannot be empty")
}
tx, err := s.store.db.Beginx()
if err != nil {
return nil, errors.Wrap(err, "could not begin transaction")
}
defer s.store.finalizeTransaction(tx)
var rawPlaybookRun sqlPlaybookRun
err = s.store.getBuilder(tx, &rawPlaybookRun, s.playbookRunSelect.Where(sq.Eq{"i.ID": playbookRunID}))
if err == sql.ErrNoRows {
return nil, errors.Wrapf(app.ErrNotFound, "playbook run with id '%s' does not exist", playbookRunID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get playbook run by id '%s'", playbookRunID)
}
playbookRun, err := s.toPlaybookRun(rawPlaybookRun)
if err != nil {
return nil, err
}
var statusPosts playbookRunStatusPosts
postInfoSelect := s.statusPostsSelect.
Where(sq.Eq{"sp.IncidentID": playbookRunID}).
OrderBy("p.CreateAt")
err = s.store.selectBuilder(tx, &statusPosts, postInfoSelect)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get playbook run status posts for playbook run with id '%s'", playbookRunID)
}
timelineEvents, err := s.getTimelineEventsForPlaybookRun(tx, []string{playbookRunID})
if err != nil {
return nil, err
}
var metricsData []app.RunMetricData
err = s.store.selectBuilder(tx, &metricsData, s.metricsDataSelectSingleRun.
Where(sq.Eq{"IncidentID": playbookRunID}).
OrderBy("MetricConfigID")) // Entirely for consistency for the tests)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get metrics data for run with id `%s`", playbookRunID)
}
if err = tx.Commit(); err != nil {
return nil, errors.Wrap(err, "could not commit transaction")
}
for _, p := range statusPosts {
playbookRun.StatusPosts = append(playbookRun.StatusPosts, p.StatusPost)
}
playbookRun.TimelineEvents = append(playbookRun.TimelineEvents, timelineEvents...)
playbookRun.MetricsData = metricsData
return playbookRun, nil
}
func (s *playbookRunStore) getTimelineEventsForPlaybookRun(q sqlx.Queryer, playbookRunIDs []string) ([]app.TimelineEvent, error) {
var timelineEvents []app.TimelineEvent
timelineEventsSelect := s.timelineEventsSelect.
OrderBy("te.EventAt ASC").
Where(sq.And{sq.Eq{"te.IncidentID": playbookRunIDs}, sq.Eq{"te.DeleteAt": 0}})
err := s.store.selectBuilder(q, &timelineEvents, timelineEventsSelect)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to get timelineEvents")
}
return timelineEvents, nil
}
func (s *playbookRunStore) getMetricsForPlaybookRun(q sqlx.Queryer, playbookRunIDs []string) ([]sqlRunMetricData, error) {
var metricsData []sqlRunMetricData
sqlMetricsDataSelect := s.sqlMetricsDataSelectMultipleRuns.
Where(sq.Eq{"IncidentID": playbookRunIDs})
err := s.store.selectBuilder(q, &metricsData, sqlMetricsDataSelect)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to get metricsData")
}
return metricsData, nil
}
// GetTimelineEvent returns the timeline event by id for the given playbook run.
func (s *playbookRunStore) GetTimelineEvent(playbookRunID, eventID string) (*app.TimelineEvent, error) {
var event app.TimelineEvent
timelineEventSelect := s.timelineEventsSelect.
Where(sq.And{sq.Eq{"te.IncidentID": playbookRunID}, sq.Eq{"te.ID": eventID}})
err := s.store.getBuilder(s.store.db, &event, timelineEventSelect)
if err == sql.ErrNoRows {
return nil, errors.Wrapf(app.ErrNotFound, "timeline event with id (%s) does not exist for playbook run with id (%s)", eventID, playbookRunID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get timeline event with id (%s) for playbook run with id (%s)", eventID, playbookRunID)
}
return &event, nil
}
// GetPlaybookRunIDsForChannel gets the playbook run IDs list associated with the given channel ID.
func (s *playbookRunStore) GetPlaybookRunIDsForChannel(channelID string) ([]string, error) {
query := s.queryBuilder.
Select("i.ID").
From("IR_Incident i").
Where(sq.Eq{"i.ChannelID": channelID}).
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}).
OrderBy("i.CreateAt DESC").
OrderBy("i.ID")
var ids []string
err := s.store.selectBuilder(s.store.db, &ids, query)
if err == sql.ErrNoRows || len(ids) == 0 {
return nil, errors.Wrapf(app.ErrNotFound, "channel with id (%s) does not have a playbook run", channelID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get playbook run by channelID '%s'", channelID)
}
return ids, nil
}
// GetHistoricalPlaybookRunParticipantsCount returns the count of all members of a playbook run's channel
// since the beginning of the playbook run, excluding bots.
func (s *playbookRunStore) GetHistoricalPlaybookRunParticipantsCount(channelID string) (int64, error) {
query := s.queryBuilder.
Select("COUNT(DISTINCT cmh.UserId)").
From("ChannelMemberHistory AS cmh").
Where(sq.Eq{"cmh.ChannelId": channelID}).
Where(sq.Expr("cmh.UserId NOT IN (SELECT UserId FROM Bots)"))
var numParticipants int64
err := s.store.getBuilder(s.store.db, &numParticipants, query)
if err != nil {
return 0, errors.Wrap(err, "failed to query database")
}
return numParticipants, nil
}
// GetOwners returns the owners of the playbook runs selected by options
func (s *playbookRunStore) GetOwners(requesterInfo app.RequesterInfo, options app.PlaybookRunFilterOptions) ([]app.OwnerInfo, error) {
permissionsExpr := s.buildPermissionsExpr(requesterInfo)
teamLimitExpr := buildTeamLimitExpr(requesterInfo, options.TeamID, "i")
// At the moment, the options only includes teamID
query := s.queryBuilder.
Select("DISTINCT u.Id AS UserID", "u.Username", "u.FirstName", "u.LastName", "u.Nickname").
From("IR_Incident AS i").
Join("Users AS u ON i.CommanderUserID = u.Id").
Where(teamLimitExpr).
Where(permissionsExpr)
var owners []app.OwnerInfo
err := s.store.selectBuilder(s.store.db, &owners, query)
if err != nil {
return nil, errors.Wrap(err, "failed to query database")
}
return owners, nil
}
// NukeDB removes all playbook run related data.
func (s *playbookRunStore) NukeDB() (err error) {
tx, err := s.store.db.Beginx()
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer s.store.finalizeTransaction(tx)
if _, err := tx.Exec("DROP TABLE IF EXISTS IR_Metric, IR_MetricConfig, IR_PlaybookMember, IR_Run_Participants, IR_PlaybookAutoFollow, IR_StatusPosts, IR_TimelineEvent, IR_Incident, IR_Playbook, IR_System"); err != nil {
return errors.Wrap(err, "could not delete all IR tables")
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "could not commit")
}
return s.store.RunMigrations()
}
func (s *playbookRunStore) ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error {
updateQuery := s.queryBuilder.Update("IR_Incident").
Where(sq.Eq{"ID": playbookRunID}).
Set("CreateAt", model.GetMillisForTime(creationTimestamp))
sqlResult, err := s.store.execBuilder(s.store.db, updateQuery)
if err != nil {
return errors.Wrapf(err, "unable to execute the update query")
}
numRows, err := sqlResult.RowsAffected()
if err != nil {
return errors.Wrapf(err, "unable to check how many rows were updated")
}
if numRows == 0 {
return app.ErrNotFound
}
return nil
}
func (s *playbookRunStore) GetBroadcastChannelIDsToRootIDs(playbookRunID string) (map[string]string, error) {
var retAsJSON string
query := s.store.builder.Select("COALESCE(ChannelIDToRootID, '')").
From("IR_Incident").
Where(sq.Eq{"ID": playbookRunID})
err := s.store.getBuilder(s.store.db, &retAsJSON, query)
if err == sql.ErrNoRows {
return nil, errors.Wrapf(app.ErrNotFound, "could not find playbook with id '%s'", playbookRunID)
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get channelID to rootID map for playbookRunID '%s'", playbookRunID)
}
ret := make(map[string]string)
if retAsJSON == "" {
return ret, nil
}
if err := json.Unmarshal([]byte(retAsJSON), &ret); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal channelID to rootID map for playbookRunID: '%s'", playbookRunID)
}
return ret, nil
}
func (s *playbookRunStore) SetBroadcastChannelIDsToRootID(playbookRunID string, channelIDsToRootIDs map[string]string) error {
data, err := json.Marshal(channelIDsToRootIDs)
if err != nil {
return errors.Wrap(err, "failed to marshal channelIDsToRootIDs map")
}
_, err = s.store.execBuilder(s.store.db,
sq.Update("IR_Incident").
Set("ChannelIDToRootID", data).
Where(sq.Eq{"ID": playbookRunID}))
if err != nil {
return errors.Wrapf(err, "failed to set ChannelIDsToRootID column for playbookRunID '%s'", playbookRunID)
}
return nil
}
func (s *playbookRunStore) buildPermissionsExpr(info app.RequesterInfo) sq.Sqlizer {
if info.IsAdmin {
return nil
}
// Guests must be participants
if info.IsGuest {
return sq.Expr(`
EXISTS(SELECT 1
FROM IR_Run_Participants as rp
WHERE rp.IncidentID = i.ID
AND rp.UserId = ?
AND rp.IsParticipant = true
)
`, info.UserID)
}
// 1. Is the user a participant of the run?
// 2. Is the playbook open to everyone on the team, or is the user a member of the playbook?
// If so, they have permission to view the run.
return sq.Expr(`
((
EXISTS (
SELECT 1
FROM IR_Run_Participants as rp
WHERE rp.IncidentID = i.ID
AND rp.UserId = ?
AND rp.IsParticipant = true
)
) OR (
(SELECT Public
FROM IR_Playbook
WHERE ID = i.PlaybookID)
OR EXISTS(
SELECT 1
FROM IR_PlaybookMember
WHERE PlaybookID = i.PlaybookID
AND MemberID = ?)
))`, info.UserID, info.UserID)
}
func buildTeamLimitExpr(info app.RequesterInfo, teamID, tableName string) sq.Sqlizer {
filterToSelectedTeam := sq.Eq{fmt.Sprintf("%s.TeamID", tableName): teamID}
onlyTeamsUserIsAMember := sq.Expr(fmt.Sprintf(`
EXISTS(SELECT 1
FROM TeamMembers as tm
WHERE tm.TeamId = %s.TeamID
AND tm.DeleteAt = 0
AND tm.UserId = ?)
`, tableName), info.UserID)
if info.IsAdmin {
if teamID != "" {
return filterToSelectedTeam
}
return nil
}
if teamID != "" {
return sq.And{
filterToSelectedTeam,
onlyTeamsUserIsAMember,
}
}
return onlyTeamsUserIsAMember
}
func (s *playbookRunStore) toPlaybookRun(rawPlaybookRun sqlPlaybookRun) (*app.PlaybookRun, error) {
playbookRun := rawPlaybookRun.PlaybookRun
if err := json.Unmarshal(rawPlaybookRun.ChecklistsJSON, &playbookRun.Checklists); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", rawPlaybookRun.ID)
}
playbookRun.InvitedUserIDs = []string(nil)
if rawPlaybookRun.ConcatenatedInvitedUserIDs != "" {
playbookRun.InvitedUserIDs = strings.Split(rawPlaybookRun.ConcatenatedInvitedUserIDs, ",")
}
playbookRun.InvitedGroupIDs = []string(nil)
if rawPlaybookRun.ConcatenatedInvitedGroupIDs != "" {
playbookRun.InvitedGroupIDs = strings.Split(rawPlaybookRun.ConcatenatedInvitedGroupIDs, ",")
}
playbookRun.ParticipantIDs = []string(nil)
if rawPlaybookRun.ConcatenatedParticipantIDs != "" {
playbookRun.ParticipantIDs = strings.Split(rawPlaybookRun.ConcatenatedParticipantIDs, ",")
}
playbookRun.BroadcastChannelIDs = []string(nil)
if rawPlaybookRun.ConcatenatedBroadcastChannelIDs != "" {
playbookRun.BroadcastChannelIDs = strings.Split(rawPlaybookRun.ConcatenatedBroadcastChannelIDs, ",")
}
playbookRun.WebhookOnCreationURLs = []string(nil)
if rawPlaybookRun.ConcatenatedWebhookOnCreationURLs != "" {
playbookRun.WebhookOnCreationURLs = strings.Split(rawPlaybookRun.ConcatenatedWebhookOnCreationURLs, ",")
}
playbookRun.WebhookOnStatusUpdateURLs = []string(nil)
if rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs != "" {
playbookRun.WebhookOnStatusUpdateURLs = strings.Split(rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs, ",")
}
// force false broadcast-on-status-update flags if they have no destinations
if len(playbookRun.WebhookOnStatusUpdateURLs) == 0 {
playbookRun.StatusUpdateBroadcastWebhooksEnabled = false
}
if len(playbookRun.BroadcastChannelIDs) == 0 {
playbookRun.StatusUpdateBroadcastChannelsEnabled = false
}
return &playbookRun, nil
}
// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID
func (s *playbookRunStore) GetRunsWithAssignedTasks(userID string) ([]app.AssignedRun, error) {
var raw []struct {
app.AssignedRun
ChecklistsJSON json.RawMessage
}
query := s.store.builder.Select("i.ID AS PlaybookRunID", "i.Name", "i.ChecklistsJSON AS ChecklistsJSON").
From("IR_Incident AS i").
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}).
OrderBy("i.Name")
if s.store.db.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Like{"i.ChecklistsJSON": fmt.Sprintf("%%\"%s\"%%", userID)})
} else {
query = query.Where(sq.Like{"i.ChecklistsJSON::text": fmt.Sprintf("%%\"%s\"%%", userID)})
}
if err := s.store.selectBuilder(s.store.db, &raw, query); err != nil {
return nil, errors.Wrap(err, "failed to query for assigned tasks")
}
var ret []app.AssignedRun
for _, rawItem := range raw {
run := rawItem.AssignedRun
var checklists []app.Checklist
err := json.Unmarshal(rawItem.ChecklistsJSON, &checklists)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", rawItem.PlaybookRunID)
}
// Check which item(s) have this user as an assignee and add them to the list
for _, checklist := range checklists {
for _, item := range checklist.Items {
if item.AssigneeID == userID && item.State == "" {
task := app.AssignedTask{
ChecklistID: checklist.ID,
ChecklistTitle: checklist.Title,
ChecklistItem: item,
}
run.Tasks = append(run.Tasks, task)
}
}
}
if len(run.Tasks) > 0 {
ret = append(ret, run)
}
}
return ret, nil
}
// GetParticipatingRuns returns the list of active runs with userID as a participant
func (s *playbookRunStore) GetParticipatingRuns(userID string) ([]app.RunLink, error) {
membershipClause := s.queryBuilder.
Select("1").
Prefix("EXISTS(").
From("IR_Run_Participants AS rp").
Where("rp.IncidentID = i.ID").
Where(sq.Eq{"rp.UserId": userID}).
Where(sq.Eq{"rp.IsParticipant": true}).
Suffix(")")
query := s.store.builder.
Select("i.ID AS PlaybookRunID", "i.Name").
From("IR_Incident AS i").
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}).
Where(membershipClause).
OrderBy("i.Name")
var ret []app.RunLink
if err := s.store.selectBuilder(s.store.db, &ret, query); err != nil {
return nil, errors.Wrap(err, "failed to query for active runs")
}
return ret, nil
}
// GetOverdueUpdateRuns returns runs owned by userID and that have overdue status updates.
func (s *playbookRunStore) GetOverdueUpdateRuns(userID string) ([]app.RunLink, error) {
// only notify if the user is still a participant
// in other words: don't notify the commander of an overdue run if they have left the run
membershipClause := s.queryBuilder.
Select("1").
Prefix("EXISTS(").
From("IR_Run_Participants AS rp").
Where("rp.IncidentID = i.ID").
Where(sq.Eq{"rp.UserId": userID}).
Where(sq.Eq{"rp.IsParticipant": true}).
Suffix(")")
query := s.store.builder.
Select("i.ID AS PlaybookRunID", "i.Name").
From("IR_Incident AS i").
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}).
Where(sq.NotEq{"i.PreviousReminder": 0}).
Where(sq.Eq{"i.CommanderUserId": userID}).
Where(sq.Eq{"i.StatusUpdateEnabled": true}).
Where(membershipClause).
OrderBy("i.Name")
if s.store.db.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Expr("(i.PreviousReminder / 1e6 + i.LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)"))
} else {
query = query.Where(sq.Expr("(i.PreviousReminder / 1e6 + i.LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)"))
}
var ret []app.RunLink
if err := s.store.selectBuilder(s.store.db, &ret, query); err != nil {
return nil, errors.Wrap(err, "failed to query for active runs")
}
return ret, nil
}
func (s *playbookRunStore) Follow(playbookRunID, userID string) error {
return s.updateFollowing(playbookRunID, userID, true)
}
func (s *playbookRunStore) Unfollow(playbookRunID, userID string) error {
return s.updateFollowing(playbookRunID, userID, false)
}
func (s *playbookRunStore) updateFollowing(playbookRunID, userID string, isFollowing bool) error {
var err error
if s.store.db.DriverName() == model.DatabaseDriverMysql {
_, err = s.store.execBuilder(s.store.db, sq.
Insert("IR_Run_Participants").
Columns("IncidentID", "UserID", "IsFollower").
Values(playbookRunID, userID, isFollowing).
Suffix("ON DUPLICATE KEY UPDATE IsFollower = ?", isFollowing))
} else {
_, err = s.store.execBuilder(s.store.db, sq.
Insert("IR_Run_Participants").
Columns("IncidentID", "UserID", "IsFollower").
Values(playbookRunID, userID, isFollowing).
Suffix("ON CONFLICT (IncidentID,UserID) DO UPDATE SET IsFollower = ?", isFollowing))
}
if err != nil {
return errors.Wrapf(err, "failed to upsert follower '%s' for run '%s'", userID, playbookRunID)
}
return nil
}
func (s *playbookRunStore) GetFollowers(playbookRunID string) ([]string, error) {
query := s.queryBuilder.
Select("UserID").
From("IR_Run_Participants").
Where(sq.And{sq.Eq{"IsFollower": true}, sq.Eq{"IncidentID": playbookRunID}})
var followers []string
err := s.store.selectBuilder(s.store.db, &followers, query)
if err == sql.ErrNoRows {
return []string{}, nil
} else if err != nil {
return nil, errors.Wrapf(err, "failed to get followers for run '%s'", playbookRunID)
}
return followers, nil
}
// Get number of active runs.
func (s *playbookRunStore) GetRunsActiveTotal() (int64, error) {
var count int64
query := s.store.builder.
Select("COUNT(*)").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusInProgress})
if err := s.store.getBuilder(s.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count active runs'")
}
return count, nil
}
// GetOverdueUpdateRunsTotal returns number of runs that have overdue status updates.
func (s *playbookRunStore) GetOverdueUpdateRunsTotal() (int64, error) {
query := s.store.builder.
Select("COUNT(*)").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusInProgress}).
Where(sq.Eq{"StatusUpdateEnabled": true}).
Where(sq.NotEq{"PreviousReminder": 0})
if s.store.db.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)"))
} else {
query = query.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)"))
}
var count int64
if err := s.store.getBuilder(s.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count active runs that have overdue status updates")
}
return count, nil
}
// GetOverdueRetroRunsTotal returns the number of completed runs without retro and with reminder
func (s *playbookRunStore) GetOverdueRetroRunsTotal() (int64, error) {
query := s.store.builder.
Select("COUNT(*)").
From("IR_Incident").
Where(sq.Eq{"CurrentStatus": app.StatusFinished}).
Where(sq.Eq{"RetrospectiveEnabled": true}).
Where(sq.Eq{"RetrospectivePublishedAt": 0}).
Where(sq.NotEq{"RetrospectiveReminderIntervalSeconds": 0})
var count int64
if err := s.store.getBuilder(s.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count finished runs without retro")
}
return count, nil
}
// GetFollowersActiveTotal returns total number of active followers, including duplicates
// if a user is following more than one run, it will be counted multiple times
func (s *playbookRunStore) GetFollowersActiveTotal() (int64, error) {
var count int64
query := s.store.builder.
Select("COUNT(*)").
From("IR_Run_Participants as rp").
Join("IR_Incident AS i ON (i.ID = rp.IncidentID)").
Where(sq.Eq{"rp.IsFollower": true}).
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress})
if err := s.store.getBuilder(s.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count active followers'")
}
return count, nil
}
// GetParticipantsActiveTotal returns number of active participants
// if a user is a participant in more than one run they will be counted multiple times
func (s *playbookRunStore) GetParticipantsActiveTotal() (int64, error) {
var count int64
query := s.store.builder.
Select("COUNT(*)").
From("IR_Run_Participants as rp").
Join("IR_Incident AS i ON i.ID = rp.IncidentID").
Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}).
Where(sq.Eq{"rp.IsParticipant": true}).
Where(sq.Expr("rp.UserId NOT IN (SELECT UserId FROM Bots)"))
if err := s.store.getBuilder(s.store.db, &count, query); err != nil {
return 0, errors.Wrap(err, "failed to count active participants")
}
return count, nil
}
// GetSchemeRolesForChannel scheme role ids for the channel
func (s *playbookRunStore) GetSchemeRolesForChannel(channelID string) (string, string, string, error) {
query := s.queryBuilder.
Select("COALESCE(s.DefaultChannelGuestRole, 'channel_guest') DefaultChannelGuestRole",
"COALESCE(s.DefaultChannelUserRole, 'channel_user') DefaultChannelUserRole",
"COALESCE(s.DefaultChannelAdminRole, 'channel_admin') DefaultChannelAdminRole").
From("Schemes as s").
Join("Channels AS c ON (c.SchemeId = s.Id)").
Where(sq.Eq{"c.Id": channelID})
var scheme model.Scheme
err := s.store.getBuilder(s.store.db, &scheme, query)
return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, err
}
// GetSchemeRolesForTeam scheme role ids for the team
func (s *playbookRunStore) GetSchemeRolesForTeam(teamID string) (string, string, string, error) {
query := s.queryBuilder.
Select("COALESCE(s.DefaultChannelGuestRole, 'channel_guest') DefaultChannelGuestRole",
"COALESCE(s.DefaultChannelUserRole, 'channel_user') DefaultChannelUserRole",
"COALESCE(s.DefaultChannelAdminRole, 'channel_admin') DefaultChannelAdminRole").
From("Schemes as s").
Join("Teams AS t ON (t.SchemeId = s.Id)").
Where(sq.Eq{"t.Id": teamID})
var scheme model.Scheme
err := s.store.getBuilder(s.store.db, &scheme, query)
return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, err
}
// updateRunMetrics updates run metrics values.
func (s *playbookRunStore) updateRunMetrics(q queryExecer, playbookRun app.PlaybookRun) error {
if len(playbookRun.MetricsData) == 0 {
return nil
}
//retrieve metrics configurations ids for this run to validate received data
query := s.queryBuilder.
Select("ID").
From("IR_MetricConfig").
Where(sq.Eq{"PlaybookID": playbookRun.PlaybookID}).
Where(sq.Eq{"DeleteAt": 0})
var metricsConfigsIDs []string
err := s.store.selectBuilder(q, &metricsConfigsIDs, query)
if err != nil {
return errors.Wrapf(err, "failed to get metric configs ids for playbook '%s'", playbookRun.PlaybookID)
}
validIDs := make(map[string]bool)
for _, id := range metricsConfigsIDs {
validIDs[id] = true
}
retrospectivePublished := !playbookRun.RetrospectiveWasCanceled && playbookRun.RetrospectivePublishedAt > 0
for _, m := range playbookRun.MetricsData {
//do not store if id is not in run's playbook configuration
if !validIDs[m.MetricConfigID] {
continue
}
if s.store.db.DriverName() == model.DatabaseDriverMysql {
_, err = s.store.execBuilder(q, sq.
Insert("IR_Metric").
Columns("IncidentID", "MetricConfigID", "Value", "Published").
Values(playbookRun.ID, m.MetricConfigID, m.Value, retrospectivePublished).
Suffix("ON DUPLICATE KEY UPDATE Value = ?, Published = ?", m.Value, retrospectivePublished))
} else {
_, err = s.store.execBuilder(q, sq.
Insert("IR_Metric").
Columns("IncidentID", "MetricConfigID", "Value", "Published").
Values(playbookRun.ID, m.MetricConfigID, m.Value, retrospectivePublished).
Suffix("ON CONFLICT (IncidentID,MetricConfigID) DO UPDATE SET Value = ?, Published = ?", m.Value, retrospectivePublished))
}
if err != nil {
return errors.Wrapf(err, "failed to upsert metric value '%s'", m.MetricConfigID)
}
}
return nil
}
func (s *playbookRunStore) AddParticipants(playbookRunID string, userIDs []string) error {
return s.updateParticipating(playbookRunID, userIDs, true)
}
func (s *playbookRunStore) RemoveParticipants(playbookRunID string, userIDs []string) error {
return s.updateParticipating(playbookRunID, userIDs, false)
}
func (s *playbookRunStore) updateParticipating(playbookRunID string, userIDs []string, isParticipating bool) error {
if len(userIDs) == 0 {
return nil
}
query := sq.
Insert("IR_Run_Participants").
Columns("IncidentID", "UserID", "IsParticipant")
for _, userID := range userIDs {
query = query.Values(playbookRunID, userID, isParticipating)
}
var err error
if s.store.db.DriverName() == model.DatabaseDriverMysql {
_, err = s.store.execBuilder(
s.store.db,
query.Suffix("ON DUPLICATE KEY UPDATE IsParticipant = ?", isParticipating),
)
} else {
_, err = s.store.execBuilder(
s.store.db,
query.Suffix("ON CONFLICT (IncidentID,UserID) DO UPDATE SET IsParticipant = ?", isParticipating),
)
}
if err != nil {
return errors.Wrapf(err, "failed to upsert participants '%+v' for run '%s'", userIDs, playbookRunID)
}
return nil
}
// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following
func (s *playbookRunStore) GetPlaybookRunIDsForUser(userID string) ([]string, error) {
requesterInfo := app.RequesterInfo{UserID: userID}
permissionsExpr := s.buildPermissionsExpr(requesterInfo)
teamLimitExpr := buildTeamLimitExpr(requesterInfo, "", "i")
query := s.store.builder.
Select("i.ID").
From("IR_Incident AS i").
Join("IR_Run_Participants AS p ON p.IncidentID = i.ID").
Where(sq.Or{sq.Eq{"p.IsParticipant": true}, sq.Eq{"p.IsFollower": true}}).
Where(sq.Eq{"p.UserID": strings.ToLower(userID)}).
Where(teamLimitExpr).
Where(permissionsExpr)
var ids []string
if err := s.store.selectBuilder(s.store.db, &ids, query); err != nil {
return nil, errors.Wrap(err, "failed to query for playbook runs")
}
return ids, nil
}
// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs.
func (s *playbookRunStore) GetRunMetadataByIDs(runIDs []string) ([]app.RunMetadata, error) {
var runs []app.RunMetadata
query := s.store.builder.
Select("ID", "TeamID", "Name").
From("IR_Incident").
Where(sq.Eq{"ID": runIDs})
if err := s.store.selectBuilder(s.store.db, &runs, query); err != nil {
return nil, errors.Wrap(err, "failed to query playbook run by runIDs")
}
runsMap := make(map[string]app.RunMetadata, len(runs))
for _, run := range runs {
runsMap[run.ID] = run
}
orderedRuns := make([]app.RunMetadata, len(runIDs))
for i, runID := range runIDs {
orderedRuns[i] = runsMap[runID]
}
return orderedRuns, nil
}
// GetTaskAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs
func (s *playbookRunStore) GetTaskAsTopicMetadataByIDs(taskIDs []string) ([]app.TopicMetadata, error) {
tasksMap := make(map[string]app.TopicMetadata, len(taskIDs))
for _, taskID := range taskIDs {
var runsInDB []struct {
app.TopicMetadata
ChecklistsJSON json.RawMessage
}
query := s.store.builder.
Select("ID AS RunID", "TeamID", "ChecklistsJSON").
From("IR_Incident")
if s.store.db.DriverName() == model.DatabaseDriverMysql {
query = query.Where(sq.Like{"ChecklistsJSON": fmt.Sprintf("%%\"%s\"%%", taskID)})
} else {
query = query.Where(sq.Like{"ChecklistsJSON::text": fmt.Sprintf("%%\"%s\"%%", taskID)})
}
if err := s.store.selectBuilder(s.store.db, &runsInDB, query); err != nil {
return nil, errors.Wrapf(err, "failed to query playbook run by taskID - %s", taskID)
}
for _, run := range runsInDB {
var checklists []app.Checklist
err := json.Unmarshal(run.ChecklistsJSON, &checklists)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", run.RunID)
}
if isTaskInChecklists(checklists, taskID) {
tasksMap[taskID] = app.TopicMetadata{
ID: taskID,
RunID: run.RunID,
TeamID: run.TeamID,
}
}
}
}
tasks := make([]app.TopicMetadata, len(taskIDs))
for i, taskID := range taskIDs {
tasks[i] = tasksMap[taskID]
}
return tasks, nil
}
func isTaskInChecklists(checklists []app.Checklist, taskID string) bool {
for _, checklist := range checklists {
for _, item := range checklist.Items {
if item.ID == taskID {
return true
}
}
}
return false
}
// GetStatusAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs
func (s *playbookRunStore) GetStatusAsTopicMetadataByIDs(statusIDs []string) ([]app.TopicMetadata, error) {
var statuses []app.TopicMetadata
query := s.store.builder.
Select("sp.PostID AS ID", "sp.IncidentID AS RunID", "i.TeamID AS TeamID").
From("IR_StatusPosts as sp").
Join("IR_Incident as i ON sp.IncidentID = i.ID").
Where(sq.Eq{"sp.PostID": statusIDs})
if err := s.store.selectBuilder(s.store.db, &statuses, query); err != nil {
return nil, errors.Wrap(err, "failed to query playbook runs by statusIDs")
}
statusesMap := make(map[string]app.TopicMetadata, len(statuses))
for _, status := range statuses {
statusesMap[status.ID] = status
}
orderedStatuses := make([]app.TopicMetadata, len(statusIDs))
for i, statusID := range statusIDs {
orderedStatuses[i] = statusesMap[statusID]
}
return orderedStatuses, nil
}
func (s *playbookRunStore) GraphqlUpdate(id string, setmap map[string]interface{}) error {
if id == "" {
return errors.New("id should not be empty")
}
_, err := s.store.execBuilder(s.store.db, sq.
Update("IR_Incident").
SetMap(setmap).
Where(sq.Eq{"ID": id}))
if err != nil {
return errors.Wrapf(err, "failed to update playbook run with id '%s'", id)
}
return nil
}
func toSQLPlaybookRun(playbookRun app.PlaybookRun) (*sqlPlaybookRun, error) {
checklistsJSON, err := checklistsToJSON(playbookRun.Checklists)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal checklist json for playbook run id '%s'", playbookRun.ID)
}
if len(checklistsJSON) > maxJSONLength {
return nil, errors.Wrapf(errors.New("invalid data"), "checklist json for playbook run id '%s' is too long (max %d)", playbookRun.ID, maxJSONLength)
}
return &sqlPlaybookRun{
PlaybookRun: playbookRun,
ChecklistsJSON: checklistsJSON,
ConcatenatedInvitedUserIDs: strings.Join(playbookRun.InvitedUserIDs, ","),
ConcatenatedInvitedGroupIDs: strings.Join(playbookRun.InvitedGroupIDs, ","),
ConcatenatedBroadcastChannelIDs: strings.Join(playbookRun.BroadcastChannelIDs, ","),
ConcatenatedWebhookOnCreationURLs: strings.Join(playbookRun.WebhookOnCreationURLs, ","),
ConcatenatedWebhookOnStatusUpdateURLs: strings.Join(playbookRun.WebhookOnStatusUpdateURLs, ","),
}, nil
}
// populateChecklistIDs returns a cloned slice with ids entered for checklists and checklist items.
func populateChecklistIDs(checklists []app.Checklist) []app.Checklist {
if len(checklists) == 0 {
return nil
}
newChecklists := make([]app.Checklist, len(checklists))
for i, c := range checklists {
newChecklists[i] = c.Clone()
if newChecklists[i].ID == "" {
newChecklists[i].ID = model.NewId()
}
for j, item := range newChecklists[i].Items {
if item.ID == "" {
newChecklists[i].Items[j].ID = model.NewId()
}
}
}
return newChecklists
}
// A playbook run needs to assign unique ids to its checklist items
func checklistsToJSON(checklists []app.Checklist) (json.RawMessage, error) {
checklistsJSON, err := json.Marshal(checklists)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal checklist json")
}
return checklistsJSON, nil
}
func addStatusPostsToPlaybookRuns(statusIDs playbookRunStatusPosts, playbookRuns []app.PlaybookRun) {
iToPosts := make(map[string][]app.StatusPost)
for _, p := range statusIDs {
iToPosts[p.PlaybookRunID] = append(iToPosts[p.PlaybookRunID], p.StatusPost)
}
for i, playbookRun := range playbookRuns {
playbookRuns[i].StatusPosts = iToPosts[playbookRun.ID]
}
}
func addTimelineEventsToPlaybookRuns(timelineEvents []app.TimelineEvent, playbookRuns []app.PlaybookRun) {
iToTe := make(map[string][]app.TimelineEvent)
for _, te := range timelineEvents {
iToTe[te.PlaybookRunID] = append(iToTe[te.PlaybookRunID], te)
}
for i, playbookRun := range playbookRuns {
playbookRuns[i].TimelineEvents = iToTe[playbookRun.ID]
}
}
func addMetricsToPlaybookRuns(metrics []sqlRunMetricData, playbookRuns []app.PlaybookRun) {
playbookRunToMetrics := make(map[string][]app.RunMetricData)
for _, metric := range metrics {
playbookRunToMetrics[metric.IncidentID] = append(playbookRunToMetrics[metric.IncidentID],
app.RunMetricData{
MetricConfigID: metric.MetricConfigID,
Value: metric.Value,
})
}
for i, run := range playbookRuns {
playbookRuns[i].MetricsData = playbookRunToMetrics[run.ID]
}
}
// queryActiveBetweenTimes will modify the query only if one (or both) of start and end are non-zero.
// If both are non-zero, return the playbook runs active between those two times.
// If start is zero, return the playbook run active before the end (not active after the end).
// If end is zero, return the playbook run active after start.
func queryActiveBetweenTimes(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder {
if start > 0 && end > 0 {
return queryActive(query, start, end)
} else if start > 0 {
return queryActive(query, start, model.GetMillis())
} else if end > 0 {
return queryActive(query, 0, end)
}
// both were zero, don't apply a filter:
return query
}
func queryActive(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder {
return query.Where(
sq.And{
sq.Or{
sq.GtOrEq{"i.EndAt": start},
sq.Eq{"i.EndAt": 0},
},
sq.Lt{"i.CreateAt": end},
})
}
// queryStartedBetweenTimes will modify the query only if one (or both) of start and end are non-zero.
// If both are non-zero, return the playbook runs started between those two times.
// If start is zero, return the playbook run started before the end
// If end is zero, return the playbook run started after start.
func queryStartedBetweenTimes(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder {
if start > 0 && end > 0 {
return queryStarted(query, start, end)
} else if start > 0 {
return queryStarted(query, start, model.GetMillis())
} else if end > 0 {
return queryStarted(query, 0, end)
}
// both were zero, don't apply a filter:
return query
}
func queryStarted(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder {
return query.Where(
sq.And{
sq.GtOrEq{"i.CreateAt": start},
sq.Lt{"i.CreateAt": end},
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/playbooks"
"github.com/mattermost/mattermost-server/v6/model"
)
// StoreAPI is the interface exposing the underlying database, provided by pluginapi
// It is implemented by mattermost-plugin-api/Client.Store, or by the mock StoreAPI.
type StoreAPI interface {
GetMasterDB() (*sql.DB, error)
DriverName() string
}
// KVAPI is the key value store interface for the pluginkv stores.
// It is implemented by mattermost-plugin-api/Client.KV, or by the mock KVAPI.
type KVAPI interface {
Get(key string, out interface{}) error
}
type ConfigurationAPI interface {
GetConfig() *model.Config
}
// PluginAPIClient is the struct combining the interfaces defined above, which is everything
// from pluginapi that the store currently uses.
type PluginAPIClient struct {
Store StoreAPI
KV KVAPI
Configuration ConfigurationAPI
}
// NewClient receives a pluginapi.Client and returns the PluginAPIClient, which is what the
// store will use to access pluginapi.Client.
func NewClient(serviceAdapter playbooks.ServicesAPI) PluginAPIClient {
return PluginAPIClient{
Store: serviceAdapter,
KV: serviceAdapter,
Configuration: serviceAdapter,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"fmt"
"math"
"reflect"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/guregu/null.v4"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
)
type StatsStore struct {
pluginAPI PluginAPIClient
store *SQLStore
}
func NewStatsStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) *StatsStore {
return &StatsStore{
pluginAPI: pluginAPI,
store: sqlStore,
}
}
type StatsFilters struct {
TeamID string
PlaybookID string
}
func applyFilters(query sq.SelectBuilder, filters *StatsFilters) sq.SelectBuilder {
ret := query
if filters.TeamID != "" {
ret = ret.Where(sq.Eq{"i.TeamID": filters.TeamID})
}
if filters.PlaybookID != "" {
ret = ret.Where(sq.Eq{"i.PlaybookID": filters.PlaybookID})
}
return ret
}
func (s *StatsStore) TotalInProgressPlaybookRuns(filters *StatsFilters) int {
query := s.store.builder.
Select("COUNT(i.ID)").
From("IR_Incident as i").
Where("i.EndAt = 0")
query = applyFilters(query, filters)
var total int
if err := s.store.getBuilder(s.store.db, &total, query); err != nil {
logrus.WithError(err).Error("failed to query total in progress playbook runs")
return -1
}
return total
}
// TotalPlaybooks returns the number of playbooks in the server
func (s *StatsStore) TotalPlaybooks() (int, error) {
query := s.store.builder.
Select("COUNT(p.ID)").
From("IR_Playbook as p")
var total int
if err := s.store.getBuilder(s.store.db, &total, query); err != nil {
return 0, errors.Wrap(err, "Error retrieving total playbooks stat")
}
return total, nil
}
// TotalPlaybookRuns returns the number of playbook runs in the server
func (s *StatsStore) TotalPlaybookRuns() (int, error) {
query := s.store.builder.
Select("COUNT(i.ID)").
From("IR_Incident as i")
var total int
if err := s.store.getBuilder(s.store.db, &total, query); err != nil {
return 0, errors.Wrap(err, "Error retrieving total runs stat")
}
return total, nil
}
func (s *StatsStore) TotalActiveParticipants(filters *StatsFilters) int {
query := s.store.builder.
Select("COUNT(DISTINCT rp.UserId)").
From("IR_Run_Participants as rp").
Join("IR_Incident AS i ON i.ID = rp.IncidentID").
Where("i.EndAt = 0").
Where("rp.IsParticipant = true").
Where(sq.Expr("rp.UserId NOT IN (SELECT UserId FROM Bots)"))
query = applyFilters(query, filters)
var total int
if err := s.store.getBuilder(s.store.db, &total, query); err != nil {
logrus.WithError(err).Error("failed to query total active participants")
return -1
}
return total
}
// RunsFinishedBetweenDays are calculated from startDay to endDay (inclusive), where "days"
// are "number of days ago". E.g., for the last 30 days, begin day would be 30 (days ago), end day
// would be 0 (days ago) (up until now).
func (s *StatsStore) RunsFinishedBetweenDays(filters *StatsFilters, startDay, endDay int) int {
dayInMS := int64(86400000)
startInMS := beginningOfTodayMillis() - int64(startDay)*dayInMS
endInMS := endOfTodayMillis() - int64(endDay)*dayInMS
query := s.store.builder.
Select("COUNT(i.Id) as Count").
From("IR_Incident as i").
Where(sq.And{
sq.Expr("i.EndAt > ?", startInMS),
sq.Expr("i.EndAt <= ?", endInMS),
})
query = applyFilters(query, filters)
var total int
if err := s.store.getBuilder(s.store.db, &total, query); err != nil {
logrus.WithError(err).Error("failed to query runs finished between days")
return -1
}
return total
}
// Not efficient. One query per day.
func (s *StatsStore) MovingWindowQueryActive(query sq.SelectBuilder, numDays int) ([]int, error) {
now := model.GetMillis()
dayInMS := int64(86400000)
results := []int{}
for i := 0; i < numDays; i++ {
modifiedQuery := query.Where(
sq.Expr(
`i.CreateAt < ? AND (i.EndAt > ? OR i.EndAt = 0)`,
now-(int64(i)*dayInMS),
now-(int64(i+1)*dayInMS),
),
)
var value int
if err := s.store.getBuilder(s.store.db, &value, modifiedQuery); err != nil {
return nil, err
}
results = append(results, value)
}
return results, nil
}
// RunsStartedPerWeekLastXWeeks returns the number of runs started each week for the last X weeks.
// Returns data in order of oldest week to most recent week.
func (s *StatsStore) RunsStartedPerWeekLastXWeeks(x int, filters *StatsFilters) ([]int, [][]int64) {
day := int64(86400000)
week := day * 7
startOfWeek := beginningOfLastSundayMillis()
endOfWeek := startOfWeek + week - 1
var weeksStartAndEnd [][]int64
q := s.store.builder.Select()
for i := 0; i < x; i++ {
if s.store.db.DriverName() == model.DatabaseDriverMysql {
q = q.Column(`
CAST(
COALESCE(
SUM(
CASE
WHEN i.CreateAt >= ? AND i.CreateAt < ?
THEN 1
ELSE 0
END)
, 0)
AS UNSIGNED)
`, startOfWeek, endOfWeek)
} else {
q = q.Column(`
COALESCE(
SUM(CASE
WHEN i.CreateAt >= ? AND i.CreateAt < ?
THEN 1
ELSE 0
END)
, 0)
`, startOfWeek, endOfWeek)
}
weeksStartAndEnd = append(weeksStartAndEnd, []int64{startOfWeek, endOfWeek})
endOfWeek -= week
startOfWeek -= week
}
q = q.From("IR_Incident as i")
q = applyFilters(q, filters)
counts, err := s.performQueryForXCols(q, x)
if err != nil {
logrus.WithError(err).WithField("x", x).Error("failed to query runs started per week last x weeks")
return []int{}, [][]int64{}
}
reverseSlice(counts)
reverseSlice(weeksStartAndEnd)
return counts, weeksStartAndEnd
}
// ActiveRunsPerDayLastXDays returns the number of active runs per day for the last X days.
// Returns data in order of oldest day to most recent day.
func (s *StatsStore) ActiveRunsPerDayLastXDays(x int, filters *StatsFilters) ([]int, [][]int64) {
startOfDay := beginningOfTodayMillis()
endOfDay := endOfTodayMillis()
day := int64(86400000)
var daysAsStartAndEnd [][]int64
q := s.store.builder.Select()
for i := 0; i < x; i++ {
// a playbook run was active if it was created before the end of the day and ended after the
// start of the day (or still active)
if s.store.db.DriverName() == model.DatabaseDriverMysql {
q = q.Column(`
CAST(
COALESCE(
SUM(
CASE
WHEN (i.EndAt >= ? OR i.EndAt = 0) AND i.CreateAt < ?
THEN 1
ELSE 0
END)
, 0)
AS UNSIGNED)
`, startOfDay, endOfDay)
} else {
q = q.Column(`
COALESCE(
SUM(CASE
WHEN (i.EndAt >= ? OR i.EndAt = 0) AND i.CreateAt < ?
THEN 1
ELSE 0
END)
, 0)
`, startOfDay, endOfDay)
}
daysAsStartAndEnd = append(daysAsStartAndEnd, []int64{startOfDay, endOfDay})
endOfDay -= day
startOfDay -= day
}
q = q.From("IR_Incident as i")
q = applyFilters(q, filters)
counts, err := s.performQueryForXCols(q, x)
if err != nil {
logrus.WithError(err).WithField("x", x).Error("failed to query active runs per day last x days")
return []int{}, [][]int64{}
}
reverseSlice(counts)
reverseSlice(daysAsStartAndEnd)
return counts, daysAsStartAndEnd
}
// ActiveParticipantsPerDayLastXDays returns the number of active participants per day for the last X days.
// Returns data in order of oldest day to most recent day.
func (s *StatsStore) ActiveParticipantsPerDayLastXDays(x int, filters *StatsFilters) ([]int, [][]int64) {
startOfDay := beginningOfTodayMillis()
endOfDay := endOfTodayMillis()
day := int64(86400000)
var daysAsTimes [][]int64
q := s.store.builder.Select()
for i := 0; i < x; i++ {
// COUNT( DISTINCT( CASE: the CASE will return the userId if the row satisfies the conditions,
// therefore COUNT( DISTINCT will return the number of unique userIds
//
// first two lines of the WHEN: a playbook run was active if it was ended after the start of
// the day (or still active) and created before the end of the day
//
// second two lines: a user was active in the same way--if they left after the start of
// the day (or are still in the channel) and joined before the end of the day
q = q.Column(`
COALESCE(
COUNT(DISTINCT
(CASE
WHEN (i.EndAt >= ? OR i.EndAt = 0) AND
i.CreateAt < ? AND
(cmh.LeaveTime >= ? OR cmh.LeaveTime is NULL) AND
cmh.JoinTime < ?
THEN cmh.UserId
END))
, 0)
`, startOfDay, endOfDay, startOfDay, endOfDay)
daysAsTimes = append(daysAsTimes, []int64{startOfDay, endOfDay})
endOfDay -= day
startOfDay -= day
}
q = q.
From("IR_Incident as i").
InnerJoin("ChannelMemberHistory as cmh ON i.ChannelId = cmh.ChannelId").
Where(sq.Expr("cmh.UserId NOT IN (SELECT UserId FROM Bots)"))
q = applyFilters(q, filters)
counts, err := s.performQueryForXCols(q, x)
if err != nil {
logrus.WithError(err).WithField("x", x).Error("failed to query active participants per day last x days")
return []int{}, [][]int64{}
}
reverseSlice(counts)
reverseSlice(daysAsTimes)
return counts, daysAsTimes
}
// MetricOverallAverage for a specific playbook returns a list that contains an average value for each metric.
// Only published metrics values are included.
// Returns empty list when Playbook doesn't have configured metrics
// If for some metrics there are no published values, the corresponding element will be nil in the resulting slice
func (s *StatsStore) MetricOverallAverage(filters StatsFilters) []null.Int {
// this query will return average values only for the metrics that have published data in the database
// so we need to add to the result array nil values for metrics that don't have data
query := s.store.builder.
Select("mc.ID as ID, FLOOR(AVG(m.Value)) as Value").
From("IR_Metric as m").
InnerJoin("IR_MetricConfig as mc ON m.MetricConfigID = mc.ID").
Where(sq.Eq{"mc.PlaybookID": filters.PlaybookID}).
Where(sq.Eq{"m.Published": true}).
GroupBy("mc.ID").
OrderBy("mc.Ordering ASC")
type Average struct {
ID string
Value string
}
var averages []Average
if err := s.store.selectBuilder(s.store.db, &averages, query); err != nil {
logrus.WithError(err).Error("failed to query metric averages")
return []null.Int{}
}
configs, err := s.retrieveMetricConfigs(filters.PlaybookID)
if err != nil {
logrus.WithError(err).WithField("playbook_id", filters.PlaybookID).Error("Error retrieving metrics configs ids for playbook")
return []null.Int{}
}
// use metrics configurations to build a result array, where overallAverage[i] will be average value for
// the i-th metric or nil if there is no data in the database for this specific metric
overallAverage := make([]null.Int, len(configs))
for i, id := range configs {
for _, av := range averages {
if av.ID == id {
val, _ := strconv.ParseInt(av.Value, 10, 64)
overallAverage[i] = null.IntFrom(val)
break
}
}
}
return overallAverage
}
// MetricValueRange returns min and max values for each metric
// Only published metrics are included.
// If there are no configured metrics, returns an empty list
// If for some metrics there are no published values, the corresponding slice will be nil in the resulting slice
func (s *StatsStore) MetricValueRange(filters StatsFilters) [][]int64 {
type MinMax struct {
ID string
Min int64
Max int64
}
// this query will return min-max values only for the metrics that have published data in the database
// so we need to add to the result array nil values for metrics that don't have data
q := s.store.builder.
Select("mc.ID as ID, MIN(Value) as Min, MAX(Value) as Max").
From("IR_Metric as m").
InnerJoin("IR_MetricConfig as mc ON m.MetricConfigID = mc.ID").
Where(sq.Eq{"mc.PlaybookID": filters.PlaybookID}).
Where(sq.Eq{"m.Published": true}).
GroupBy("mc.ID").
OrderBy("mc.Ordering ASC")
var res []MinMax
if err := s.store.selectBuilder(s.store.db, &res, q); err != nil {
logrus.WithError(err).Error("Error retrieving metric min and max values")
return [][]int64{}
}
configs, err := s.retrieveMetricConfigs(filters.PlaybookID)
if err != nil {
logrus.WithError(err).WithField("playbook_id", filters.PlaybookID).Error("Error retrieving metrics configs ids for playbook")
return [][]int64{}
}
// use metrics configurations to build a result array, where valueRange[i] will be min-max values for
// the i-th metric or nil if there is no data in the database for this specific metric
valueRange := make([][]int64, len(configs))
for i, id := range configs {
for _, minMax := range res {
if minMax.ID == id {
valueRange[i] = []int64{minMax.Min, minMax.Max}
break
}
}
}
return valueRange
}
// MetricRollingValuesLastXRuns for each metric returns list of last `x` published values, starting from `offset`
// first element in the list is most recent. And returns the names of the last `x` runs.
// Returns empty list if Playbook doesn't have metrics.
// If for some metrics there are no published values, the corresponding slice will be nil in the resulting slice
func (s *StatsStore) MetricRollingValuesLastXRuns(x int, offset int, filters StatsFilters) ([][]int64, []string) {
logger := logrus.WithField("playbook_id", filters.PlaybookID)
// retrieve metric configs metricsConfigsIDs for playbook
metricsConfigsIDs, err := s.retrieveMetricConfigs(filters.PlaybookID)
if err != nil {
logger.WithError(err).Error("failed to retrieve metrics configs")
return [][]int64{}, []string{}
}
//NOTE: It would be possible to turn this into a single statement; keep in mind if the playbookStats call becomes slow
metricsValues := make([][]int64, 0)
runNames := make([]string, 0)
for _, id := range metricsConfigsIDs {
query := s.store.builder.
Select("m.Value AS Value", "c.DisplayName AS Name").
From("IR_Incident as i").
Join("Channels AS c ON (c.Id = i.ChannelId)").
InnerJoin("IR_Metric AS m ON (i.ID = m.IncidentID)").
Where(sq.Eq{"i.PlaybookID": filters.PlaybookID}).
Where("i.RetrospectivePublishedAt > 0").
Where(sq.Eq{"i.RetrospectiveWasCanceled": false}).
Where(sq.Eq{"m.MetricConfigID": id}).
OrderBy("i.RetrospectivePublishedAt DESC").
Limit(uint64(x)).
Offset(uint64(offset))
var rows []struct {
Value int64
Name string
}
if err := s.store.selectBuilder(s.store.db, &rows, query); err != nil {
logger.WithError(err).WithField("metric_config_id", id).Error("failed to query metrics")
return [][]int64{}, []string{}
}
var values []int64
var names []string
for _, r := range rows {
values = append(values, r.Value)
names = append(names, r.Name)
}
metricsValues = append(metricsValues, values)
runNames = names // overwrites, but it'll be the same data each time -- simpler than making a separate query
}
return metricsValues, runNames
}
// MetricRollingAverageAndChange for each metric returns average of last `x` published values and
// change with comparison to the previous period
// returns empty list if the Playbook doesn't have metrics
// If for some metrics there are no published values, the corresponding element will be nil in the resulting slice
func (s *StatsStore) MetricRollingAverageAndChange(x int, filters StatsFilters) (metricRollingAverage []null.Int, metricRollingAverageChange []null.Int) {
metricValuesWholePeriod, _ := s.MetricRollingValuesLastXRuns(2*x, 0, filters)
if len(metricValuesWholePeriod) == 0 {
return []null.Int{}, []null.Int{}
}
metricRollingAverage = make([]null.Int, 0)
metricRollingAverageChange = make([]null.Int, 0)
for i, nums := range metricValuesWholePeriod {
firstPeriodEnd := int(math.Min(float64(x), float64(len(nums))))
// add null values when there are no metric values available
if firstPeriodEnd == 0 {
metricRollingAverage = append(metricRollingAverage, null.NewInt(0, false))
metricRollingAverageChange = append(metricRollingAverageChange, null.NewInt(0, false))
continue
}
metricRollingAverage = append(metricRollingAverage, null.IntFrom(getAverage(nums[:firstPeriodEnd])))
secondPeriodEnd := int(math.Min(float64(2*x), float64(len(nums))))
// add null value when change can't be calculated
if firstPeriodEnd >= secondPeriodEnd || metricRollingAverage[i].IsZero() {
metricRollingAverageChange = append(metricRollingAverageChange, null.NewInt(0, false))
continue
}
diff := metricRollingAverage[i].Int64*100/getAverage(nums[firstPeriodEnd:secondPeriodEnd]) - 100
metricRollingAverageChange = append(metricRollingAverageChange, null.IntFrom(diff))
}
return
}
func (s *StatsStore) performQueryForXCols(q sq.SelectBuilder, x int) ([]int, error) {
sqlString, args, err := q.ToSql()
if err != nil {
return []int{}, errors.Wrap(err, "failed to build sql")
}
sqlString = s.store.db.Rebind(sqlString)
rows, err := s.store.db.Queryx(sqlString, args...)
if err != nil {
return []int{}, errors.Wrap(err, "failed to get rows from Queryx")
}
defer rows.Close()
if !rows.Next() {
return []int{}, errors.Wrap(rows.Err(), "failed to get rows.Next()")
}
cols, err2 := rows.SliceScan()
if err2 != nil {
return []int{}, errors.Wrap(err, "failed to get SliceScan")
}
if len(cols) != x {
return []int{}, fmt.Errorf("failed to get correct length for columns, wanted %d, got %d", x, len(cols))
}
counts := make([]int, x)
for i := 0; i < x; i++ {
val, ok := cols[i].(int64)
if !ok {
return []int{}, fmt.Errorf("column was unexpected type, wanted int64, got: %T", cols[i])
}
counts[i] = int(val)
}
return counts, nil
}
func (s *StatsStore) retrieveMetricConfigs(playbookID string) ([]string, error) {
query := s.store.builder.
Select("ID").
From("IR_MetricConfig").
Where(sq.Eq{"PlaybookID": playbookID}).
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("Ordering ASC")
var ids []string
if err := s.store.selectBuilder(s.store.db, &ids, query); err != nil {
return nil, err
}
return ids, nil
}
func beginningOfTodayMillis() int64 {
year, month, day := time.Now().UTC().Date()
bod := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
return bod.UnixNano() / int64(time.Millisecond)
}
func endOfTodayMillis() int64 {
year, month, day := time.Now().UTC().Add(24 * time.Hour).Date()
bod := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
return bod.UnixNano()/int64(time.Millisecond) - 1
}
func beginningOfLastSundayMillis() int64 {
// Weekday is an iota where Sun = 0, Mon = 1, etc. So this is an offset to get back to Sun.
offset := int(time.Now().UTC().Weekday())
now := time.Now().UTC()
startOfSunday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -offset)
return startOfSunday.UnixNano() / int64(time.Millisecond)
}
func reverseSlice(s interface{}) {
value := reflect.ValueOf(s)
if value.Kind() != reflect.Slice {
panic(errors.New("s must be a slice type"))
}
n := reflect.ValueOf(s).Len()
swap := reflect.Swapper(s)
for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
swap(i, j)
}
}
func getAverage(nums []int64) int64 {
var sum int64
for _, num := range nums {
sum += num
}
return sum / int64(len(nums))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/sirupsen/logrus"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
)
// maxJSONLength holds the limit we set for JSON data in postgres
// Since JSON data type is unboounded, we need to set a limit
// that we'll control manually.
const maxJSONLength = 256 * 1024 // 256KB
type SQLStore struct {
db *sqlx.DB
builder sq.StatementBuilderType
scheduler app.JobOnceScheduler
}
// New constructs a new instance of SQLStore.
func New(pluginAPI PluginAPIClient, scheduler app.JobOnceScheduler) (*SQLStore, error) {
var db *sqlx.DB
origDB, err := pluginAPI.Store.GetMasterDB()
if err != nil {
return nil, err
}
db = sqlx.NewDb(origDB, pluginAPI.Store.DriverName())
builder := sq.StatementBuilder.PlaceholderFormat(sq.Question)
if pluginAPI.Store.DriverName() == model.DatabaseDriverPostgres {
builder = builder.PlaceholderFormat(sq.Dollar)
}
if pluginAPI.Store.DriverName() == model.DatabaseDriverMysql {
db.MapperFunc(func(s string) string { return s })
}
return &SQLStore{
db,
builder,
scheduler,
}, nil
}
// queryer is an interface describing a resource that can query.
//
// It exactly matches sqlx.Queryer, existing simply to constrain sqlx usage to this file.
type queryer interface {
sqlx.Queryer
}
// builder is an interface describing a resource that can construct SQL and arguments.
//
// It exists to allow consuming any squirrel.*Builder type.
type builder interface {
ToSql() (string, []interface{}, error)
}
// get queries for a single row, building the sql, and writing the result into dest.
//
// Use this to simplify querying for a single row or column. Dest may be a pointer to a simple
// type, or a struct with fields to be populated from the returned columns.
func (sqlStore *SQLStore) getBuilder(q sqlx.Queryer, dest interface{}, b builder) error {
sqlString, args, err := b.ToSql()
if err != nil {
return errors.Wrap(err, "failed to build sql")
}
sqlString = sqlStore.db.Rebind(sqlString)
return sqlx.Get(q, dest, sqlString, args...)
}
// selectBuilder queries for one or more rows, building the sql, and writing the result into dest.
//
// Use this to simplify querying for multiple rows (and possibly columns). Dest may be a slice of
// a simple, or a slice of a struct with fields to be populated from the returned columns.
func (sqlStore *SQLStore) selectBuilder(q sqlx.Queryer, dest interface{}, b builder) error {
sqlString, args, err := b.ToSql()
if err != nil {
return errors.Wrap(err, "failed to build sql")
}
sqlString = sqlStore.db.Rebind(sqlString)
return sqlx.Select(q, dest, sqlString, args...)
}
// execer is an interface describing a resource that can execute write queries.
//
// It allows the use of *sqlx.Db and *sqlx.Tx.
type execer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
DriverName() string
}
type queryExecer interface {
queryer
execer
}
// exec executes the given query using positional arguments, automatically rebinding for the db.
func (sqlStore *SQLStore) exec(e execer, sqlString string, args ...interface{}) (sql.Result, error) {
sqlString = sqlStore.db.Rebind(sqlString)
return e.Exec(sqlString, args...)
}
// exec executes the given query, building the necessary sql.
func (sqlStore *SQLStore) execBuilder(e execer, b builder) (sql.Result, error) {
sqlString, args, err := b.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to build sql")
}
return sqlStore.exec(e, sqlString, args...)
}
// finalizeTransaction ensures a transaction is closed after use, rolling back if not already committed.
func (sqlStore *SQLStore) finalizeTransaction(tx *sqlx.Tx) {
// Rollback returns sql.ErrTxDone if the transaction was already closed.
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
logrus.WithError(err).Error("Failed to rollback transaction")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/pkg/errors"
)
// getSystemValue queries the IR_System table for the given key
func (sqlStore *SQLStore) getSystemValue(q queryer, key string) (string, error) {
var value string
err := sqlStore.getBuilder(q, &value,
sq.Select("SValue").
From("IR_System").
Where(sq.Eq{"SKey": key}),
)
if err == sql.ErrNoRows {
return "", nil
} else if err != nil {
return "", errors.Wrapf(err, "failed to query system key %s", key)
}
return value, nil
}
// setSystemValue updates the IR_System table for the given key.
func (sqlStore *SQLStore) setSystemValue(e queryExecer, key, value string) error {
// MySQL reports 0 rows affected in the update below when the key and value
// already exist. We can use its native support for upsert instead. Postgres
// 9.4 does not have native support for upsert, but it reports 1 row
// affected even when the key and value are already present.
if sqlStore.db.DriverName() == model.DatabaseDriverMysql {
_, err := sqlStore.execBuilder(e,
sq.Insert("IR_System").
Columns("SKey", "SValue").
Values(key, value).
Suffix("ON DUPLICATE KEY UPDATE SValue = ?", value),
)
return err
}
result, err := sqlStore.execBuilder(e,
sq.Update("IR_System").
Set("SValue", value).
Where(sq.Eq{"SKey": key}),
)
if err != nil {
return errors.Wrapf(err, "failed to update system key %s", key)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
return nil
}
_, err = sqlStore.execBuilder(e,
sq.Insert("IR_System").
Columns("SKey", "SValue").
Values(key, value),
)
if err != nil {
return errors.Wrapf(err, "failed to insert system key %s", key)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
)
type sqlUserInfo struct {
app.UserInfo
DigestNotificationSettingsJSON json.RawMessage
}
type userInfoStore struct {
store *SQLStore
queryBuilder sq.StatementBuilderType
userInfoSelect sq.SelectBuilder
}
// Ensure userInfoStore implements the userInfo.Store interface
var _ app.UserInfoStore = (*userInfoStore)(nil)
func NewUserInfoStore(sqlStore *SQLStore) app.UserInfoStore {
userInfoSelect := sqlStore.builder.
Select("ID", "LastDailyTodoDMAt", "COALESCE(DigestNotificationSettingsJSON, '{}') DigestNotificationSettingsJSON").
From("IR_UserInfo")
newStore := &userInfoStore{
store: sqlStore,
queryBuilder: sqlStore.builder,
userInfoSelect: userInfoSelect,
}
return newStore
}
// Get retrieves a UserInfo struct by the user's userID.
func (s *userInfoStore) Get(userID string) (app.UserInfo, error) {
var raw sqlUserInfo
err := s.store.getBuilder(s.store.db, &raw, s.userInfoSelect.Where(sq.Eq{"ID": userID}))
if err == sql.ErrNoRows {
return app.UserInfo{}, errors.Wrapf(app.ErrNotFound, "userInfo does not exist for userId '%s'", userID)
} else if err != nil {
return app.UserInfo{}, errors.Wrapf(err, "failed to get userInfo by userId '%s'", userID)
}
return toUserInfo(raw)
}
// Upsert inserts (creates) or updates the UserInfo in info.
func (s *userInfoStore) Upsert(info app.UserInfo) error {
if info.ID == "" {
return errors.New("ID should not be empty")
}
raw, err := toSQLUserInfo(info)
if err != nil {
return err
}
if s.store.db.DriverName() == model.DatabaseDriverMysql {
_, err = s.store.execBuilder(s.store.db,
sq.Insert("IR_UserInfo").
Columns("ID", "LastDailyTodoDMAt", "DigestNotificationSettingsJSON").
Values(raw.ID, raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON).
Suffix("ON DUPLICATE KEY UPDATE LastDailyTodoDMAt = ?, DigestNotificationSettingsJSON = ?",
raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON))
} else {
_, err = s.store.execBuilder(s.store.db,
sq.Insert("IR_UserInfo").
Columns("ID", "LastDailyTodoDMAt", "DigestNotificationSettingsJSON").
Values(raw.ID, raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON).
Suffix("ON CONFLICT (ID) DO UPDATE SET LastDailyTodoDMAt = ?, DigestNotificationSettingsJSON = ?",
raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON))
}
if err != nil {
return errors.Wrapf(err, "failed to upsert userInfo with id '%s'", raw.ID)
}
return nil
}
func toUserInfo(rawUserInfo sqlUserInfo) (app.UserInfo, error) {
userInfo := rawUserInfo.UserInfo
if len(rawUserInfo.DigestNotificationSettingsJSON) == 0 {
return userInfo, nil
}
if err := json.Unmarshal(rawUserInfo.DigestNotificationSettingsJSON, &userInfo.DigestNotificationSettings); err != nil {
return userInfo, errors.Wrapf(err, "failed to unmarshal DigestNotificationSettings for userid: %s", userInfo.ID)
}
return userInfo, nil
}
func toSQLUserInfo(userInfo app.UserInfo) (*sqlUserInfo, error) {
digestNotificationSettingsJSON, err := json.Marshal(userInfo.DigestNotificationSettings)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal DigestNotificationSettings for userid: %s", userInfo.ID)
}
if len(digestNotificationSettingsJSON) > maxJSONLength {
return nil, errors.Wrapf(errors.New("invalid data"), "digestNotificationSettings json for user id '%s' is too long (max %d)", userInfo.ID, maxJSONLength)
}
return &sqlUserInfo{
UserInfo: userInfo,
DigestNotificationSettingsJSON: digestNotificationSettingsJSON,
}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"github.com/blang/semver"
"github.com/pkg/errors"
)
const systemDatabaseVersionKey = "DatabaseVersion"
func LatestVersion() semver.Version {
return migrations[len(migrations)-1].toVersion
}
func (sqlStore *SQLStore) GetCurrentVersion() (semver.Version, error) {
currentVersionStr, err := sqlStore.getSystemValue(sqlStore.db, systemDatabaseVersionKey)
if currentVersionStr == "" {
return semver.Version{}, nil
}
if err != nil {
return semver.Version{}, errors.Wrapf(err, "failed retrieving the DatabaseVersion key from the IR_System table")
}
currentSchemaVersion, err := semver.Parse(currentVersionStr)
if err != nil {
return semver.Version{}, errors.Wrapf(err, "unable to parse current schema version")
}
return currentSchemaVersion, nil
}
func (sqlStore *SQLStore) SetCurrentVersion(e queryExecer, currentVersion semver.Version) error {
return sqlStore.setSystemValue(e, systemDatabaseVersionKey, currentVersion.String())
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
)
// NoopTelemetry satisfies the Telemetry interface with no-op implementations.
type NoopTelemetry struct {
}
// Enable does nothing, returning always nil.
func (t *NoopTelemetry) Enable() error {
return nil
}
// Disable does nothing, returning always nil.
func (t *NoopTelemetry) Disable() error {
return nil
}
// Page does nothing
func (t *NoopTelemetry) Page(name app.TelemetryPage, properties map[string]interface{}) {
}
// Track does nothing
func (t *NoopTelemetry) Track(name app.TelemetryTrack, properties map[string]interface{}) {
}
// CreatePlaybookRun does nothing
func (t *NoopTelemetry) CreatePlaybookRun(*app.PlaybookRun, string, bool) {
}
// EndPlaybookRun does nothing
func (t *NoopTelemetry) FinishPlaybookRun(*app.PlaybookRun, string) {
}
// RestorePlaybookRun does nothing
func (t *NoopTelemetry) RestorePlaybookRun(*app.PlaybookRun, string) {
}
// RestartPlaybookRun does nothing
func (t *NoopTelemetry) RestartPlaybookRun(*app.PlaybookRun, string) {
}
// UpdateStatus does nothing
func (t *NoopTelemetry) UpdateStatus(*app.PlaybookRun, string) {
}
// FrontendTelemetryForPlaybookRun does nothing
func (t *NoopTelemetry) FrontendTelemetryForPlaybookRun(*app.PlaybookRun, string, string) {
}
// AddPostToTimeline does nothing
func (t *NoopTelemetry) AddPostToTimeline(*app.PlaybookRun, string) {
}
// RemoveTimelineEvent does nothing
func (t *NoopTelemetry) RemoveTimelineEvent(*app.PlaybookRun, string) {
}
// AddTask does nothing.
func (t *NoopTelemetry) AddTask(string, string, app.ChecklistItem) {
}
// RemoveTask does nothing.
func (t *NoopTelemetry) RemoveTask(string, string, app.ChecklistItem) {
}
// RenameTask does nothing.
func (t *NoopTelemetry) RenameTask(string, string, app.ChecklistItem) {
}
// SkipChecklist does nothing.
func (t *NoopTelemetry) SkipChecklist(string, string, app.Checklist) {
}
// RestoreChecklist does nothing.
func (t *NoopTelemetry) RestoreChecklist(string, string, app.Checklist) {
}
// SkipTask does nothing.
func (t *NoopTelemetry) SkipTask(string, string, app.ChecklistItem) {
}
// RestoreTask does nothing.
func (t *NoopTelemetry) RestoreTask(string, string, app.ChecklistItem) {
}
// ModifyCheckedState does nothing.
func (t *NoopTelemetry) ModifyCheckedState(string, string, app.ChecklistItem, bool) {
}
// SetAssignee does nothing.
func (t *NoopTelemetry) SetAssignee(string, string, app.ChecklistItem) {
}
// MoveChecklist does nothing.
func (t *NoopTelemetry) MoveChecklist(string, string, app.Checklist) {
}
// MoveTask does nothing.
func (t *NoopTelemetry) MoveTask(string, string, app.ChecklistItem) {
}
// CreatePlaybook does nothing.
func (t *NoopTelemetry) CreatePlaybook(app.Playbook, string) {
}
// ImportPlaybook does nothing.
func (t *NoopTelemetry) ImportPlaybook(app.Playbook, string) {
}
// UpdatePlaybook does nothing.
func (t *NoopTelemetry) UpdatePlaybook(app.Playbook, string) {
}
// DeletePlaybook does nothing.
func (t *NoopTelemetry) DeletePlaybook(app.Playbook, string) {
}
// RestorePlaybook does nothing either.
func (t *NoopTelemetry) RestorePlaybook(app.Playbook, string) {
}
// ChangeOwner does nothing
func (t *NoopTelemetry) ChangeOwner(*app.PlaybookRun, string) {
}
// RunTaskSlashCommand does nothing
func (t *NoopTelemetry) RunTaskSlashCommand(string, string, app.ChecklistItem) {
}
// AddChecklist does nothing
func (t *NoopTelemetry) AddChecklist(playbookRunID, userID string, checklist app.Checklist) {
}
// RemoveChecklist does nothing
func (t *NoopTelemetry) RemoveChecklist(playbookRunID, userID string, checklist app.Checklist) {
}
// RenameChecklist does nothing
func (t *NoopTelemetry) RenameChecklist(playbookRunID, userID string, checklist app.Checklist) {
}
func (t *NoopTelemetry) UpdateRetrospective(playbookRun *app.PlaybookRun, userID string) {
}
func (t *NoopTelemetry) PublishRetrospective(playbookRun *app.PlaybookRun, userID string) {
}
// StartTrial does nothing.
func (t *NoopTelemetry) StartTrial(userID string, action string) {
}
// NotifyAdmins does nothing.
func (t *NoopTelemetry) NotifyAdmins(userID string, action string) {
}
// FrontendTelemetryForPlaybook does nothing.
func (t *NoopTelemetry) FrontendTelemetryForPlaybook(playbook app.Playbook, userID, action string) {
}
// FrontendTelemetryForPlaybookTemplate does nothing.
func (t *NoopTelemetry) FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string) {
}
// ChangeDigestSettings does nothing
func (t *NoopTelemetry) ChangeDigestSettings(userID string, old app.DigestNotificationSettings, new app.DigestNotificationSettings) {
}
// Follow tracks userID following a playbook run.
func (t *NoopTelemetry) Follow(playbookRun *app.PlaybookRun, userID string) {
}
// Unfollow tracks userID following a playbook run.
func (t *NoopTelemetry) Unfollow(playbookRun *app.PlaybookRun, userID string) {
}
// AutoFollowPlaybook tracks the auto-follow of a playbook.
func (t *NoopTelemetry) AutoFollowPlaybook(playbook app.Playbook, userID string) {
}
// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook.
func (t *NoopTelemetry) AutoUnfollowPlaybook(playbook app.Playbook, userID string) {
}
// RunChannelAction does nothing
func (t *NoopTelemetry) RunChannelAction(action app.GenericChannelAction, userID string) {
}
// UpdateChannelAction does nothing
func (t *NoopTelemetry) UpdateChannelAction(action app.GenericChannelAction, userID string) {
}
// RunAction does nothing
func (t *NoopTelemetry) RunAction(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) {
}
// FavoriteItem does nothing
func (t *NoopTelemetry) FavoriteItem(item app.CategoryItem, userID string) {
}
// UnfavoriteItem does nothing
func (t *NoopTelemetry) UnfavoriteItem(item app.CategoryItem, userID string) {
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"sync"
"github.com/mattermost/mattermost-server/v6/server/playbooks/server/app"
"github.com/pkg/errors"
rudder "github.com/rudderlabs/analytics-go"
)
// RudderTelemetry implements Telemetry using a Rudder backend.
type RudderTelemetry struct {
client rudder.Client
diagnosticID string
pluginVersion string
serverVersion string
writeKey string
dataPlaneURL string
enabled bool
mutex sync.RWMutex
}
// Unique strings that identify each of the tracked events
const (
eventPlaybookRun = "incident"
actionCreate = "create"
actionImport = "import"
actionEnd = "end"
actionRestart = "restart"
actionChangeOwner = "change_commander"
actionUpdateStatus = "update_status"
actionAddTimelineEventFromPost = "add_timeline_event_from_post"
actionUpdateRetrospective = "update_retrospective"
actionPublishRetrospective = "publish_retrospective"
actionRemoveTimelineEvent = "remove_timeline_event"
actionFollow = "follow"
actionUnfollow = "unfollow"
eventTasks = "tasks"
actionAddTask = "add_task"
actionRemoveTask = "remove_task"
actionRenameTask = "rename_task"
actionSkipTask = "skip_task"
actionRestoreTask = "restore_task"
actionModifyTaskState = "modify_task_state"
actionMoveTask = "move_task"
actionSetAssigneeForTask = "set_assignee_for_task"
actionRunTaskSlashCommand = "run_task_slash_command"
eventChecklists = "checklists"
actionAddChecklist = "add_checklist"
actionRemoveChecklist = "remove_checklist"
actionRenameChecklist = "rename_checklist"
actionMoveChecklist = "move_checklist"
actionSkipChecklist = "skip_checklist"
actionRestoreChecklist = "restore_checklist"
eventPlaybook = "playbook"
actionUpdate = "update"
actionDelete = "delete"
actionRestore = "restore"
actionAutoFollow = "auto_follow"
actionAutoUnfollow = "auto_unfollow"
eventFrontend = "frontend"
eventNotifyAdmins = "notify_admins"
eventStartTrial = "start_trial"
// telemetryKeyPlaybookRunID records the legacy name used to identify a playbook run via telemetry.
telemetryKeyPlaybookRunID = "IncidentID"
eventSettings = "settings"
actionDigest = "digest"
eventChannelAction = "channel_action"
actionRunChannelAction = "run_channel_action"
actionChannelActionUpdate = "update_channel_action"
eventRunAction = "playbookrun_action"
actionRunAction = "run_playbookrun_action"
eventSidebarCategory = "lhs_category"
actionFavoriteRun = "favorite_run"
actionUnfavoriteRun = "unfavorite_run"
actionFavoritePlaybook = "favorite_playbook"
actionUnfavoritePlaybook = "unfavorite_playbook"
)
// Migrated
// actionRunActionsUpdate = "update_playbookrun_actions" => playbookrun_update_actions
// NewRudder builds a new RudderTelemetry client that will send the events to
// dataPlaneURL with the writeKey, identified with the diagnosticID. The
// version of the server is also sent with every event tracked.
// If either diagnosticID or serverVersion are empty, an error is returned.
func NewRudder(dataPlaneURL, writeKey, diagnosticID, pluginVersion, serverVersion string) (*RudderTelemetry, error) {
if diagnosticID == "" {
return nil, errors.New("diagnosticID should not be empty")
}
if pluginVersion == "" {
return nil, errors.New("pluginVersion should not be empty")
}
if serverVersion == "" {
return nil, errors.New("serverVersion should not be empty")
}
client, err := rudder.NewWithConfig(writeKey, dataPlaneURL, rudder.Config{})
if err != nil {
return nil, err
}
return &RudderTelemetry{
client: client,
diagnosticID: diagnosticID,
pluginVersion: pluginVersion,
serverVersion: serverVersion,
writeKey: writeKey,
dataPlaneURL: dataPlaneURL,
enabled: true,
}, nil
}
// trackOld is the generic tracker for events to rudderstack that is backwards compatible with
// old events (string based instead of enum).
//
// All new and migrated events should use Track/Page instead. This should be removed after
// event migration is complete
func (t *RudderTelemetry) trackOld(name string, properties map[string]interface{}) {
t.mutex.RLock()
defer t.mutex.RUnlock()
if !t.enabled {
return
}
properties["PluginVersion"] = t.pluginVersion
properties["ServerVersion"] = t.serverVersion
_ = t.client.Enqueue(rudder.Track{
UserId: t.diagnosticID,
Event: name,
Properties: properties,
})
}
// Track is the generic tracker for events to rudderstack
func (t *RudderTelemetry) Track(name app.TelemetryTrack, properties map[string]interface{}) {
t.mutex.RLock()
defer t.mutex.RUnlock()
if !t.enabled {
return
}
properties["PluginVersion"] = t.pluginVersion
properties["ServerVersion"] = t.serverVersion
_ = t.client.Enqueue(rudder.Track{
UserId: t.diagnosticID,
Event: name.String(),
Properties: properties,
})
}
// Page is the generic tracker for pageviews to rudderstack
func (t *RudderTelemetry) Page(name app.TelemetryPage, properties map[string]interface{}) {
t.mutex.RLock()
defer t.mutex.RUnlock()
if !t.enabled {
return
}
properties["PluginVersion"] = t.pluginVersion
properties["ServerVersion"] = t.serverVersion
_ = t.client.Enqueue(rudder.Page{
UserId: t.diagnosticID,
Name: name.String(),
Properties: properties,
})
}
func tasksWithDueDate(list app.Checklist) int {
count := 0
for _, item := range list.Items {
if item.DueDate > 0 {
count++
}
}
return count
}
func playbookRunProperties(playbookRun *app.PlaybookRun, userID string) map[string]interface{} {
totalChecklistItems := 0
itemsWithDueDate := 0
for _, checklist := range playbookRun.Checklists {
totalChecklistItems += len(checklist.Items)
itemsWithDueDate += tasksWithDueDate(checklist)
}
role := "viewer"
for _, p := range playbookRun.ParticipantIDs {
if p == userID {
role = "participant"
break
}
}
return map[string]interface{}{
"UserActualID": userID,
"UserActualRole": role,
telemetryKeyPlaybookRunID: playbookRun.ID,
"HasDescription": playbookRun.Summary != "",
"CommanderUserID": playbookRun.OwnerUserID,
"ReporterUserID": playbookRun.ReporterUserID,
"TeamID": playbookRun.TeamID,
"ChannelID": playbookRun.ChannelID,
"CreateAt": playbookRun.CreateAt,
"EndAt": playbookRun.EndAt,
"DeleteAt": playbookRun.DeleteAt, //nolint
"PostID": playbookRun.PostID,
"PlaybookID": playbookRun.PlaybookID,
"NumChecklists": len(playbookRun.Checklists),
"TotalChecklistItems": totalChecklistItems,
"ChecklistItemsWithDueDate": itemsWithDueDate,
"NumStatusPosts": len(playbookRun.StatusPosts),
"CurrentStatus": playbookRun.CurrentStatus,
"PreviousReminder": playbookRun.PreviousReminder,
"NumTimelineEvents": len(playbookRun.TimelineEvents),
"StatusUpdateBroadcastChannelsEnabled": playbookRun.StatusUpdateBroadcastChannelsEnabled,
"StatusUpdateBroadcastWebhooksEnabled": playbookRun.StatusUpdateBroadcastWebhooksEnabled,
}
}
// CreatePlaybookRun tracks the creation of the playbook run passed.
func (t *RudderTelemetry) CreatePlaybookRun(playbookRun *app.PlaybookRun, userID string, public bool) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionCreate
properties["Public"] = public
t.trackOld(eventPlaybookRun, properties)
}
// FinishPlaybookRun tracks the end of the playbook run passed.
func (t *RudderTelemetry) FinishPlaybookRun(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionEnd
t.trackOld(eventPlaybookRun, properties)
}
// RestorePlaybookRun tracks the restoration of the playbook run.
func (t *RudderTelemetry) RestorePlaybookRun(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionRestore
t.trackOld(eventPlaybookRun, properties)
}
// RestartPlaybookRun tracks the restart of the playbook run.
func (t *RudderTelemetry) RestartPlaybookRun(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionRestart
t.trackOld(eventPlaybookRun, properties)
}
// ChangeOwner tracks changes in owner
func (t *RudderTelemetry) ChangeOwner(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionChangeOwner
t.trackOld(eventPlaybookRun, properties)
}
func (t *RudderTelemetry) UpdateStatus(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionUpdateStatus
properties["ReminderTimerSeconds"] = int(playbookRun.PreviousReminder)
t.trackOld(eventPlaybookRun, properties)
}
func (t *RudderTelemetry) FrontendTelemetryForPlaybookRun(playbookRun *app.PlaybookRun, userID, action string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = action
t.trackOld(eventFrontend, properties)
}
// AddPostToTimeline tracks userID creating a timeline event from a post.
func (t *RudderTelemetry) AddPostToTimeline(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionAddTimelineEventFromPost
t.trackOld(eventPlaybookRun, properties)
}
// RemoveTimelineEvent tracks userID removing a timeline event.
func (t *RudderTelemetry) RemoveTimelineEvent(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionRemoveTimelineEvent
t.trackOld(eventPlaybookRun, properties)
}
// Follow tracks userID following a playbook run.
func (t *RudderTelemetry) Follow(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionFollow
t.trackOld(eventPlaybookRun, properties)
}
// Unfollow tracks userID following a playbook run.
func (t *RudderTelemetry) Unfollow(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionUnfollow
t.trackOld(eventPlaybookRun, properties)
}
func taskProperties(playbookRunID, userID string, task app.ChecklistItem) map[string]interface{} {
return map[string]interface{}{
telemetryKeyPlaybookRunID: playbookRunID,
"UserActualID": userID,
"TaskID": task.ID,
"State": task.State,
"AssigneeID": task.AssigneeID,
"HasCommand": task.Command != "",
"CommandLastRun": task.CommandLastRun,
"HasDescription": task.Description != "",
"HasDueDate": task.DueDate > 0,
}
}
// AddTask tracks the creation of a new checklist item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) AddTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionAddTask
t.trackOld(eventTasks, properties)
}
// RemoveTask tracks the removal of a checklist item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) RemoveTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionRemoveTask
t.trackOld(eventTasks, properties)
}
// RenameTask tracks the update of a checklist item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) RenameTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionRenameTask
t.trackOld(eventTasks, properties)
}
// SkipChecklist tracks the skipping of a checklist by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) SkipChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionSkipChecklist
t.trackOld(eventChecklists, properties)
}
// RestoreChecklist tracks the restoring of a checklist by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) RestoreChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionRestoreChecklist
t.trackOld(eventChecklists, properties)
}
// SkipTask tracks the skipping of a checklist item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) SkipTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionSkipTask
t.trackOld(eventTasks, properties)
}
// RestoreTask tracks the restoring of a checklist item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) RestoreTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionRestoreTask
t.trackOld(eventTasks, properties)
}
// ModifyCheckedState tracks the checking and unchecking of items by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) ModifyCheckedState(playbookRunID, userID string, task app.ChecklistItem, wasOwner bool) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionModifyTaskState
properties["NewState"] = task.State
properties["WasCommander"] = wasOwner
properties["WasAssignee"] = task.AssigneeID == userID
t.trackOld(eventTasks, properties)
}
// SetAssignee tracks the changing of an assignee on an item by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) SetAssignee(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionSetAssigneeForTask
t.trackOld(eventTasks, properties)
}
// MoveTask tracks the movement of checklist items by the user
// identified by userID in the given playbook run.
func (t *RudderTelemetry) MoveTask(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionMoveTask
t.trackOld(eventTasks, properties)
}
// RunTaskSlashCommand tracks the execution of a slash command on a checklist item.
func (t *RudderTelemetry) RunTaskSlashCommand(playbookRunID, userID string, task app.ChecklistItem) {
properties := taskProperties(playbookRunID, userID, task)
properties["Action"] = actionRunTaskSlashCommand
t.trackOld(eventTasks, properties)
}
func checklistProperties(playbookRunID, userID string, checklist app.Checklist) map[string]interface{} {
return map[string]interface{}{
telemetryKeyPlaybookRunID: playbookRunID,
"UserActualID": userID,
"ChecklistID": checklist.ID,
"ChecklistNumItems": len(checklist.Items),
}
}
// AddChecklist tracks the creation of a new checklist.
func (t *RudderTelemetry) AddChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionAddChecklist
t.trackOld(eventChecklists, properties)
}
// RemoveChecklist tracks the removal of a checklist.
func (t *RudderTelemetry) RemoveChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionRemoveChecklist
t.trackOld(eventChecklists, properties)
}
// RenameChecklist tracks the renaming of a checklist
func (t *RudderTelemetry) RenameChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionRenameChecklist
t.trackOld(eventChecklists, properties)
}
// MoveChecklist tracks the movement of a checklist
func (t *RudderTelemetry) MoveChecklist(playbookRunID, userID string, checklist app.Checklist) {
properties := checklistProperties(playbookRunID, userID, checklist)
properties["Action"] = actionMoveChecklist
t.trackOld(eventChecklists, properties)
}
func (t *RudderTelemetry) UpdateRetrospective(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionUpdateRetrospective
t.trackOld(eventTasks, properties)
}
func (t *RudderTelemetry) PublishRetrospective(playbookRun *app.PlaybookRun, userID string) {
properties := playbookRunProperties(playbookRun, userID)
properties["Action"] = actionPublishRetrospective
properties["NumMetrics"] = len(playbookRun.MetricsData)
t.trackOld(eventTasks, properties)
}
func playbookProperties(playbook app.Playbook, userID string) map[string]interface{} {
totalChecklistItems := 0
totalChecklistItemsWithCommands := 0
for _, checklist := range playbook.Checklists {
totalChecklistItems += len(checklist.Items)
for _, item := range checklist.Items {
if item.Command != "" {
totalChecklistItemsWithCommands++
}
}
}
return map[string]interface{}{
"UserActualID": userID,
"PlaybookID": playbook.ID,
"HasDescription": playbook.Description != "",
"TeamID": playbook.TeamID,
"IsPublic": playbook.CreatePublicPlaybookRun,
"CreateAt": playbook.CreateAt,
"DeleteAt": playbook.DeleteAt,
"NumChecklists": len(playbook.Checklists),
"TotalChecklistItems": totalChecklistItems,
"NumSlashCommands": totalChecklistItemsWithCommands,
"NumMembers": len(playbook.Members),
"UsesReminderMessageTemplate": playbook.ReminderMessageTemplate != "",
"ReminderTimerDefaultSeconds": playbook.ReminderTimerDefaultSeconds,
"NumInvitedUserIDs": len(playbook.InvitedUserIDs),
"NumInvitedGroupIDs": len(playbook.InvitedGroupIDs),
"InviteUsersEnabled": playbook.InviteUsersEnabled,
"DefaultCommanderID": playbook.DefaultOwnerID,
"DefaultCommanderEnabled": playbook.DefaultOwnerEnabled,
"BroadcastChannelIDs": playbook.BroadcastChannelIDs,
"BroadcastEnabled": playbook.BroadcastEnabled, //nolint
"NumWebhookOnCreationURLs": len(playbook.WebhookOnCreationURLs),
"WebhookOnCreationEnabled": playbook.WebhookOnCreationEnabled,
"SignalAnyKeywordsEnabled": playbook.SignalAnyKeywordsEnabled,
"NumSignalAnyKeywords": len(playbook.SignalAnyKeywords),
"HasChannelNameTemplate": playbook.ChannelNameTemplate != "",
"NumMetrics": len(playbook.Metrics),
}
}
func playbookTemplateProperties(templateName string, userID string) map[string]interface{} {
return map[string]interface{}{
"UserActualID": userID,
"TemplateName": templateName,
}
}
// CreatePlaybook tracks the creation of a playbook.
func (t *RudderTelemetry) CreatePlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionCreate
t.trackOld(eventPlaybook, properties)
}
// ImportPlaybook tracks the import of a playbook.
func (t *RudderTelemetry) ImportPlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionImport
t.trackOld(eventPlaybook, properties)
}
// UpdatePlaybook tracks the update of a playbook.
func (t *RudderTelemetry) UpdatePlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionUpdate
t.trackOld(eventPlaybook, properties)
}
// DeletePlaybook tracks the deletion of a playbook.
func (t *RudderTelemetry) DeletePlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionDelete
t.trackOld(eventPlaybook, properties)
}
// RestorePlaybook tracks the deletion of a playbook.
func (t *RudderTelemetry) RestorePlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionRestore
t.trackOld(eventPlaybook, properties)
}
// AutoFollowPlaybook tracks the auto-follow of a playbook.
func (t *RudderTelemetry) AutoFollowPlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionAutoFollow
t.trackOld(eventPlaybook, properties)
}
// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook.
func (t *RudderTelemetry) AutoUnfollowPlaybook(playbook app.Playbook, userID string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = actionAutoUnfollow
t.trackOld(eventPlaybook, properties)
}
// FrontendTelemetryForPlaybook tracks an event originating from the frontend
func (t *RudderTelemetry) FrontendTelemetryForPlaybook(playbook app.Playbook, userID, action string) {
properties := playbookProperties(playbook, userID)
properties["Action"] = action
t.trackOld(eventFrontend, properties)
}
// FrontendTelemetryForPlaybookTemplate tracks a playbook template event originating from the frontend
func (t *RudderTelemetry) FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string) {
properties := playbookTemplateProperties(templateName, userID)
properties["Action"] = action
t.trackOld(eventFrontend, properties)
}
func commonProperties(userID string) map[string]interface{} {
return map[string]interface{}{
"UserActualID": userID,
}
}
func (t *RudderTelemetry) StartTrial(userID string, action string) {
properties := commonProperties(userID)
properties["Action"] = action
t.trackOld(eventStartTrial, properties)
}
func (t *RudderTelemetry) NotifyAdmins(userID string, action string) {
properties := commonProperties(userID)
properties["Action"] = action
t.trackOld(eventNotifyAdmins, properties)
}
// Enable creates a new client to track all future events. It does nothing if
// a client is already enabled.
func (t *RudderTelemetry) Enable() error {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.enabled {
return nil
}
newClient, err := rudder.NewWithConfig(t.writeKey, t.dataPlaneURL, rudder.Config{})
if err != nil {
return errors.Wrap(err, "creating a new Rudder client in Enable failed")
}
t.client = newClient
t.enabled = true
return nil
}
// Disable disables telemetry for all future events. It does nothing if the
// client is already disabled.
func (t *RudderTelemetry) Disable() error {
t.mutex.Lock()
defer t.mutex.Unlock()
if !t.enabled {
return nil
}
if err := t.client.Close(); err != nil {
return errors.Wrap(err, "closing the Rudder client in Disable failed")
}
t.enabled = false
return nil
}
func digestSettingsProperties(userID string) map[string]interface{} {
return map[string]interface{}{
"UserActualID": userID,
}
}
// ChangeDigestSettings tracks when a user changes one of the digest settings
func (t *RudderTelemetry) ChangeDigestSettings(userID string, old app.DigestNotificationSettings, new app.DigestNotificationSettings) {
properties := digestSettingsProperties(userID)
properties["Action"] = actionDigest
properties["OldDisableDailyDigest"] = old.DisableDailyDigest
properties["NewDisableDailyDigest"] = new.DisableDailyDigest
properties["OldDisableWeeklyDigest"] = old.DisableWeeklyDigest
properties["NewDisableWeeklyDigest"] = new.DisableWeeklyDigest
t.trackOld(eventSettings, properties)
}
func channelActionProperties(action app.GenericChannelAction, userID string) map[string]interface{} {
return map[string]interface{}{
"UserActualID": userID,
"ChannelID": action.ChannelID,
"ActionType": action.ActionType,
"TriggerType": action.TriggerType,
}
}
func (t *RudderTelemetry) RunChannelAction(action app.GenericChannelAction, userID string) {
properties := channelActionProperties(action, userID)
properties["Action"] = actionRunChannelAction
t.trackOld(eventChannelAction, properties)
}
// UpdateRunActions tracks actions settings update
func (t *RudderTelemetry) UpdateChannelAction(action app.GenericChannelAction, userID string) {
properties := channelActionProperties(action, userID)
properties["Action"] = actionChannelActionUpdate
t.trackOld(eventChannelAction, properties)
}
func runActionProperties(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) map[string]interface{} {
return map[string]interface{}{
"UserActualID": userID,
"ActionType": actionType,
"TriggerType": triggerType,
"NumBroadcasts": numBroadcasts,
"PlaybookRunID": playbookRun.ID,
"PlaybookID": playbookRun.PlaybookID,
}
}
// RunAction tracks the run actions, i.e., status broadcast action
func (t *RudderTelemetry) RunAction(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) {
properties := runActionProperties(playbookRun, userID, triggerType, actionType, numBroadcasts)
properties["Action"] = actionRunAction
t.trackOld(eventRunAction, properties)
}
// FavoriteItem tracks run favoriting of an item. Item can be run or a playbook
func (t *RudderTelemetry) FavoriteItem(item app.CategoryItem, userID string) {
properties := map[string]interface{}{}
properties["UserActualID"] = userID
switch item.Type {
case app.PlaybookItemType:
properties["PlaybookID"] = item.ItemID
properties["Action"] = actionFavoritePlaybook
case app.RunItemType:
properties["PlaybookRunID"] = item.ItemID
properties["Action"] = actionFavoriteRun
}
t.trackOld(eventSidebarCategory, properties)
}
// UnfavoriteItem tracks run unfavoriting of an item. Item can be run or a playbook
func (t *RudderTelemetry) UnfavoriteItem(item app.CategoryItem, userID string) {
properties := map[string]interface{}{}
properties["UserActualID"] = userID
switch item.Type {
case app.PlaybookItemType:
properties["PlaybookID"] = item.ItemID
properties["Action"] = actionUnfavoritePlaybook
case app.RunItemType:
properties["PlaybookRunID"] = item.ItemID
properties["Action"] = actionUnfavoriteRun
}
t.trackOld(eventSidebarCategory, properties)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package timeutils
import (
"fmt"
"math"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
func GetTimeForMillis(unixMillis int64) time.Time {
return time.Unix(0, unixMillis*int64(1000000))
}
func DurationString(start, end time.Time) string {
duration := end.Sub(start).Round(time.Second)
if duration.Seconds() < 60 {
return "< 1m"
}
if duration.Minutes() < 60 {
return fmt.Sprintf("%.fm", math.Floor(duration.Minutes()))
}
if duration.Hours() < 24 {
hours := math.Floor(duration.Hours())
minutes := math.Mod(math.Floor(duration.Minutes()), 60)
if minutes == 0 {
return fmt.Sprintf("%.fh", hours)
}
return fmt.Sprintf("%.fh %.fm", hours, minutes)
}
days := math.Floor(duration.Hours() / 24)
duration %= 24 * time.Hour
hours := math.Floor(duration.Hours())
minutes := math.Mod(math.Floor(duration.Minutes()), 60)
if minutes == 0 {
if hours == 0 {
return fmt.Sprintf("%.fd", days)
}
return fmt.Sprintf("%.fd %.fh", days, hours)
}
if hours == 0 {
return fmt.Sprintf("%.fd %.fm", days, minutes)
}
return fmt.Sprintf("%.fd %.fh %.fm", days, hours, minutes)
}
func GetUserTimezone(user *model.User) (*time.Location, error) {
key := "automaticTimezone"
if user.Timezone["useAutomaticTimezone"] == "false" {
key = "manualTimezone"
}
return time.LoadLocation(user.Timezone[key])
}
func IsSameDay(time1, time2 time.Time) bool {
return time1.YearDay() == time2.YearDay() && time1.Year() == time2.Year()
}
// getDaysDiff returns days difference between two date.
func GetDaysDiff(start, end time.Time) int {
days := int(end.Sub(start).Hours() / 24)
if start.AddDate(0, 0, days).YearDay() != end.YearDay() {
days++
}
return days
}